fix(host): fence the agentFor live fast path on the agent's own session

`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.
This commit is contained in:
Tianyi Cui
2026-08-02 12:22:20 +08:00
parent 4b2fa3317e
commit 56e252bed3

View File

@@ -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: {} } }
}