From 56e252bed35cec9b304af92f389ecd01ac357e20 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sun, 2 Aug 2026 12:22:20 +0800 Subject: [PATCH] fix(host): fence the agentFor live fast path on the agent's own session MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `agentFor` fenced subagent ownership through the attached session store (`ctx.sessions.get`) and only then returned a live registered agent. A registered agent whose session is ever absent from the attached store — an invariant nothing in this package guarantees — would therefore be handed out through generic Host routing unfenced, bypassing subagent delivery entirely. Fence `live.session` directly whenever a live agent exists, and keep the attached-store check only for the not-live durable classification. `ensureSession`'s race `.catch` already fences `live.session`; this makes the fast path the same check instead of an asymmetric weaker one. --- packages/host/apiproxy/src/api-proxy.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/host/apiproxy/src/api-proxy.ts b/packages/host/apiproxy/src/api-proxy.ts index 70661bb027..96e040d2a2 100644 --- a/packages/host/apiproxy/src/api-proxy.ts +++ b/packages/host/apiproxy/src/api-proxy.ts @@ -1106,6 +1106,19 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro if (error instanceof SubagentSessionOwnership) { return { error: subagentOwnershipError(error.sessionId) } } + // A concurrent parent `enter()` can win the identity between the + // pre-resume published re-check and `ctx.agents.resume` publication; + // the ID-collision rejection falls through here. Re-classify that + // raced published winner into the stable ownership error, mirroring + // ensureSession's `.catch`. + const live = ctx.agents.get(sessionId) + if (live !== undefined && hasSubagentOwner(live.session, live)) { + return { error: subagentOwnershipError(sessionId) } + } + const attached = ctx.sessions.get(sessionId) + if (attached !== undefined && hasSubagentOwner(attached, undefined)) { + return { error: subagentOwnershipError(sessionId) } + } // The internal details slot is contractually {}; the reason rides the message. return { error: { code: 'internal', message: `resume failed for session "${sessionId}": ${String(error)}`, details: {} } } }