Summary
open_session gives a caller no way to choose the durable session id. The host always mints its own uuidv7, so every embedder that already has a stable record id for the conversation (a desktop thread, a server-side job row, an external tracker item) is forced to keep a SECOND identity and a translation layer between the two. Every feature that crosses the boundary — goal files, subagent/team matching, resume cursors, adoption provenance — then has to carry the mapping, and when the mapping is lost the user-visible result is a dead-end turn rather than a recoverable error.
Expected (the ideal state)
open_session accepts an optional caller-chosen durable id, negotiated by capability, validated, and refused cleanly when it would collide.
Acceptance criteria:
open_session accepts an optional durableSessionId. When present and the session is being CREATED, the session's durable id IS that value: the open_session reply carries state.sessionId === durableSessionId and the JSONL header records it.
- The field is named
durableSessionId, never sessionId. session-command-router.ts:250 reads "sessionId" in command generically and feeds it to drain gating (:252), active-request accounting (:256-260) and runWithSessionAttribution (:257); a caller-chosen durable id arriving under that key would be treated as a routing handle. The name also matches what list_sessions already publishes.
- Advertised as host capability
durable_session_id in get_protocol_info, so a client can detect support instead of guessing.
- Validation: a malformed id is refused with a stable
invalid_session_id code, using the existing assertValidSessionId rule.
- Collision safety: if a LIVE session already holds that durable id, the open is refused with a stable
session_id_in_use code. Two live sessions sharing one durable id would collide every per-session artifact keyed by it.
- Resume is unaffected: when
sessionPath names an existing session file, the header's id stays authoritative and durableSessionId is ignored rather than rewriting history.
- Path reservations remain the single-writer authority; this change adds an id guard, it does not replace
session_path_in_use.
- The D1 normative table and the D6 identity notes in
rpc-mode.ts document the field, the capability and both new error codes.
Actual
open_session params are sessionPath?, cwd?, provider?, modelId?, thinkingLevel?, permissionPreset?, retain_on_disconnect?, kind?, context?, auto_title? — no id field (packages/coding-agent/src/modes/rpc/rpc-types.ts:267-313).
- The id is always host-minted:
createSessionId() { return uuidv7() } (packages/coding-agent/src/core/session-manager.ts:313-315), assigned at :1031.
Evidence
- Id injection already exists in-process but is unreachable over RPC:
NewSessionOptions.id (core/session-manager.ts:99-102), honored in _resetToNewSession (:1025-1031) behind assertValidSessionId (:317-322); SessionManager.create(cwd, sessionDir?, options?) already takes it (:2052).
- The RPC seam that drops it:
modes/rpc/session-registry.ts:341 — sessionPath ? SessionManager.open(sessionPath, undefined, cwd) : SessionManager.create(cwd), neither call passing NewSessionOptions.
SessionManager.open(path, sessionDir?, cwdOverride?) (:2063) has no options parameter at all. This is the branch that matters: a client that names its own session file always lands here, and a missing file falls through to _setSessionFile -> _resetToNewSession() (:1007) with no options, minting a fresh id.
- When the file already exists the header id is adopted (
:991), which is why resume must stay untouched.
Root cause
The RPC surface was designed with the path as the only caller-supplied identity. Id injection was implemented one layer below (the session manager and the session repo) and never lifted to the wire.
Scope
In: the open_session field, capability advertisement, validation, the live-id collision guard, threading the id through RpcSessionLaunchProfile into both manager constructors, docs, tests.
Out: changing how ids are generated when the caller supplies none; any change to path reservation semantics; any client-side adoption (tracked separately in the desktop repo).
Related
Summary
open_sessiongives a caller no way to choose the durable session id. The host always mints its ownuuidv7, so every embedder that already has a stable record id for the conversation (a desktop thread, a server-side job row, an external tracker item) is forced to keep a SECOND identity and a translation layer between the two. Every feature that crosses the boundary — goal files, subagent/team matching, resume cursors, adoption provenance — then has to carry the mapping, and when the mapping is lost the user-visible result is a dead-end turn rather than a recoverable error.Expected (the ideal state)
open_sessionaccepts an optional caller-chosen durable id, negotiated by capability, validated, and refused cleanly when it would collide.Acceptance criteria:
open_sessionaccepts an optionaldurableSessionId. When present and the session is being CREATED, the session's durable id IS that value: theopen_sessionreply carriesstate.sessionId === durableSessionIdand the JSONL header records it.durableSessionId, neversessionId.session-command-router.ts:250reads"sessionId" in commandgenerically and feeds it to drain gating (:252), active-request accounting (:256-260) andrunWithSessionAttribution(:257); a caller-chosen durable id arriving under that key would be treated as a routing handle. The name also matches whatlist_sessionsalready publishes.durable_session_idinget_protocol_info, so a client can detect support instead of guessing.invalid_session_idcode, using the existingassertValidSessionIdrule.session_id_in_usecode. Two live sessions sharing one durable id would collide every per-session artifact keyed by it.sessionPathnames an existing session file, the header's id stays authoritative anddurableSessionIdis ignored rather than rewriting history.session_path_in_use.rpc-mode.tsdocument the field, the capability and both new error codes.Actual
open_sessionparams aresessionPath?,cwd?,provider?,modelId?,thinkingLevel?,permissionPreset?,retain_on_disconnect?,kind?,context?,auto_title?— no id field (packages/coding-agent/src/modes/rpc/rpc-types.ts:267-313).createSessionId() { return uuidv7() }(packages/coding-agent/src/core/session-manager.ts:313-315), assigned at:1031.Evidence
NewSessionOptions.id(core/session-manager.ts:99-102), honored in_resetToNewSession(:1025-1031) behindassertValidSessionId(:317-322);SessionManager.create(cwd, sessionDir?, options?)already takes it (:2052).modes/rpc/session-registry.ts:341—sessionPath ? SessionManager.open(sessionPath, undefined, cwd) : SessionManager.create(cwd), neither call passingNewSessionOptions.SessionManager.open(path, sessionDir?, cwdOverride?)(:2063) has no options parameter at all. This is the branch that matters: a client that names its own session file always lands here, and a missing file falls through to_setSessionFile->_resetToNewSession()(:1007) with no options, minting a fresh id.:991), which is why resume must stay untouched.Root cause
The RPC surface was designed with the path as the only caller-supplied identity. Id injection was implemented one layer below (the session manager and the session repo) and never lifted to the wire.
Scope
In: the
open_sessionfield, capability advertisement, validation, the live-id collision guard, threading the id throughRpcSessionLaunchProfileinto both manager constructors, docs, tests.Out: changing how ids are generated when the caller supplies none; any change to path reservation semantics; any client-side adoption (tracked separately in the desktop repo).
Related
senpi hostCLI #1782 — shared RPC host capability-compat umbrella;durable_session_idis a sibling ofsession_kind/session_context/retain_on_disconnect.