docs/design/acp-session-initialization-deadline.md
Status: implemented for PR3
Proposed PR title: fix(daemon): Cancel timed-out session initialization
The daemon bounds newSession, but the timeout historically rejected only the Bridge wrapper. The ACP request continued inside the child. A slow SessionStart command hook could therefore finish after the caller had already received init_timeout, publish a real child Session that the Bridge never registered, and leave hook descendants or other session resources alive.
This is different from an ACP channel teardown. A shared channel can own healthy sibling Sessions, so killing the channel at the first session timeout would turn one failed create into unrelated session loss. It is also different from the HookRunner and ACP process-tree fixes: those changes provide tree-aware cancellation and channel cleanup, but neither decides when session initialization should be cancelled.
This change makes the existing Bridge initialization budget authoritative for the standard trusted daemon-to-ACP path:
newSession request;AbortSignal;SessionStart hook execution and check it at initialization boundaries;The public API remains the existing init_timeout failure. The private deadline and internal child error kind do not become HTTP or SDK fields.
The Bridge writes qwen.daemon.sessionInitializationDeadlineMs into _meta immediately before dispatching the actual ACP request. Its value is an absolute Unix timestamp derived from the configured initializeTimeoutMs. An absolute deadline prevents serialization, transport, and child scheduling time from accidentally granting a new full budget at each layer.
Only an ACP Agent that completed the private managed-parent capability handshake reads the field. An untrusted or standalone ACP caller cannot use request metadata to cancel initialization. A trusted value must be a positive safe integer within Node's supported timer range; malformed values fail before settings or session state is created.
The Agent owns one request-scoped AbortController. Its timer is unreferenced and cleared in finally. The signal is not stored on the resulting Session and cannot cancel later turns.
The signal follows the existing initialization ownership path:
ACP newSession -> Config.initialize -> GeminiClient.initialize -> startChat -> SessionStart HookSystem -> HookRunner
Config checks cancellation before registering initialization state and after awaited initialization phases. GeminiClient passes the signal into SessionStart, checks it after the hook result, and rethrows its abort reason instead of applying the hook's ordinary best-effort error policy. This is required because HookSystem can aggregate cancellation into a result instead of throwing it directly.
Before QwenAgent publishes the new Session in its session map, it checks the signal again. The child reports the private session_initialization_timeout error kind, and the Bridge maps it back to the existing BridgeTimeoutError('newSession') contract.
The change does not race the whole Config initialization against a rejecting wrapper Promise. Doing so would let cleanup run concurrently with initialization code that still owns the same Config. Operations without an AbortSignal API finish normally and are followed by a cancellation checkpoint; the Bridge compatibility lifecycle remains the outer containment boundary.
The Bridge observes the raw ACP request after its public timer fires. This protects rolling upgrades and other Agents that do not yet consume the private deadline.
qwen/control/session/close for the returned Session ID, and only closed: true is accepted as proof that cleanup completed.The Bridge holds the fresh-session admission reservation and a caller-supplied ID reservation until the raw request and cleanup settle. Abandoned requests count toward maxSessions, and shutdown awaits their settlement after initiating channel teardown. This prevents retries from overcommitting resources or reclaiming an ID that a late child response can still create.
newSession times out and later succeeds: the child creates a Session that the Bridge cannot see.SessionStart and does not swallow cancellation as an ordinary hook failure.