From dbe053fe082f799102a6f73c72011b9937099eb4 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sun, 2 Aug 2026 13:58:22 +0800 Subject: [PATCH] refactor(host): share the fenced-live-agent resolution between agentFor paths The live fast-path fence and the raced-collision catch duplicated the same subagent-ownership classification, tripping the duplication gate. Extract `fencedLiveAgent` so both paths resolve one live identity through the fence identically. --- packages/host/apiproxy/src/api-proxy.ts | 34 ++++++++++++++----------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/packages/host/apiproxy/src/api-proxy.ts b/packages/host/apiproxy/src/api-proxy.ts index 14f03b3773..a84e1d5c47 100644 --- a/packages/host/apiproxy/src/api-proxy.ts +++ b/packages/host/apiproxy/src/api-proxy.ts @@ -1065,17 +1065,24 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro return inspected } - async function agentFor(sessionId: SessionId): Promise<{ agent: Agent } | { error: RpcError }> { + /** + * Resolve one live registered identity through the subagent-ownership + * fence: subagent-owned agents answer `agent-busy`, plain agents pass. + * Fences the live agent's own session rather than trusting a + * "registered ⇒ attached-store" invariant — a registered subagent whose + * session is ever absent from the attached store must still not be handed + * out through generic Host routing. `undefined` means no live agent. + */ + function fencedLiveAgent(sessionId: SessionId): { agent: Agent } | { error: RpcError } | undefined { const live = ctx.agents.get(sessionId) - if (live !== undefined) { - // Fence the live agent's own session rather than trusting a - // "registered ⇒ attached-store" invariant: a registered subagent whose - // session is ever absent from the attached store must still not be - // handed out through generic Host routing (ensureSession's `.catch` - // already fences `live.session`; this is the same check on the fast path). - if (hasSubagentOwner(live.session, live)) return { error: subagentOwnershipError(sessionId) } - return { agent: live } - } + if (live === undefined) return undefined + if (hasSubagentOwner(live.session, live)) return { error: subagentOwnershipError(sessionId) } + return { agent: live } + } + + async function agentFor(sessionId: SessionId): Promise<{ agent: Agent } | { error: RpcError }> { + const fenced = fencedLiveAgent(sessionId) + if (fenced !== undefined) return fenced const attached = ctx.sessions.get(sessionId) if (attached !== undefined && hasSubagentOwner(attached, undefined)) { return { error: subagentOwnershipError(sessionId) } @@ -1119,11 +1126,8 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro // rejection falls through here. Mirror ensureSession's `.catch` in // full: classify a subagent-owned winner into the stable ownership // error, and hand a clean plain-agent winner straight back. - const live = ctx.agents.get(sessionId) - if (live !== undefined) { - if (hasSubagentOwner(live.session, live)) return { error: subagentOwnershipError(sessionId) } - return { agent: live } - } + const fenced = fencedLiveAgent(sessionId) + if (fenced !== undefined) return fenced const attached = ctx.sessions.get(sessionId) if (attached !== undefined && hasSubagentOwner(attached, undefined)) { return { error: subagentOwnershipError(sessionId) }