From ea713064e1a0b4030db3b4bc36302f407dc0c13a Mon Sep 17 00:00:00 2001 From: hzhang Date: Sun, 31 May 2026 20:32:41 +0100 Subject: [PATCH] fix(routing): resolveAgentRoute uses binding.accountId, not agent_id MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `socket.on('message.created', ...)` dispatched with `accountId: agentId` (the openclaw agent id, e.g. 'analyst2') instead of the binding's `match.accountId` (the fabric account slot label, e.g. 'interviewee'). For most agents the binding is `{agentId: X, accountId: X}` so the two coincide and the call works by accident. For shared-placeholder slots (the recruitment `interviewee` apikey reused across pre-onboard agents) the slot label is `interviewee` not the agent_id, so the lookup returns bindings=0 and openclaw core silently falls back to the `main` agent — which then handles the sub-discussion turn under main's workspace identity. Symptom: every sub-discussion interview reply masquerades as the human user's IDENTITY.md text. Walk cfg.bindings for the entry that ties this agentId to a fabric account; use its accountId. Fall back to agentId when the agent has no explicit fabric binding declared (preserves prior behavior for agents wired before the binding format was uniform). Verified on prod-t2 recruitment retest 2026-05-31: Before: routing log `accountId=analyst2 ... bindings=0`, main session ran instead of analyst2. After: `accountId=interviewee ... bindings=1`, analyst2 session ran (0 main sessions in sub channel). --- src/inbound.ts | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/src/inbound.ts b/src/inbound.ts index 1ce77f4..bbeefb8 100644 --- a/src/inbound.ts +++ b/src/inbound.ts @@ -51,6 +51,30 @@ type FabricMessage = { xType?: string; }; +// Walk cfg.bindings for the entry that ties `agentId` to a fabric account. +// Returns the binding's match.accountId (the slot label routing keys on); +// returns undefined when the agent has no explicit fabric binding so the +// caller can fall back to agentId without changing pre-existing semantics +// for agents whose binding accountId == agent_id anyway. +function findFabricBindingAccountId(cfg: unknown, agentId: string): string | undefined { + const bindings = (cfg as { bindings?: Array<{ + agentId?: string; + match?: { channel?: string; accountId?: string }; + }> })?.bindings; + if (!Array.isArray(bindings)) return undefined; + for (const b of bindings) { + if ( + b?.agentId === agentId && + b?.match?.channel === 'fabric' && + typeof b?.match?.accountId === 'string' && + b.match.accountId.length > 0 + ) { + return b.match.accountId; + } + } + return undefined; +} + export class FabricInbound { private sockets: Socket[] = []; private seen = new Set(); @@ -541,10 +565,22 @@ export class FabricInbound { // (commands-handlers `isDirectMessage` checks ChatType==='direct') // misclassifies the turn. const { peerKind, chatType } = fabricPeerRoutingForXType(m.xType); + // resolveAgentRoute needs the *binding* accountId (the channel-side + // slot name) — not the openclaw agentId. For most agents the binding + // is `{agentId: X, match: {channel: fabric, accountId: X}}` so the + // two coincide; but for shared-placeholder cases (e.g. the recruitment + // `interviewee` slot bound to multiple agents over its lifetime) the + // binding accountId is the slot label ("interviewee", "Neon", …) not + // the agent_id. Passing agentId there returned bindings=0 and silently + // fell back to `main`, hijacking sub-discussion turns. Look up the + // agent's fabric binding accountId here; fall back to agentId when no + // explicit binding exists (preserves prior behavior for agents with + // no fabric binding declared). + const bindingAccountId = findFabricBindingAccountId(this.cfg, agentId) ?? agentId; const route = core.channel.routing.resolveAgentRoute({ cfg: this.cfg, channel: 'fabric', - accountId: agentId, + accountId: bindingAccountId, peer: { kind: peerKind, id: channelId }, }); const storePath = core.channel.session.resolveStorePath(cfg.session?.store, { -- 2.49.1