Repository navigation
Conversation
Rust now owns three counters — Workspace numbers, `ws-<n>` window labels, and `surface-<n>` Surface numbers — in a new pure `ids.rs`. Each keeps a hi/lo ceiling in `<state root>/ids.json`, written atomically (under the counters' lock) before any number at or above it is handed out, so a closed highest-numbered Workspace or window is never re-minted on the next launch. Boot seeds with max semantics (never store) from the file plus the disk scans, which now also collect Surface ids from snapshot and retained arrival-journal panes and doors. A failed state root or write leaves the counters in memory with a log line. Adds `surface_reserve_ids(count, floor)` beside `workspace_reserve_ids`; the webview does not call it yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One seeding rule for every kind: `ids::seed_next(kind, ids)` over `Kind::number`, replacing `workspaces::seed_next`, `routing::seed_next_ws`, and `seed_next_surface`; `ws_index` and `ref_number` share the parser. `Kind::first()` owns the never-mint-`workspace-1` rule instead of a call argument. Reservations serialize on the ids-file lock across the write, so the counters' own lock is only ever held briefly and the main-thread `workspace_report` never waits on an fsync. `saved_windows` reuses `read_snapshot_from`; tests duplicated across ids.rs and lib.rs are dropped, and the spec and rationale lose their mechanism detail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New `lib/src/lib/surface-ids.ts`, mirroring the Workspace id pool: a synchronous `mintSurfaceId(taken?)` drawing from a block the host reserved (64, refilled below 16), `surface-<uuid>` when a reservation fails, and an in-page `surface-1…` counter where no host is installed (playground, Storybook, Pocket, tests). A minted id a live Session or Wall already holds is skipped and logged. Every reservation names a floor, the highest `surface-<n>` the webview restored. The Wall's `generatePaneId`, the helper's `helper-<uuid>`, and Reopen's `pane-<uuid>` all mint through it. Standalone installs the pool beside the Workspace pool via `surface_reserve_ids`; the browser-dev harness gets a matching stub. VS Code gets an extension-host allocator persisting its ceiling in `surface-ids.json` (storageUri, else globalStorageUri), re-read before each raise, behind new `surface:reserveIds` / `surface:reservedIds` messages; the adapter's `init` installs the pool and `main.tsx` now renders after both restore and `init` settle. `dor` refs are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One reserved-id pool, `lib/src/lib/id-pool.ts`, now backs both the Workspace and the Surface id stores instead of two copies of the refill and stale-install guards; the Surface pool binds its floor into the reserve call rather than holding it as state. `mintSurfaceId()` owns the one "already in use" check — the terminal registry plus a Wall-ownership check `wall-handles.ts` installs — so the Wall, helpers, and Reopen all call it bare; `mint-surface-id.ts` is gone and `withFreshSurfaceIds` defaults its mint again. Standalone's `installWorkspaceRegistry` takes the restored Window and computes the floor itself. The VS Code allocator uses the shared `createSerialQueue` and a module-level in-memory allocator that activation replaces. Wiring tests no longer re-assert the skip log the unit test owns. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Surface's dor ref is now derived from its id (`surface-347` is `surface:347`), so refs are unique across the host and survive moves between Workspaces. One grammar in dor/src/protocol.ts (`surfaceRefForId`, `surfaceIdNumber`, `compareSurfaceIds`, `parseSurfaceTarget`) replaces the Wall's classifier; bare numbers, `pane:N`, and an empty `surface:` are refused naming the `surface:N` form. `surface:<n>` targets route Window-wide to the owning Wall, as ids did. Deleted: the per-Workspace ref registry and its counter, the persisted `surfaceRefs`/`surfaceRefsNext` plumbing, ref retire-on-move and mint-in-destination, ref transfer on replacement, and the pending kill's `ref` field. `dor list` sorts by id number. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the router - Internal Wall results (createSplitSurface, createContentSurface, reopenSurface, ensureBrowserSurface, the viewport target) carry only the id; responses derive the ref with surfaceRefForId. - The router refuses a malformed --surface before any Wall is asked, and parses the target once. - WallHandle.iframeSurfaceRefs becomes iframeSurfaceIds; the CLI refusal formats refs where it builds its text. - Drop the unused `pane` list filter; pendingSurfaceRefusal uses getPendingKill; open-target help no longer claims bare ids. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A render swap (agent-browser -> iframe, iframe -> an automated browser, or a provider change) now swaps the renderer in place: the old renderer closes through closeBrowserSurface and the leaf takes the new meta under the same id. The Surface keeps its slot, its TODO, and its dor ref; the new renderer acquires its own controller under that id when it mounts. Replacing an untouched shell still creates a new Surface with a new id. Specs: glossary I10 and its rationale, dor-browser.md render swaps and placement, dor-cli.md ref stability. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
swapBrowserRenderer now takes the old params and the new params, derives the title, and leaves selection to the Display-modal callers, so the launch-failure restore uses it too. Swap tests address the Surface by its known id, share leafIds, abPaneLayout and a raw browser-dispatch helper. Stale comments on the controller registry and AgentBrowserPanel's provider key now describe same-id swaps; dor-browser.md points at I10 instead of restating the mechanism. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PersistedSession.version goes 3 -> 4 and PersistedWindow.version 1 -> 2, so no snapshot written before numbered Surface ids is ever restored. Readers accept only the current versions and treat any other as a quiet fresh start (one info log); the bare-Session-in-Window wrap is gone. Every writer stamps PERSISTED_SESSION_VERSION / PERSISTED_WINDOW_VERSION. Standalone Rust deletes every snapshot of another format, with its temp and geometry, and drops arrival-journal records holding another session format, at boot before the arrival merge and the id seed. ids.json and the recovery record are untouched. standalone/scripts/persisted-format.json pins the pair for both the lib and the Rust host. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
loadWindowState hands the raw blob straight to readPersistedWindow, which already parses strings and starts fresh on anything unreadable. Both readers log a discard through one logDiscard (info for another build's version, warn for anything else) instead of a predicate that also logs. Rust reads the journal record's session through one record_session helper and compares versions directly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Readers now discard every pre-v4 Session and pre-v2 Window, so the shims that kept older blobs readable or older strict builds happy are dead: - the notification-source allowlist the writer kept for strict pre-tolerant readers (a WATCHING detail now persists with its TODO) - the scrollback/resumeCommand deny-list in normalizeSession - the `untouched` default: a pane without it is unreadable - the `nameIsAuto` fallback: a Workspace record without it is dropped - VS Code's redundant panes-array checks after readPersistedSession Specs say how older transcript-bearing blobs leave the disk now: standalone deletes them at boot, VS Code's first save overwrites them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- readPersistedWindow validates a Workspace through isPersistedWorkspaceShape and warns when it drops one - drop the remaining Array.isArray(panes) checks on parsed Sessions - inline isDefaultWorkspaceName into its only caller - refresh comments and rationale the removed shims left stale Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- One numbered-id grammar per language: Rust formats through `ids::Kind::id`, TS parses `surface-<n>` / `workspace-<n>` through one helper in dor/protocol (`workspaceIdNumber`, `surfaceIdFor`). - `ids.json`: a write raises every counter already at its ceiling, so a boot's first Workspace and Surface reservations share one fsync'd write. - Inline the window-persistence wrappers into `windowStateSlot`, their one caller; its tests were already covered by session-window/session-types. - Test fixtures use `surface-<n>` ids whose refs derive from them (dor cli-output, workspace-control), dropping a dead `pane` filter. - Specs: each id rule once — standalone points at transport for the Surface counter's persistence and floor, and at glossary I10 for replacement and render-swap identity. - Drop an unused Rust test import and fix stale comments. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Surface and Workspace refs are the id number, so every launch's gap shows up in refs. Rust persisted a ceiling a slack past each block (1024 Surfaces, 64 Workspaces, 16 windows), and the webview pools held 64 / 32 ids that exit discards. - ids.rs persists exactly the end of each block, written (fsynced, off the main thread) on every reservation; window labels reserve exactly one. - The VS Code allocator matches: re-read, write the block's end, no slack. - Webview pools shrink to Surfaces 8 (refill below 3) and Workspaces 4 (refill below 2), refilling only up to their size. - A burst minted synchronously (Reopen of a Workspace, a closing window's reopen snapshot) reserves its whole count first through surfaceIdMinter / workspaceIdMinter, looping past the hosts' 64-id clamp, so it never falls back to opaque ids. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every window's extension host now reserves from one surface-ids.json under globalStorageUri, under a cross-process lock, flushed before ids are handed out, so windows no longer all number from surface-1 and collide in the Burrow's phone directory. A reservation also stays above every Surface PTY the extension host holds, covering sibling panels. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The view's dormouse.session blob from an older format is no longer read, so it could sit in workspaceState, transcript and all, until the view next saved. Activation now deletes any saved session no reader takes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
minter(count) drained the whole pool before its awaited reservation, so a create landing meanwhile (a dor split while a Workspace reopens) got a permanent opaque id. A burst now reserves its own ids from the host, and both pools refill sooner (Surface below 6 of 8, Workspace below 3 of 4) so a quick run of creates stays ahead of the round trip. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Closing a window reserved a fresh id for every Workspace and Surface for a Reopen that rarely comes, putting a host round trip and a flushed write in front of the close. The record now keeps the window's own ids, marked reopened; the window that boots from it remaps them with the pools it installed, then saves them at once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The skill said either form works, but navigation targets read only the ref form: dor iframe surface-3 opens https://surface-3/. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With no host, the page counted from surface-1 and leaned on a Wall ownership callback, injected by importing wall-handles, to skip ids its restored Surfaces held. A restore or resume now raises the page counter above every Surface it brings back, leaving only the registry guard. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The harness kept its Workspace and Surface counters in memory, so a killed highest Surface's number was minted again after a restart. They now persist, before any id is handed out, under the worktree's node_modules cache, which outlives a run as the harness browser's localStorage does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One maxSurfaceIdNumber fold in dor/protocol for the extension host's PTY floor, the page seed, and maxSurfaceNumber; the router reads PTY ids without copying the buffer map. The allocator creates its directory once and breaks a stale lock after 1s, since a webview boot waits on it. A reopened window's save is issued without holding up render. The harness creates its cache directory once, and its tests share one command helper. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Surface's id is now `surface:<n>` (fallback `surface:<uuid>`) and a Workspace's `workspace:<n>` (fallback `workspace:<uuid>`); the id is the `dor` handle, so the id-to-ref mapping is gone. Rust, the VS Code extension host, the browser harness, and the page mint the colon form; a registry-less page counts Workspaces from `workspace:1` instead of reading strip positions. `parseSurfaceTarget` takes only `surface:<x>`, `surface:self`, `surface:focused`, and `title:`; `3`, `pane:3`, `surface-3`, and bare strings are refused with the `surface:N` hint. `dor list --id-format` is removed, and every JSON payload carries one key per id (`surface_ref`, `workspace_ref`, and the row `ref` are dropped; `workspace_ref` becomes `workspace_id`). Wire types drop `surfaceRef`/`workspaceRef` likewise. The managed agent-browser session name already scrubs the colon (`workspace:3` -> `dormouse.workspace-3.<key>`); a test pins it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The id is the handle: dor-cli.md's Handle Model, transport.md's Surface ids, standalone.md's Workspace registry, vscode.md, glossary, reopen, dor-tool, dor-browser (the session-name scrub), and the skill drop the ref vocabulary, `--id-format`, positional Workspace refs, and the stale "forget the surface ref" tiling rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Workspace target now resolves through one exact-id comparison: `parseWorkspaceRef` (and Rust's `parse_target`) restores the `workspace:` prefix, so numbers and fallback ids share the lookup, the VS Code guard compares ids, and the webview no longer reads Workspace id numbers (`workspaceIdNumber` is gone; only Rust and the harness seed from them). An omitted target with a caller parses as `surface:self`; the stray `ref` key on listed Surfaces, the getter around the answering Workspace id, and a pass-through destination mapping are gone; UUID fallbacks mint through `surfaceIdFor` / `workspaceIdFor`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # lib/src/lib/session-types.ts
Deploying mouseterm with
|
| Latest commit: |
2001b97
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://2c77f66f.mouseterm.pages.dev |
| Branch Preview URL: | https://surface-numbered-ids.mouseterm.pages.dev |
dormouse-bot
left a comment
There was a problem hiding this comment.
VS Code's id minting can lose its numbered ids, and in one case hand the same id to two windows. Both problems come from lock timing in vscode-ext/src/surface-ids.ts, and both show up in the case the lock exists for: several windows booting at once. Details are inline.
| // No initialization needed — the webview is already running | ||
| const restored = readPersistedSession(this.getState()); | ||
| await installSurfaceIdPool(async (count, floor) => { | ||
| const ids = await this.requestResponse('surface:reserveIds', 'surface:reservedIds', { count, floor }, (msg) => msg.ids as string[]); |
There was a problem hiding this comment.
requestResponse gives up after its default 1000 ms. The host side of this request can legitimately take longer. withLock retries for up to LOCK_ATTEMPTS × LOCK_RETRY_MS (~2 s), and reserveSurfaceIds is serialized per extension host, so it also waits behind other webviews' reservations. Each one then does a durable write.
When several windows restore together, the first reservation times out and install resolves with an empty pool, so the Wall's first panes are minted as surface:<uuid>. The host still reserves the block, and that block is skipped. Both outcomes contradict installSurfaceIdPool's "never carries an opaque id". A timeout longer than the host's worst-case lock wait would close this. withLock gives the bound.
| break; | ||
| } | ||
| const age = await stat(lock).then((s) => Date.now() - s.mtimeMs, () => 0); | ||
| if (age > LOCK_STALE_MS) await rm(lock, { recursive: true, force: true }).catch(() => {}); |
There was a problem hiding this comment.
This removes a lock that is older than 1 s without checking whether its holder is alive, and a live holder can stay past 1 s. The holder's critical section ends in a fsynced writeJsonAtomic, which can stall that long on a slow disk, under Windows AV, or during a multi-window restore.
When that happens, window B breaks A's lock and reads the same surface value, because A's rename hasn't landed. Both windows then hand out the same surface:<n>, which is the one outcome this counter exists to prevent. A's finally then rms the path, which is now B's lock, so a third window can get in too.
The ~2 s fall-through that runs body with no lock has the same result. A stale threshold well above any realistic fsync, or a liveness check before breaking the lock (for example, the holder's pid written inside the lock directory), would keep a breaker from overlapping a holder that is still running.
Surface and Workspace ids get one spelling,
surface:<n>andworkspace:<n>. Each comes from a host-wide counter that is saved to disk, never reused (restarts included), and serves directly as thedorhandle. This replaces per-Workspacesurface:Nrefs,pane-<counter>-<random>ids, and the separate hyphenated id / colon ref spellings.This resets saved layouts. Session and window snapshots from older builds are discarded on first launch, with no migration.
What changed
Ids
surface:<n>and a Workspace's id isworkspace:<n>. If the host can't hand out an id, the fallback issurface:<uuid>/workspace:<uuid>.DORMOUSE_SURFACE_IDcarries the same string.<state root>/ids.json.surface-ids.jsoninglobalStorageUrifor every window of the install, behind a lock across processes and fsynced before ids are handed out.Handles
3,pane:3andsurface-3are refused with "use surface:3".dor list --jsonhas one key per id (id,workspace_id,caller_surface_id, …).--id-formatis removed.workspace:1, so positional refs are gone.Browser Surfaces
Persisted format
PersistedSession.versionis now 4 andPersistedWindow.version2. Readers discard any other version.dormouse.sessionat activation.standalone/scripts/persisted-format.jsonpins the versions in both languages.STRICT_READER_NOTIFICATION_SOURCES, and theuntouched/nameIsAutodefaults.Dev harness
What resets
sessions/{main,ws-<n>}.json(plus temp and geometry files) and old arrival records. Windows reopen fresh at their default position.dormouse.sessionand panel state.Verification
pnpm testpass. New tests were mutation-checked: each goes red with its fix reverted.Follow-ups (not in this PR)
surface:<n>target owned by another window. Today it answers "not found", as stable ids already did.main: a killed Surface stays listed and targetable for about 0.5 s during its fade, contrary todor-cli.md.DirectoryWorkspace.refkeeps its name for older Pocket clients. Its value is now the Workspace id.🤖 Generated with Claude Code