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