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: |
c3ad114
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://edf823b8.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.
A Surface's id is now its dor handle, so the terminal context, the agent-browser hint and modal, and the Workspace tab menu show the raw id. Story fixtures named their Surfaces with free-form strings, which those snapshots now printed where main showed surface:<n>. Give every fixture Surface (and the Workspace strip's Workspaces) the production shape, numbered as main's per-Workspace refs were: a restored Wall's panes from surface:1 in tree order, so Helper placement's bottom row again shows surface:2 and its wide bottom row surface:3. Stories that mint beside fixture panes seed the page's id counter past them, as a restore does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The VS Code allocator broke any lock older than 1 s, though a live holder's flushed write can take longer, and ran unlocked after 2 s: two windows could read one counter and hand out the same surface:<n>. The lock now records its holder (pid + token) atomically, is broken only when that pid is gone or this window has watched it stand 30 s, and is released only by its holder. A reservation that cannot take the lock within SURFACE_ID_RESERVE_WAIT_MS of arrival, or cannot persist its block, hands out nothing, so the webview mints surface:<uuid>. The webview waits SURFACE_ID_RESERVE_TIMEOUT_MS (the wait plus one durable write) for every reservation, not the default 1 s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Wall booted with numbered Surfaces (initialPaneIds or a restored layout) could mint below them on a page with no host pool, where only reconnect seeded the counter. The Wall's seed now raises the page counter past its panes and Doors, so the stories drop their per-story seeds; the preview resets the counter after each story so minted numbers do not depend on story order. Story fixtures build ids with surfaceIdFor, and Helper placement derives its ids from the roles' tree order in one place. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dormouse-bot
left a comment
There was a problem hiding this comment.
The new commit closes both earlier threads. One leftover: installSurfaceIdPool's doc comment still promises that a Surface created after boot never carries an opaque id. With this commit the host deliberately answers a reservation it can't lock in time with no ids, and the new surface-ids.test.ts case pins the resulting surface:<uuid>, so the comment now contradicts the contract docs/specs/vscode.md states. Suggestion inline.
|
|
||
| /** Mint from `reserve`, which hands out ids numbered above `floor` — the highest | ||
| * `surface:<n>` this page restored. Resolves once the first block is in hand, | ||
| * so a Surface created after boot never carries an opaque id. */ |
There was a problem hiding this comment.
| * so a Surface created after boot never carries an opaque id. */ | |
| * so a Surface created after boot carries an opaque id only when the host | |
| * hands out none (VS Code, when it cannot lock its counter in time). */ |
The VS Code allocator persists to globalStorageUri alone: before activation a reservation answers no ids. Reservations queued behind one in flight share one lock, one read, and one durable write, each still bounded by its own deadline. The Windows rename retry is one helper both the atomic writer and the lock release use, and the block cap is one constant beside the reserve wait. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dor/protocol drops ParsedWorkspaceRef's unused number and exports workspaceIdNumber. One prefix-parameterized case file, dor/test/id-cases.json, replaces the Workspace-only one and runs against dor/protocol and Rust's ids::Kind::number. They disagreed past 2^53: the webview read such a number inexactly where Rust read it exactly up to u64::MAX; both now read none past Number.MAX_SAFE_INTEGER. The dev harness imports the grammar and the block cap instead of hand-rolling them, behind one reserve helper. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the counter Counters::reserve clamps a block itself and leaves the file to Counters::file, so WindowState::reserve_blocks persists several blocks with one write and each reserve command is one line. seed_next returns one past the highest number, leaving the first-number floor to reserve alone. A booting window takes its first Workspace and Surface blocks from one boot_reserve_ids and fetches the registry alongside, instead of two durable writes and then the registry in turn. discard_other_formats reads only a snapshot's version. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n one call createIdPool takes its prefix and owns the page counter, the opaque fallback, and the in-use skip the Surface and Workspace wrappers each kept. A burst the pool already holds is served from it synchronously; a larger one reserves only the difference. Low-water marks drop to 2 of 8 Surfaces and the last of 4 Workspaces, for fewer durable writes at the same burn bound. withFreshSurfaceIds is async over a list of sessions: it counts their distinct Surfaces, reserves that many, and remaps, so Reopen and a reopened window stop pairing a count with a minter. sessionSurfaceIds is the one panes-then-Doors listing. Also inlines a no-op wrapper in resolveWorkspaceRef, builds story ids with surfaceIdFor/workspaceIdFor, and corrects a stale routing comment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The id is the dor handle, unique across windows and stable across moves; the phone directory's dedupe is a second reason, not the only one. Re-baselines dor-browser.md's word budget for this branch's id-scrubbing rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…another version's VS Code's allocator read any failure but ENOENT, or another version's file, as a counter at 0: it handed out its own in-memory numbers and wrote a lower mark over the stored one. Rust's reserve_blocks handed a block out even when the ids.json write failed, and seed_ids only logged an unreadable or other-version file, which the next reservation overwrote from the disk scan. Both hosts now hand out no Surface (or, in Rust, Workspace) numbers while the file cannot be read or another version wrote it, and never write over another version's; Rust re-reads an unreadable file on the next reservation. A file this version cannot parse is overwritten from what the host knows: writes are atomic, so only outside damage leaves one. A failed Rust write hands out no Workspace or Surface block; window labels, which have no opaque fallback, still come from memory. transport.md states the rule both hosts implement; standalone.md drops its contradicting in-memory fallback. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dropping the low-water marks to 2 and 1 left a quick run of creates outpacing the refill round trip, and each create past the pool keeps an opaque id for life. Back to refilling below 6 of 8 Surfaces and 3 of 4 Workspaces; tests now pin a pool's worth of creates after one round trip. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The pool's in-use check read only the terminal registry, so a browser or Tool Surface's id was invisible to it. It now also asks the Workspace membership every mounted Wall publishes (isWorkspaceSurface), which lists every pane and Door; it lives in lib/src/lib, so the VS Code extension's typecheck, which reaches surface-ids.ts, pulls in no component. The restore seeds stay: a restore mints before its Wall publishes, and every skip logs a fault. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Wall built a fake session to hand sessionSurfaceIds its panes and Doors, and reconnect took the max of two maxima; each now reads one list. With Wall's caller gone, sessionSurfaceIds takes a PersistedSession again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The harness wrote ids.json with its own temp-and-rename, and a failed write threw out of the invoke. It now queues durable writeJsonAtomic writes, so the file never moves back, and a reservation it cannot persist hands out no ids, as both hosts do; a Workspace report's seed is persisted before it answers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.<uuid>fallback. Another version's file is never overwritten. A file that can't be parsed at all is overwritten from what the host knows. Standalone window labels have no fallback, so they still come from memory.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