From 1b950fdb6312185c42e7962c7ccac055ca9e2ad5 Mon Sep 17 00:00:00 2001 From: Yuto Nishida Date: Sun, 27 Sep 2026 14:59:14 -0700 Subject: [PATCH] dsh-plugins fix-workspace-multiple-writers: note the cross-instance session reuse trap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clicking a workspace in a second instance can open a session the first instance owns, and the first prompt then fails with `SessionAlreadyOwnedError`. Records the mechanism: `connectWorkspace` returns an existing session id instead of creating one whenever its scan matches, the `blank` term in that scan is the client's conservative local mirror rather than the session's real state, and `workspace.sessionIds` — which this plugin is what makes cross processes — supplies the eligibility. Notes that a session log has exactly one live writer, that the trap is reachable without this plugin at all because the workspace account is loaded from the shared store at boot, that archiving the peer's session is the workaround, and where the fix belongs. --- .../README.md | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/dsh-plugins/dsh-plugin-fix-workspace-multiple-writers/README.md b/dsh-plugins/dsh-plugin-fix-workspace-multiple-writers/README.md index a76dc7e..3b3c091 100644 --- a/dsh-plugins/dsh-plugin-fix-workspace-multiple-writers/README.md +++ b/dsh-plugins/dsh-plugin-fix-workspace-multiple-writers/README.md @@ -52,6 +52,35 @@ Consequences worth knowing: locally unvalidatable", so it declines to attach to any workspace it already knows. Only newly created workspaces are attached to, since a fresh record is empty and cannot be pruned. +- **Opening a workspace can land on a session another instance owns.** Clicking a + workspace — including its "New Session" — routes through + `dsh-client-ui-workspace`'s `connectWorkspace`, which *reuses* any session with + `summary.blank`, a matching `summary.cwd`, membership in + `workspace.sessionIds`, and no archive entry; it calls + `sessions.create({ workspaceId })` only when that scan finds nothing. The + `blank` it consults is the **client's local mirror**, not the session's state: + that mirror is documented as "unknown bare sessions begin conservatively + blank" and is cleared only by evidence this client has engaged the session. So + a session another instance created is a reuse candidate here — this plugin is + what puts its id in `workspace.sessionIds` — even though it is not blank on + disk (its projection records `sessionListMetadata.blank: false` and its log + holds a `turn/start`). The reuse then cannot be resumed at all: a session log + has exactly one live writer (an exclusive `flock` on `session.lock`), so the + first prompt fails with `SessionAlreadyOwnedError`. + + The predicate asks whether the *workspace* accounts for the session, never + whether *this process* can open it — safe while a workspace could only account + for sessions this process had attached, and no longer true once a peer's + sessions are visible. It is reachable without this plugin, since the workspace + account is loaded from the shared store at boot; the plugin makes it immediate + rather than boot-order dependent. + + Workaround: archive the peer's session, because the scan skips archived ids. + Making the session non-blank does not help — the observed one was already + non-blank, which is what identifies the client mirror rather than the session + state as the lever. The fix belongs in `connectWorkspace`: require that the + session is actually openable here, or fall back to `sessions.create` when the + resume is refused. **It is not atomic, and that cuts both ways.** Every registry write republishes the whole store from *this* process's memory, which was loaded once at open. So -- 2.51.2