Skip to content

rpc: durable_session_id guard missing on WorkerSessionRegistry, and a caller-chosen id is not written to disk until the first assistant message #2010

Description

@code-yeongyu

Summary

Two gaps in the open_session.durableSessionId contract shipped in #1956 (#1951), both found by driving the REAL senpi --mode rpc --multi-session process over its stdio protocol rather than the registry unit harness.

Reproduction

Real host (.agents/skills/senpi-qa/scripts/scenarios/durable-session-id-qa.mjs style), hermetic sandbox:

  1. open_session { sessionPath: A, durableSessionId: X } → success, state.sessionId === X.
  2. open_session { sessionPath: B, durableSessionId: X } (a DIFFERENT path, same id, first session still live)
    • Expected: refused with session_id_in_use.
    • Actual: success: true. Two live sessions now share durable id X.
  3. close_session on the first, then open_session { sessionPath: A } with no id.
    • Expected: state.sessionId === X (the id the caller chose for that file).
    • Actual: a fresh uuidv7. A never materialized on disk because no assistant message arrived before the close, so the chosen identity evaporated.

Root cause

Gap 1 — the guard lives in the wrong registry. multi-session-host.ts:163-171 instantiates WorkerSessionRegistry whenever a worker configuration is present, which is the real host's normal shape. #1956 added the synchronous duplicate-live-id scan only to RpcSessionRegistry.openSession (session-registry.ts:229-236). WorkerSessionRegistry.openSession (worker-session-registry.ts:60-130) has no such scan; it forwards the profile to worker.prepare and only learns the durable id from snapshot.state.sessionId after commit (:123). The invalid_session_id refusal DOES work on the real host, but only because SessionManager.assertValidSessionId fires inside the worker and surfaces as open_failed: invalid_session_id.

The unit suite added in #1956 drives RpcSessionRegistry directly, so it stayed green while the real host bypassed the guard.

Gap 2 — a caller-chosen id is not durable until the first assistant message. SessionManager._persist defers creating an absent session file until an assistant message exists. That is the right default for a host-minted id (nothing else references it yet). It is the wrong default for a CALLER-chosen id: the caller already holds that id in its own records and expects the file to answer to it. Today the identity exists only in memory between open and first reply.

Expected (ideal state)

  1. WorkerSessionRegistry.openSession refuses a durableSessionId held by any non-closed entry with session_id_in_use, taken synchronously before its first await, and records the requested id on the entry before that await so a concurrent open sees it — the same shape RpcSessionRegistry has.
  2. WorkerSessionRegistry validates the id format at its own boundary with invalid_session_id (not the open_failed: wrapper), matching RpcSessionRegistry.validateProfile.
  3. When a session is CREATED under a caller-supplied id, its header is written to disk immediately, so a reopen of that path yields the chosen id even if no assistant message ever arrived.
  4. A real-host scenario script asserts all three so the two registries cannot drift again.

Related

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions