Skip to content

detachClient steals other attachers' attachCount, causing premature session kill in daemon serve mode #7385

Description

@doudouOUC

Summary

detachClient in packages/acp-bridge/src/bridge.ts unconditionally decrements entry.attachCount and then unregisters the client. The decrement is completely decoupled from whether the detach actually released a live attach reference, so several perfectly legal call patterns steal another attacher's count:

  • Duplicate detach: the same client detaching twice (e.g. route handler + disconnect reaper both firing) decrements twice for a single attach.
  • Unknown clientId: a stray DELETE with a bogus X-Qwen-Client-Id still decrements.
  • Anonymous detach: a DELETE without X-Qwen-Client-Id still decrements.
  • Spawn owner self-detach: the spawn owner never contributes to attachCount (it stays 0 for the owner), yet its own DELETE detach decrements the counter that belongs to other attachers.

Impact

Once the counter is stolen down to 0 while real attachers are still connected, two kill paths fire early:

  1. spawnOwnerWantedKill (the deferred-reap tombstone set when the owner's killSession({requireZeroAttaches: true}) bails) completes the reap and kills a session that still has live clients.
  2. The close-on-last-detach path (clientIds.size === 0) becomes reachable earlier than it should.

Either way a live client's session is torn down underneath it — every subsequent request from that client 404s.

Reproduction (unit level)

With client A spawning (owner) and client B attached (attachCount === 1):

await bridge.detachClient(b.sessionId, b.clientId); // 1 -> 0, fine
await bridge.detachClient(b.sessionId, b.clientId); // duplicate: decrements again on pre-fix code, stealing the next attacher's count

Or more directly: after killSession(a.sessionId, { requireZeroAttaches: true }) bails and sets the tombstone, a detach with an unknown/anonymous clientId completes the deferred reap even though B never left.

Expected behavior

Every attachCount decrement must correspond to the release of a real attach reference, keyed by clientId. Detaches that don't hold such a reference (duplicates, unknown ids, anonymous requests, owner-style registrations) must leave the counter untouched — while still removing the client registration itself, which is idempotent.

Environment

  • Affects the ACP bridge daemon serve mode (packages/acp-bridge/src/bridge.ts), present on current main.

Metadata

Metadata

Assignees

Labels

category/coreCore engine and logicpriority/P2Medium - Moderately impactful, noticeable problemscope/session-managementSession state and persistencetype/bugSomething isn't working as expectedwelcome-pr

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions