silverwood docstore: serialize concurrent writers with a per-workstream lock master
silverwood assumed a single writer per forest — papyrus, which serializes its own writes in-process. Codex broke that: its SessionStart hook runs `__register-codex-session` as a *separate process* that writes the same workstream document while papyrus is also writing it (the reconcile loop's `tabOrder` KV, rename, lock). With a snapshot-overwrite store and no cross-process locking, two writers to one doc produced: - a crash — `save()` used a fixed `<id>.tmp`, so one process's `rename` could find the temp already moved by the other and fail with ENOENT (surfaced, misleadingly, at the `.loro` path); and - lost updates — each writer overwrites the whole doc, so the loser's change (e.g. the just-registered Codex session) is clobbered. Fix, entirely in silverwood-core since every writer is a `silverwood` process: - `DocStore::write_lock(id)` returns an RAII guard; `FilesDocStore` implements it as an advisory `flock` on a sibling `<id>.lock` file (fd-based, so it auto-releases on drop and on process exit — no stale locks). Default no-op for lock-less backends. - The forest holds that guard across the *entire* load-modify-save of every mutating op (sessions, kv, status, rename, checkout-state transitions, per-doc upgrade), so cross-process writers take turns. `create_workstream` needs none (fresh id); nested helpers (`provision_checkout`) never re-lock, so there's no flock self-deadlock. Serializing also keeps the shared forest peer id safe (writes stay causally ordered). - Reads never write: `load_doc_ephemeral` performs the lazy schema migration in memory without persisting it, so `spawn`/`doctor` take no lock. The persisting `load_doc` now runs only inside a locked mutation. - Harden `save()` with a per-(process, save) unique temp name so a stray concurrent save can't collide on the temp even outside the lock. Tests: docstore lock exclusivity + a concurrent-save stress test. This is what makes papyrus's `fresh codex launches …` e2e deterministic (was ~50% flaky). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0179izRQK57x2JpKTn6huvgL