Channel workers keep an SSE event pump attached to every cached route, so the idle reaper treated those sessions as actively watched and never reclaimed them. A workspace could fill every maxSessions slot with sessions nobody was using and then reject new creates with 503. Let the reaper look past a channel-owned subscriber while preserving the local-work, active-prompt, and child conditional-close guards. Surface session_limit_exceeded as a banner with a release action. The release dialog falls back to the workspace daemon client when it opens from the empty new-session screen, where no session client exists. Design: docs/design/channel-session-idle-reclaim.md
@qwen-code/acp-bridge
Shared ACP bridge primitives consumed by qwen serve, channels, IDE, TUI,
and remote-control adapters. Lives in the monorepo, not published to npm.
Lift history (#4175 Mode B daemon roadmap):
| Slice | Scope | Status |
|---|---|---|
| PR 22a (#4295) | Skeleton + EventBus + inMemoryChannel + AcpChannel types + PermissionMediator type-only stub |
✅ merged |
| PR 22b/1 (#4298) | Lift status + workspacePaths + bridgeErrors + bridgeTypes |
✅ merged |
| PR 22b/2 (#4304) | Lift BridgeOptions + new DaemonStatusProvider injection seam |
✅ merged |
| F1 (#4490) | Lift defaultSpawnChannelFactory + BridgeClient + createHttpAcpBridge factory closure + new BridgeFileSystem injection seam (22b' scope) |
✅ merged |
| F3 PR 24 | Implement the four PermissionMediator policies (first-responder, designated, consensus, local-only) plus audit/emit fan-out; pair-token binding and revocation stay future scope |
✅ implemented |
What's here today
eventBus— per-session NDJSON pub/sub with bounded ring replay,Last-Event-IDreconnect, and slow-client backpressure (slow_client_warning→client_evicted).inMemoryChannel— paired NDJSON streams without spawning a child; used for in-process bridge tests and the parked Mode A (qwen --serve) path.channel—AcpChannel/AcpChannelExitInfo/ChannelFactorytype contract thatcreateHttpAcpBridge(now in this package) plus the channels / VSCode IDE companion's own-spawn paths consume viaBridgeOptions.channelFactory.permission—PermissionMediatorinterface,PermissionPolicyliteral union (4 strategies), andPermissionResolutiondiscriminated union.MultiClientPermissionMediatorimplements the four policies, owns pending/resolved permission state, and handles strategy dispatch plus audit/emit fan-out;BridgeClientonly plumbsrequestPermissionintomediator.request.status(PR 22b/1) — wire-contract status types for/workspace/{mcp,skills,providers,env,preflight}and/session/:id/{context,supported-commands,tasks}routes, theSTATUS_SCHEMA_VERSION/SERVE_*_EXT_METHODSconstants,BridgeTimeoutError/MissingCliEntryError/BridgeChannelClosedErrortyped exceptions, and themapDomainErrorToErrorKindclassifier (regex →instanceofafter #4299 / #4300). The 27-symbol contractacp-integration/acpAgent.tsconsumes lives here.workspacePaths(PR 22b/1) —canonicalizeWorkspace(the cross-module BX9_q contract used byconfig.ts/settings.ts/sandbox.ts/ bridge to collapse boot-time + per-request workspace paths to one canonical key) plusMAX_WORKSPACE_PATH_LENGTH.bridgeErrors(PR 22b/1) — 11 typedErrorsubclasses the bridge throws (SessionNotFoundError,WorkspaceMismatchError,RestoreInProgressError, etc.); HTTP route layerinstanceof-branches on these to map to specific status codes.bridgeTypes(PR 22b/1) — public bridge contract types:BridgeSpawnRequest,BridgeSession,BridgeRestoreSessionRequest,BridgeSessionState,BridgeRestoredSession,BridgeSessionSummary,SessionMetadataUpdate,BridgeClientRequestContext,BridgeHeartbeatResult,BridgeHeartbeatState, plus theHttpAcpBridgeinterface itself (~30-method facade).bridgeOptions(PR 22b/2) —BridgeOptionsinterface (factory construction contract:boundWorkspace,channelFactory,maxSessions,eventRingSize,permissionResponseTimeoutMs, persistence callbacks, etc.) plus theDaemonStatusProviderinjection seam for daemon-host env / preflight cells (production impl incli/src/serve/daemon-status-provider.ts) and the F1BridgeFileSysteminjection seam for the ACP fs proxy.delegateReadTextFileToClientdefaults totrue; same-host daemon callers may set it tofalseso child text reads use the regular CLI filesystem service while final ACP text writes remain delegated.spawnChannel(F1) —defaultSpawnChannelFactory+killChild+SCRUBBED_CHILD_ENV_KEYSdenylist +scrubChildEnvpure env-policy helper (exported for adapter reuse + unit-test access; isolates the scrub + override + defense-in-depth ordering invariant the security argument relies on). Production spawn of theqwen --acpchild with stderr prefix-and-forward, kill cascade, and env passthrough. Channels (packages/channels/base/AcpBridge.ts) and the VSCode IDE companion consume this directly instead of each reimplementing the child lifecycle.bridgeClient(F1) —BridgeClientclass implementing the ACPClientsurface: permission requests delegated toPermissionMediator, session-update fan-out intoEventBus, child-sideextNotificationrouting, early-event buffer + tombstone bookkeeping, inline fs proxy forwriteTextFile/readTextFile. Exports the supportingBridgeClientSessionEntrytype consumed by the session-entry lookup passed in by the factory.bridge(F1) —createHttpAcpBridgefactory closure (~3000 LOC)ChannelInfo/SessionEntryinterfaces + factory-only helpers (withTimeout,canonicalizeExistingAncestor,verifyParentWithinWorkspace, debug log helpers,hasControlCharacter) + factory constants. Owns session bookkeeping, constructs theMultiClientPermissionMediatorthat owns permission state, and passes the session-entry lookup intoBridgeClient.
bridgeFileSystem(F1) —BridgeFileSysteminterface for the ACP fs proxy. When wired throughBridgeOptions.fileSystem,BridgeClient.readTextFile/BridgeClient.writeTextFiledelegate to it instead of the inlinefs.realpath/fs.writeFile/fs.readFileproxy. Productionqwen serveinjectsWorkspaceFileSystemfor final ACPwriteTextFilecontent writes and for defensive handling of unexpected or capability-violating delegated reads; normal same-host text reads stay in the child. The interface remains workspace-scoped by default. A daemon-owned adapter may recognize the strict versioned provenance of a final built-in-tool write and use its same-host writer for an external target; this is not a generic ACP capability or authorization token.
Imports — root vs subpaths
The package exposes both a barrel root (@qwen-code/acp-bridge) and
per-module subpaths (/eventBus, /inMemoryChannel, /channel,
/permission, /status, /workspacePaths, /bridgeErrors,
/bridgeTypes, /bridgeOptions, /spawnChannel, /bridgeClient,
/bridge, /bridgeFileSystem). They re-export the same symbols, so
either form resolves to the same module at runtime. Pick by intent:
- Root for application/test code that uses several primitives at
once — concise and matches how
serve/imports landed today. - Subpaths for client adapters (TUI / channels / IDE / future
remoteControl) that only consume one slice — keeps the dependency surface explicit and lets bundlers tree-shake the rest.
Both variants are stable across the F1 lift.
Backward compatibility
CLI code imports event-bus and in-memory channel primitives directly from
@qwen-code/acp-bridge/eventBus and
@qwen-code/acp-bridge/inMemoryChannel.
packages/cli/src/serve/acp-session-bridge.ts remains as the CLI-local
compatibility facade for the broader bridge surface, forwarding previously
exported symbols (createHttpAcpBridge, defaultSpawnChannelFactory,
BridgeClient, typed errors, and type aliases) from the lifted subpaths.
See also
- #4175 Mode B daemon roadmap (feature-cohesive F1-F5 plan targeting
daemon_mode_b_main) - #3803
Stage 1.5-prereq AcpChannel lift(chiga0's original framing) permissionMediator.tsimplements the fourPermissionMediatorstrategies declared inpermission.ts;BridgeClient.requestPermissiondelegates to that mediator.