fix(acp): share Hermes process across independent agent sessions - #3194
fix(acp): share Hermes process across independent agent sessions#3194desmond-rai wants to merge 4 commits into
Conversation
|
| Filename | Overview |
|---|---|
| packages/server/src/server/agent/providers/acp-agent.ts | Implements shared ACP host acquisition, session routing, lease-based lifecycle management, bounded probes, and process replacement. |
| packages/server/src/server/agent/providers/acp-agent.test.ts | Adds broad behavioral coverage for shared sessions, timeout cleanup, environment identity, routing, process exit, and deterministic lifecycle synchronization. |
| packages/server/src/server/agent/providers/generic-acp-agent.ts | Enables shared-process behavior only for the built-in Hermes provider. |
| packages/server/src/server/agent/provider-registry.ts | Threads the daemon-scoped Hermes process identity through built-in and derived provider construction. |
| packages/server/src/server/agent/provider-snapshot-manager.ts | Owns a stable process-sharing scope across provider snapshot rebuilds. |
Sequence Diagram
sequenceDiagram
participant A1 as Paseo Agent 1
participant A2 as Paseo Agent 2
participant C as ACPAgentClient
participant H as Shared Hermes Host
A1->>C: createSession()
C->>H: acquire lease
C->>H: newSession()
H-->>A1: native session 1
A2->>C: createSession()
C->>H: acquire lease
C->>H: newSession()
H-->>A2: native session 2
A1->>C: close session 1
C->>H: release lease 1
Note over H: Host remains alive for session 2
Reviews (4): Last reviewed commit: "fix(acp): preserve shared host across pr..." | Re-trigger Greptile
|
Added a third commit (15ada19): fix(acp): buffer shared-router session updates received before registration. While exercising this branch against a Hermes ACP agent that advertises slash commands via available_commands_update, the shared router dropped the notification: Hermes sends it immediately after the session/new response, before the client continuation registers the session, so the router threw 'unknown session' and the command batch (and early usage_update) was lost for the life of the session. The router now buffers up to 100 session updates per unregistered session and replays them in arrival order from register(); unregister() and notifyProcessExit() discard pending buffers. Requests for unknown sessions (permission prompts, file/terminal IO) still throw. Red-green regression test included ('delivers session updates that arrive before session registration'): fails on 3d4653a, passes with the fix. Full acp-agent + generic-acp-agent suites: 112 tests passed. Server typecheck clean. Related upstream fix for the non-shared path (same race at the session level): #3324. |
…ration Agents may push session-scoped notifications (for example available_commands_update) immediately after the session/new response, before the client continuation registers the session with the shared router. The router previously threw 'unknown session' and the daemon dropped the notification, so Hermes skill slash commands never reached the cached command list and the slash popup stayed empty. Buffer up to 100 pending session updates per unregistered session and replay them in arrival order from register(); unregister() and notifyProcessExit() discard pending buffers. Requests for unknown sessions (permission prompts, file and terminal IO) still throw.
15ada19 to
bf2e690
Compare
Linked issue
Closes #3193
Type of change
Reasoning
Paseo's built-in Hermes integration currently launches one ACP subprocess per Paseo agent. That normally gives providers process isolation, but Hermes's durable profile is shared outside the workspace. Parallel agents therefore become concurrent OS-process writers to the same profile-level memory and configuration even when every agent has a separate Paseo workspace and Git worktree.
The user-visible workflow is ordinary Paseo parallelism: open Paseo, select Hermes, and start several agents. Users reasonably expect separate conversations, but they should not need to know that repository isolation does not isolate a provider's global durable state or that the built-in provider requires a one-process rule.
Hermes's ACP server already supports the primitive needed to fix this without merging conversations: one transport can host multiple native ACP sessions. This change makes that process sharing an explicit ACP-client capability and enables it only for the built-in
hermesprovider. Each Paseo agent still creates and owns a separate native ACP session, transcript, turn state, permissions, terminals, and cancellation path. The shared process only centralizes ownership of the profile writer.The alternatives did not preserve the intended workflow:
~/.hermesor another provider home.startTurn(), so a turn gate cannot prevent concurrent profile writers. It also defeats parallel work.The implementation keeps ordinary ACP providers on the existing one-process-per-agent path. Process sharing is opt-in in
ACPAgentClient;GenericACPAgentClientenables it only whenproviderId === "hermes".What changes
ClientSideConnectionfor many independent native ACP sessions.PASEO_AGENT_ID,PASEO_AGENT_CWD,PASEO_WORKSPACE_ID) from shared launch identity; other provider environment values remain part of the key and launch environment.Goals
Non-goals
QA
Automated provider coverage
Commands run from the rebased feature worktree at
40b97e09893c7887cecc599af2da98c5053542c1:The tests cover:
Static and build checks
The root typecheck script runs protocol generation and consumers concurrently. In this checkout that produced transient missing generated-message types. Running protocol generation first and each consumer sequentially passed. This PR does not touch protocol or generated files.
Full server-suite reconciliation
A highly parallel full server run completed 319 files and 4,701 tests, with 41 timeout/path failures concentrated in seven unrelated supervisor/workspace/WebSocket/Hub files:
Every affected area was then rerun serially with a canonical macOS temp directory:
Real Hermes acceptance: source build
I ran ten real Paseo agents in parallel through matching server and CLI binaries, using Hermes 0.20.0 and
deepseek:deepseek-v4-flash. A process-tree monitor started before provider discovery and sampled the daemon descendants throughout the run.Real Hermes acceptance: packaged installed artifact
I built the macOS Electron package, installed that artifact, launched its bundled daemon and CLI on isolated disposable Paseo state, and repeated the ten-agent test against the packaged code rather than the source server.
Durable-memory behavior
Using a disposable Hermes profile:
hermes acpprocess.No acceptance marker was written to the user's normal Hermes profile.
Lifecycle and regression checks
mockprovider agent completed normally on the independent-process path.PASSafter timeout, termination ownership, environment reuse, and pending-permission lifecycle findings were fixed and covered by tests.Greptile's first public review identified that a timed-out management probe could invalidate a healthy shared host and fail unrelated active sessions. Commit
3d4653a70now releases the abandoned probe lease without invalidating a host that has other references; an isolated host is still invalidated and replaced. A new regression test keeps an agent session attached while a catalog probe hangs and verifies that the process is not terminated. The same review flagged fixed-duration sleeps in timeout tests; all of those sleeps were replaced with explicit deferred synchronization. The updated focused suite is 111/111, and the full pre-commit lint, format, and workspace typecheck hook passes.Platforms
Checklist
npm run typecheckpasses when protocol generation and dependent workspaces run sequentially; see QA note about the root script's generation racenpm run lintpassesnpm run format/npm run format:checkpasses