Repository navigation
fix(solo): delete a removed workspace's settings after the groups write - #2819
Conversation
kalvinnchau
left a comment
There was a problem hiding this comment.
🤖 Two findings: a reproduced settings-save regression and an incomplete cleanup retry path.
Deleting a Solo workspace removed it from the groups record but left its
settings template (me-section-<sha256(id)>) live on the relay. Its
instructions, folders and agents became unreachable but were never
deleted.
removeSection now queues a best-effort tombstone for that template once
the groups write is confirmed. It uses the kit's existing
save(value, expected, deleted = true), with the lineup emptied so the
tombstone no longer carries the encrypted instructions, folders, agents
or teams. The name becomes the neutral "Deleted workspace" instead of
keeping "<workspace> chats". The TS parser and native kit.rs admission
both accept any non-empty name of up to 120 characters, and every reader
skips a deleted template, so nothing displays it.
The tombstone runs through the store's serialize queue, so removeSection
still resolves once the groups write lands and later mutations wait
behind it. A failed tombstone only logs a warning: the workspace stays
deleted and the record stays live but unreachable, as before. A failed
groups write queues nothing. There is no write when no live template
exists.
A groups write can land and still reject: exact relay confirmation
fails and the outbox delivers later. The retry found the workspace
already absent and returned before queuing the tombstone, so the record
stayed live. removeSection now queues the same cleanup when the
workspace is absent. It writes nothing without a live template.
Workspace settings and New workspace save defaults through the channel
kit directly, outside the Solo queue. The kit allowed one save per
session, so saving another workspace's defaults while a tombstone was
pending failed with "Recipe saving is unavailable or already in
progress". The kit now allows one save per record coordinate. Saves of
other records proceed, and a second save of the same record still fails
fast. Every check a save makes (head read, unresolved-save guard,
created_at) is already per record. Once saves overlap, a save's trailing
refresh could join a catalog read that another save began before this
one was confirmed, leaving the snapshot without it. The trailing refresh
is now refresh({ after: true }), which never joins such a read.
The cleanup signal is the queued op's first statement, ahead of the
snapshot read and the projectTemplateId await. That binds the tombstone
to the generation serialize just admitted, so a clearCache() during the
id hash still aborts it. Bound after the await, it would take the new
generation's unaborted signal and write after the clear. An account
switch is not the case it guards: that ends the session, and the
lifetime signal already aborts the tombstone.
The import of solo/workspace from relay/solo-preferences adds no cycle:
workspace.ts never reaches solo-preferences or session at runtime.
Tests cover the write order, no write without a live template, an
already-deleted template, a failed groups write, a failed tombstone and
the emptied, renamed payload. A held-prepare test checks that
removeSection resolves while the tombstone is held, a later
createSection stays unsettled behind it, and the writes land
groups -> template -> groups. It fails with an unqueued
`void (async () => ...)()`, where createSection settles while the
tombstone is held, and with the tombstone awaited inside the mutation,
where removeSection never resolves. A fence test holds
crypto.subtle.digest, clears the preferences and releases; it fails if
the cleanup signal is bound after the await.
A direct kit save of another workspace's settings succeeds while the
tombstone is held, and the writes land groups -> settings -> tombstone.
It fails with the session-wide guard. A retry test hides the groups
write from confirmation reads, so the first removeSection rejects after
the write landed. The retry then writes the tombstone; without the
absent-workspace cleanup it writes nothing. Two kit tests check that a
held save lets another record save but refuses the same record, and
that a save reads itself back after a catalog read that began before its
confirmation. Each fails with its half of the kit change reverted, and
the first also fails with no guard. The Solo storage docs note the
tombstone and the retry.
Signed-off-by: Matt Toohey <[email protected]>
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
wesbillman
left a comment
There was a problem hiding this comment.
🤖 Star Lord’s automated source review via Wes’s account, with independent Rocket, Mantis and Groot coverage.
Reviewed head 1d650bb50367b19b00f2ad9aa2eef065a6f547e7, target base fb9368ea29076e8d4be8c6f8552835aa46a49182 (merge-base 744aad76bd19d15c2064ad00bd8b7d8e96561618). No new actionable introduced findings.
The earlier concurrent-settings-save and absent-workspace-retry findings appear addressed in source: saves are guarded by record coordinate, the trailing catalog refresh follows older in-flight reads, and a retry still queues the emptied, renamed tombstone when the groups write already landed. The cleanup remains best effort and preserves the existing delete completion/error boundary.
Validation limits: source and regression-test inspection only; we ran no tests or app workflows. The description’s checks are attributed to 86feaaa30, not execution at this reviewed head. The feature implementation/tests match that revision; capability.ts additionally contains the base’s canvasReadOnly helper. Native signing, real-relay behavior, browser repro and human acceptance remain unverified by this review. The description and both attached screenshots were inspected; the older video was not replayed.
|
🤖 Drip design review at Deleting a workspace keeps the existing flow while clearing its saved name, instructions and agent defaults afterward. Source inspection shows retries still clean up an already-removed workspace, and saving another workspace’s defaults can proceed during cleanup (deletion, save guard). This is a contained launch bug fix. Unchecked: I ran no tests or app workflows. The attached light-theme screenshots predate the final fixes; I did not replay the video or verify dark mode, narrow widths, enlarged text, keyboard/focus, real relay/native signing, or stale-device behavior. Cleanup remains best effort; this review does not establish data erasure. |
|
🤖 Design follow-up: no design review needed. This supersedes the design classification in my earlier review. Any engineering findings there remain with code review. |
…uctions * origin/main: refactor(settings): move external identity authorization to security (#3032) test(typeahead): serve the fixture pages as a build so cold loads fit (#2793) feat(channels): list unread top-level mentions in sidebar Activity, and count only unread mentions on the Inbox card (#2722) fix(solo): delete a removed workspace's settings after the groups write (#2819) fix(solo): ask before dismissing chat and workspace settings drops edits (#2616) fix(messages): open the forwarded message from Forward's confirmation (#2802) fix(pairing): tell the tool why its relay was refused, and refuse at once when Buzz can't ask (#2823)
Picks up c202642 (block#2837), which fixes the ThreadPanel catch-up test that fails on every run of a merge with the older main. Co-Authored-By: Claude Opus 5.5 <[email protected]> * origin/main: (155 commits) deps: accept RUSTSEC-2026-0459 for libsqlite3-sys 0.38, move rmcp to 3.9.0 (block#3077) fix(communities): accept invite links and bare hosts in Join a community (block#2412) fix(builderlab): open a remote agent's Activity on the conversation it was opened from (block#3051) refactor(builderlab): keep capabilities fresh in the session (block#3003) feat(moderation): show invite links as a QR code (block#3029) Keep Create agent's Model row readable and give Goose a Thinking row (block#2673) Join a community right after creating it in Settings (block#2991) Let new Solo chats start temporary by default (block#2874) fix(messages): fill a screen that a head of folded agent activity leaves empty (block#3037) fix(search): search saved conversations while the community is offline or connecting (block#2724) test(agents): make version-history date assertion time-zone independent (block#3010) feat(relay): gate artifact writers on NIP-AR support (block#2735) fix(channels): open the dock's community switcher from Personal space (block#2806) fix(members): keep the list's place after a member change (block#2818) fix(channels): keep a launcher's side panel open on Close all tabs (block#2803) fix(activity): show a remote agent's Buzz send as its reply (block#3025) refactor(settings): move external identity authorization to security (block#3032) test(typeahead): serve the fixture pages as a build so cold loads fit (block#2793) feat(channels): list unread top-level mentions in sidebar Activity, and count only unread mentions on the Inbox card (block#2722) fix(solo): delete a removed workspace's settings after the groups write (block#2819) ... Signed-off-by: Andrew Harvard <[email protected]>
…pover-card-layout-7eeda1e0 * origin/main: (128 commits) deps: accept RUSTSEC-2026-0459 for libsqlite3-sys 0.38, move rmcp to 3.9.0 (#3077) fix(communities): accept invite links and bare hosts in Join a community (#2412) fix(builderlab): open a remote agent's Activity on the conversation it was opened from (#3051) refactor(builderlab): keep capabilities fresh in the session (#3003) feat(moderation): show invite links as a QR code (#3029) Keep Create agent's Model row readable and give Goose a Thinking row (#2673) Join a community right after creating it in Settings (#2991) Let new Solo chats start temporary by default (#2874) fix(messages): fill a screen that a head of folded agent activity leaves empty (#3037) fix(search): search saved conversations while the community is offline or connecting (#2724) test(agents): make version-history date assertion time-zone independent (#3010) feat(relay): gate artifact writers on NIP-AR support (#2735) fix(channels): open the dock's community switcher from Personal space (#2806) fix(members): keep the list's place after a member change (#2818) fix(channels): keep a launcher's side panel open on Close all tabs (#2803) fix(activity): show a remote agent's Buzz send as its reply (#3025) refactor(settings): move external identity authorization to security (#3032) test(typeahead): serve the fixture pages as a build so cold loads fit (#2793) feat(channels): list unread top-level mentions in sidebar Activity, and count only unread mentions on the Inbox card (#2722) fix(solo): delete a removed workspace's settings after the groups write (#2819) ...
Solo's "Delete workspace" deleted on the first click. Since block#2819 it also clears the workspace's instructions, folders and agents on every device, so a misclick lost that setup for good. Every neighboring destructive action (Remove section, Delete team, Delete agent) already asks first. The menu item now opens the shared AlertDialog, "Delete {name}?", with Cancel focused. Confirming runs the existing removeSection; a failure stays in the dialog for retry, and focus returns to the heading or to Chats. Follows the pattern from block#2820. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Jonathan Ho <[email protected]>
Deleting a Solo workspace (Actions → Delete workspace) removed it from the groups record but left its settings record live on the relay. That record holds the workspace's instructions, folders and agents. Once the workspace was gone, nothing could reach the record, but nothing deleted it either. This PR tombstones the record after the groups write.
What you see in the app: nothing changes. The sidebar, the delete flow and its timing are the same as on
main. Only the relay record changes:me-section-<sha256(workspace id)>becomes a tombstone (deleted: true). It has no instructions, folders, agents or teams, and its name is the neutral "Deleted workspace".Before / after
Left:
mainat067db608f. Right: this change, recorded atd61d1dbb7, a pre-squash commit made before the review fixes. The recorded flow is a delete whose groups write confirms, with no other save in flight, and it takes the same path on this head. The review fixes only change a retried delete and overlapping kit saves (see below). The recording uses Chromium at 1280 px and fixture data only.No app surface shows a deleted workspace's settings record. So the dashed magenta TEST HARNESS · NOT PART OF BUZZ panel in the bottom right reads the record from the fixture relay and decrypts it. That panel is a throwaway hook in the recording worktree and is not part of this PR.
What to notice: the app looks the same on both sides, so watch the harness panel.
mainstill reads Launch's record as LIVE. It is still named "Launch chats" and still has the instructions and~/Development/launch-site.""and folders[].side-by-side.mp4
Before, step 5 (
main): the sidebar no longer lists Launch. The harness panel shows its record still LIVE, still named "Launch chats", with the instructions and folder.After, step 5 (this change): the sidebar is the same. The same record is now DELETED (tombstone). Its name no longer mentions Launch, and its instructions and folders are empty.
What changes and why
The fix is in
removeSectioninsrc/features/relay/solo-preferences.ts(+38/−14). The channel kit's save guard insrc/features/channel-templates/capability.ts(+8/−5) changes too, so the tombstone doesn't block other settings saves.removeSectionqueues a second write through the store's existingserializequeue. That write looks up the workspace's template withtemplateEntry(…, await projectTemplateId(id)). If a live template exists, it saves it back with the kit's existingsave(value, expected, deleted = true).emptyLineup()(instructions, folders, agents and teams), so it doesn't republish the encrypted instructions."<workspace> chats"with the constant "Deleted workspace", so the user's workspace name isn't kept either. The TS parser and nativekit.rsadmission both accept any non-empty name of up to 120 characters. Every reader skips a deleted template, so nothing displays it.removeSectionstill resolves once the groups write lands, so the sidebar and the focus move don't wait for the second write. Mutations queued after that point wait behind the tombstone.console.warn. It never rejectsremoveSectionor undoes the groups write, and the record stays live but unreachable, as onmain. A failed groups write queues no tombstone. There's no write when no live template exists.removeSectionused to return early there, so the record stayed live. It now queues the same cleanup whenever the workspace is absent. The cleanup writes nothing without a live template.AbortSignal.any([lifetime, generation.signal]), ahead of the id-hashawait. That ties the tombstone to the generationserializeadmitted, so a cache clear (clearCache()→soloPreferences.clear()) during the hash still aborts it. Bound after theawait, the tombstone would take the new generation's signal and write after the clear. An account switch isn't the case this guards: it ends the session, andlifetimealready aborts the tombstone.relay/solo-preferences.tsnow importssolo/workspace.ts, butworkspace.tsnever reachessolo-preferencesorsessionat runtime.docs/solo/README.mdnow notes the best-effort tombstone, what a failed tombstone leaves behind and the retried delete.docs/buzz-help.mddoesn't describe what a delete keeps, so it's unchanged.Overlap with #2788
#2788 (draft) changes the same
removeSectionhunk. It makes the same split,const groups = …; const saved = await save(groups, entry, active); … return saved;. Then it queues its own best-effort cleanup of the workspace's Lead withvoid serialize(…). This PR uses the same shape on purpose. Whichever PR lands second gets only a small conflict:emptyLineupto the../channel-templates/modelimport, and importsprojectTemplateIdandtemplateEntryfrom../solo/workspace.isTransientFailure.removeSection: keep this PR's removal of the early return and its conditionalconst saved = value.groups.some(…) ? await save(groups, entry, active) : project(value);. Keep bothvoid serialize(…)blocks betweenconst saved = …andreturn saved;, each with its own.catchand warning. Their order doesn't matter: they write different records, and each binds its own cleanup signal first. Without the early return, NEXUS-4156: Add workspace Leads and choose a chat's workspace #2788's Lead cleanup also runs for a workspace that is already absent. Check that it writes nothing when there's no Lead.solo-preferences.test.ts.The channel kit's save guard is per record
The kit used to have one
savingflag for the whole session (src/features/channel-templates/capability.ts). Workspace settings (WorkspaceSettings.tsx) and New workspace (NewWorkspace.tsx) save defaults through the kit directly, outside the Solo queue. So saving another workspace's defaults while the tombstone was pending failed with "Recipe saving is unavailable or already in progress". kalvinnchau reproduced this in Chromium and WebKit.dtag). Saves of other records proceed. A second save of the same record still fails fast with the same message. Every check a save makes is already per record: the head read, the unresolved-save guard and thecreated_atcheck.kit.savenow ends withrefresh({ after: true }). That only adds a read when another catalog read is already in flight.removeSectionresolves.void serializestays, for three reasons:kit.saveinside the delete would double the delete's latency, and a slow signer or confirm would hold the delete open.await serialize(…)from inside the op would deadlock on its own queue.void serializematches NEXUS-4156: Add workspace Leads and choose a chat's workspace #2788's Leads cleanup.Follow-up (pre-existing, not in this PR)
Take a second device that hasn't received the groups update. Its sidebar still lists the deleted workspace. If someone opens that workspace's Workspace settings there and saves, a live template is published over the tombstone. The problem predates this change: on
main, that save overwrites a record that was never deleted. A fence would belong in the project-defaults save path, which is outside this change.Verification
Checked on this head (
86feaaa30):src/features/relay/solo-preferences.test.tsand all 35 insrc/features/channel-templates/capability.test.tspass.removeSectionstill resolves;removeSectionresolves while the tombstone is held, and a latercreateSectionstays unsettled behind it. The writes land groups → template → groups. The test fails in two wrong versions:void (async () => …)():createSectionsettles while the tombstone is held;removeSectionnever resolves.crypto.subtle.digest, clears the preferences, then releases. It fails if the cleanup signal is bound after the hashawait.session.channelKit.save, as Workspace settings does. That save succeeds and the snapshot shows the edit. On release, the writes land groups → settings → tombstone. With the session-wide flag restored, the save fails with "Recipe saving is unavailable or already in progress".removeSectionrejects with "awaiting exact relay confirmation" after the write landed, and only the groups record is written. Then the retry writes the tombstone. Without the fix, the retry writes nothing.refresh().refresh();deleted: false;.catchremoved.tsc --noEmitpasses. Biome passes on the four changed source files, andgit diff --checkis clean. The pre-commit hooks also passed: literal-names, icons, biome, sadscan and git-lfs.Checked on earlier heads only:
pnpm lintandpnpm design:checkpassed on the pre-squash branch, before the review fixes.d61d1dbb7, it passed in Chromium (25.2 s) and WebKit (27.0 s).main, it fails at the tombstone assertion, because no tombstone is written;void serializewas kept, and the second-device fence is left as a follow-up. No new agent review ran on the review fixes.Not checked:
WorkspaceSettings,NewWorkspaceand session tests weren't run locally for the kit change. CI covers them.🤖 Generated with Claude Code