fix(rpc): enforce durable_session_id on the worker session registry and write a chosen id to disk at open - #2013
Conversation
a3f30c3 to
d8941cf
Compare
Rebased onto main (
|
| gate | result |
|---|---|
rpc-worker-durable-session-id + rpc-open-session-durable-id + rpc-open-session-resume + rpc-worker-routing |
31/31 |
real-surface scenario (real senpi --mode rpc --multi-session over stdio) |
9/9 |
pgrep -f rpc-host-fixture.mjs after |
0 |
| changelog gate vs new base | PASS |
The earlier relocated-worker (windows-latest) failure was rmSync EBUSY on a compile-fixture temp dir in a test that never touches session code; its rerun passed, and every other recent run of that job is green — a Windows teardown flake, not this change.
Deferring the session file until an assistant message exists is the right default for a host-minted id, which nothing references yet. It is the wrong default for a CALLER-chosen id: the caller already holds that id in its own records, so the file has to answer to it now. Measured on the real multi-session host: create under a chosen id, close before the first reply, reopen the path - the file did not exist and the host minted a fresh uuidv7, so the id the caller stored pointed at nothing. When `NewSessionOptions.id` is present on a not-yet-existing path, write the header at once.
only. multi-session-host.ts instantiates WorkerSessionRegistry whenever a worker configuration is present - the real host's normal shape - and that registry forwarded the profile straight to worker.prepare. On the real host two live sessions could share one durable id; a malformed id was refused only as `open_failed: invalid_session_id` after the worker had already died. Both checks now run in WorkerSessionRegistry.openSession synchronously before its first await, mirroring RpcSessionRegistry: the requested id is recorded on the entry before the await so a concurrent open sees it, and snapshot.state.sessionId still overwrites it after commit. A new real-surface scenario drives the actual `senpi --mode rpc --multi-session` process over stdio and asserts the whole contract, including the header id on disk, so the two registries cannot drift again. Fixes #2010
…eleased] The entry was intended in the previous commit but the string replacement matched nothing because the [Unreleased] block orders Fixed before Added; the changelog gate caught it.
d8941cf to
d178a50
Compare
What
Completes the
open_session.durableSessionIdcontract from #1956 on the registry the multi-session host actually runs, and makes a caller-chosen id durable from the moment of open.Fixes #2010
How this was found
Not by a unit test. The registry-level suite from #1956 was green. I drove the REAL
senpi --mode rpc --multi-sessionprocess over its stdio protocol with a new scenario script (.agents/skills/senpi-qa/scripts/scenarios/durable-session-id-qa.mjs, zero model tokens) and it scored 6/8:Root causes
1. The guard was on the wrong registry.
multi-session-host.ts:163-171instantiatesWorkerSessionRegistrywhenever a worker configuration is present — the real host's normal shape. #1956 added the duplicate-live-id scan only toRpcSessionRegistry.openSession.WorkerSessionRegistry.openSessionforwarded the profile straight toworker.prepare, so two live sessions could share one durable id. The malformed-id refusal did fire on the real host, but only asopen_failed: invalid_session_idfrom the worker's ownSessionManagerafter the worker had already died.2. A caller-chosen id was not on disk until the first assistant message.
SessionManager._persistdefers creating the file — correct for a host-minted id that nothing references yet, wrong for an id the caller already holds in its own records. Create → close before the first reply → reopen the same path minted a different identity.Fixes
worker-session-registry.ts:openSessionvalidates the id withassertValidSessionId(→invalid_session_id) and refuses an id any non-closed entry holds (→session_id_in_use), both synchronously before its first await, mirroringRpcSessionRegistry. The requested id is recorded on the entry before that await so a concurrent open sees it;snapshot.state.sessionIdstill overwrites it after commit (so a resume keeps its header id). Re-opening the same path stays an attach.session-manager.ts: whenNewSessionOptions.idis present on a not-yet-existing path, the header is written at open.Evidence
RED first — new
test/suite/rpc-worker-durable-session-id.test.tsagainst the real worker registry (real Worker threads, production routing), 3 cases, 3 failed before the change:After:
rpc-worker-durable-session-id.test.ts(real workers)senpiprocess over stdio)rpc-open-session-durable-id+rpc-open-session-resume+rpc-worker-capacity+rpc-worker-routing+rpc-retain-on-disconnect+rpc-close-orderingpgrep -f rpc-host-fixture.mjs | wc -lafter the runLesson recorded
When a contract lives behind an interface with two implementations, the invariant goes on both — and only a real-process scenario proves which one actually runs. The scenario script stays in the repo so the two registries cannot drift again.
Summary by cubic
Fixes the durable session id contract on
WorkerSessionRegistry, the registry the real multi-session host runs, so the duplicate-live-id and malformed-id guards apply there too. A caller-chosen id is now written to disk at open, so create → close → reopen keeps the same identity.Bug Fixes
WorkerSessionRegistry.openSessionrefuses a duplicate live id withsession_id_in_useand a malformed id withinvalid_session_id, synchronously before spawning a worker, mirroringRpcSessionRegistry.[Unreleased].Written for commit d178a50. Summary will update on new commits.