Made the recipe state truthful: the Explorer and status bar follow config.yaml edits on disk, new-sandbox keys derive from the title, and names claimed by leaked sbx state are remembered and skipped (FR-009, FR-057) - #26
Merged
Conversation
…nfig.yaml edits on disk, new-sandbox keys derive from the title, and names claimed by leaked sbx state are remembered and skipped (FR-009, FR-057)
There was a problem hiding this comment.
Pull request overview
This PR makes sandbox naming and recipe-derived state “truthful” by ensuring the Explorer/status bar react promptly to on-disk recipe edits, deriving new sandbox keys from the user’s title (while keeping keys frozen thereafter), and persisting a per-working-copy set of sbx names proven to be permanently uncreatable due to leaked upstream runtime state (FR-009, FR-057).
Changes:
- Add a debounced
.sandbox/*.yamlwatcher so UI surfaces refresh immediately after recipe/identity edits on disk. - Seed new sandbox
keyfrom the Title (sanitized + capped), keep it frozen after creation, and avoid generating keys that would map to locally-recorded unusable sbx names. - Introduce a typed
NameClaimedErrorand aworkspaceState-backed unusable-name record, written when a create fails with the leaked-state error and consulted during key derivation.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| src/sbx.ts | Adds NameClaimedError and centralizes “name claimed by leaked state” detection into a typed failure. |
| src/ops.ts | Records unusable sbx names when creates fail with NameClaimedError. |
| src/names.ts | New workspaceState store for unusable sbx names (bounded list), queried by key derivation. |
| src/form.ts | Derives new keys from Title once (fallback to agent), skips unusable-name candidates, and drops abandoned pinned entries on retry. |
| src/extension.ts | Initializes names.ts and adds a debounced filesystem watcher to refresh UI on .sandbox/*.yaml edits. |
| docs/specs/completed/014 - Truthful Recipe State.md | Adds shipped iteration spec documenting the behavior changes and rationale. |
| docs/Features.md | Updates FR-009 behavior and adds FR-057 requirements. |
| docs/Architecture.md | Updates module map/dependencies and documents watcher + key derivation + unusable-name workaround. |
| CLAUDE.md | Updates module map/dependency direction to include names.ts. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Risk & Size
Risk: High
Size: Medium
Risk is High by the repo's own classification because
src/sbx.tschanged — though thechange there is error typing only (a create failure is now raised as a typed
NameClaimedError); no argument vector, flag, or CLI shape moved, sosrc/script.tsneeds no sync.
What & Why
Acceptance of spec 013 produced three failures with one root: the recipe and the names
derived from it go stale or collide, and the UI keeps acting on the old value — most
visibly, creating a sandbox could be blocked outright. Ships spec 014 (FR-009 extended,
FR-057):
key(and therefore its sbx name) at render time and refreshed only on focus change, terminal
events, and lifecycle completion. Renaming a key in
.sandbox/config.yamland clickingConnect went out under the old name, which reads as "renaming does not help".
claude,claude-2, …), adense counter over a tiny namespace, so a key freed by a rename or a removal was handed
straight to the next new sandbox — reissuing a name that was permanently unusable.
sbx createfails because the name isstill claimed by leaked sbx runtime state (upstream sbx rm <name> --force reports 'not found' but name remains claimed in meta.db, blocking subsequent sbx run --name <name> docker/sbx-releases#129, no released
fix — Architecture §14), the extension forgot immediately and could derive it again.
Changes
src/extension.ts: a debouncedFileSystemWatcherover.sandbox/*.yamlrefreshes thestatus bar and fires
sandboxConsole.refresh(dropping the tree's cached refs), so anedited recipe takes effect at once. Read-only discovery is preserved (FR-002) — the
watcher observes, it never writes.
src/form.ts: a new sandbox's key is seeded from the entered title(
Backend API (v2)→backend-api-v2, capped at 40 chars, agent id as fallback) andthen frozen — the form still locks it in edit mode, so renaming a title never moves a
sandbox name, Dockerfile, or image tag. Derivation skips keys already in the recipe and
keys whose sbx name is recorded unusable; a retry that had to abandon its pinned key
drops the dead recipe entry instead of leaving a definition that cannot be built.
src/names.ts(new): the per-working-copyworkspaceStaterecord of unusable sbx names— local by construction, names only, capped at 50, irrelevant after
sbx reset.src/sbx.ts: the leaked-state 500 is raised asNameClaimedError(so no callerre-parses CLI output) and its message now says the name is remembered and skipped.
src/ops.ts: records the name innames.tswhen a create fails with that error.Features.mdFR-009 (UI follows the recipe) + new FR-057;Architecture.md§4 (module), §6 (key derivation + watching), §7/§12 (Title seeds thekey once), §14 (the remembered-names workaround);
CLAUDE.mdmodule map;spec
014 - Truthful Recipe Stateflipped to shipped and moved todocs/specs/completed/.Deliberate consequence: the default Dockerfile name follows the key, so two sandboxes on
one agent no longer default to a shared
claude.Dockerfileand one shared image.Sharing stays available — typing the same file name in both — and is now explicit.
How to Verify
npm run verify— exits 0.Backend API (v2)→ Save..sandbox/config.yamlgets the keybackend-api-v2and thesandbox name reads
<project>-backend-api-v2-<id>. Create a second one with an emptytitle → key falls back to the agent id (
claude).any
<key>.Dockerfileare unchanged; only the Explorer label moves..sandbox/config.yamloutsidethe window (or in another editor) — rename a key or add an entry. The tree and the
status bar update within ~1s without a manual Refresh, and Connect targets the new name.
read the code path): a create failing with
failed to create network: … already existsreports the name as claimed and says itis remembered; the next New Sandbox with the same title derives
<key>-2rather thanthe dead name, and retrying the still-open form does not reuse it either.