#1: implement worktree and clone workspace strategies #2
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "issue-1-workspace-strategies"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Implements the
worktreeandcloneworkspace strategies specified in docs/workspaces.md §4 (onlysharedwas implemented; the others were refused at set time).worktree/cloneare now valid strategies; both require a configuredbase(refused at set time — the workspace is created under<base>/.bob/).ResolveWorkspacederives the per-session path (<base>/.bob/wt/s<id>/<base>/.bob/clone/s<id>) and takes the session id, since the path is named by it.git worktree add(one branchbob/s<id>per session) /git clone --no-local(independent object store) at bind, guarded removal at session end (INV-9: a dirty workspace is refused unless forced, naming the dirty files). The base's git-ness is verified at bind, where a failure refuses the bind loudly (§5) — the server cannot see the filesystem, so the set-time check is limited to base presence.workspace.create/workspace.destroy/workspace.statusserver→client requests (§11). Bind dispatches creation (creation happens at bind, not lazily); session delete dispatches the guarded destroy, andsession rmgains--forceto discard a dirty workspace. session.create/project/delete run on a goroutine so a host: caller bind/delete can read the workspace-op response from its own connection (no read-loop deadlock).Refs #1.
Review of PR #2 — worktree and clone workspace strategies (issue #1).
Overall. The core is solid: strategies are validated at set time (worktree/clone require a base), per-session resolution (
<base>/.bob/{wt,clone}/s<id>) is correct and collision-free, creation happens at bind on the pinned host and fails the bind loudly, the INV-9 dirty guard refuses with the dirty files named and--forcediscards them, the host:caller bind/delete path avoids the read-loop deadlock by running binds/deletes on a goroutine, and the subagent spawn creates a real workspace for children. Unit + end-to-end tests cover create/reuse/refuse/destroy for all strategies, including the caller path.make checkis green and the rpc/toolhost suites pass under-race. However, the workspace destruction side of the lifecycle has real gaps.Blocking findings
Subagent worktree/clone workspaces are created but never destroyed.
subagentSpawnnow creates a real worktree/clone for the child (server-wiredCreateWorkspacehook), but neithersubagentCompletenorsubagent_kill/KillSubagent/terminateSessionTreeever dispatchesworkspace.destroy— the only dispatch site issession.delete, and nothing callssession.deletefor a subagent (completion merely moves the activity toexitedand keeps the session row). This contradicts SUB-8 ("the claim is released, the workspace destroyed, the transcript kept") and workspaces.md §6. Every finished or killed subagent on a worktree/clone project permanently leaks a worktree plus abob/s<id>branch in the base repo. The PR added the creation half of the subagent workspace lifecycle; the destruction half is missing.Destroy routing for host:"caller" dispatches to the connection that sends
session.delete, not the connection that created the workspace.workspaceHostConnreturns the current caller wheneverhost == "caller", so a caller-hosted worktree/clone workspace deleted from a different client is destroyed on that other client's filesystem: a TUI/REPL on another machine would rungit worktree remove/RemoveAllat the same absolute path on its machine (INV-2: a workspace's host is part of its identity and is not substitutable). A plain CLI (session rm/chatpath) registers no workspace handlers and answers-32601, so the delete silently degrades to "warn + orphan": the INV-9 refusal never surfaces and the workspace is left behind without the operator being told. The destroy should be routed to the connection that owns the workspace, or refused for a caller-owned workspace when the delete comes from a non-owner connection.workspace.statusis dead on the wire. It is implemented in the Executor and registered on every client surface, but no server code ever sends it, and the §5/§11 orphan-visibility surface (toolhost.listgainsworkspaces[]) is absent. §5's "orphans are visible — a host reports the workspaces it holds" is therefore unimplemented, and it is the only backstop for the leaks findings 1 and 2 create. Either wire it (status fromtoolhost.listor an explicit command) or drop the method.Non-blocking
workspaceKindinfers the strategy from the path shape: a clone workspace under a base whose own path contains/.bob/wt/is misclassified asworktree, so its destroy fails and it becomes a permanent orphan (no data loss — the destroy refuses).issue-to-pron a non-git base is refused at configuration time" remains unimplemented (pre-existing).git worktree removeleaves thebob/s<id>branch in the base forever (intentional, for re-attach on re-bind; note it accumulates one branch per session).git worktree addpermanently blocks that session's re-bind until an operator removes it — conservative refusal is fine, but the empty-dir case could be cleaned up instead.VERDICT: REJECT
Review round 2 — all three blocking findings addressed on this branch (commit
8fe6dab), plus the cheap non-blocking items.Subagent workspaces are now destroyed. subagentComplete, KillSubagent and terminateSessionTree all dispatch workspace.destroy to the host that served the child, through a new DestroyWorkspace hook on the agent service (wired by the server, spawn-time host pins verified against the registration like routing — INV-4). The destroy is INV-9-guarded without force: an automatic path has no operator to confirm a discard, so a dirty workspace is left as an orphan rather than deleted — and the finished/killed notice now tells the owner ("its workspace was left as an orphan — see 'tool-host list'"). A wedged host cannot hang the completion driver: the destroy is bounded to 30s. Finished/killed subagents on worktree/clone projects no longer leak a worktree + bob/s branch.
Destroy routing for host:"caller" uses the owning connection. The server records which connection bound each caller-hosted workspace (dropped when that connection closes) and routes session.delete's destroy there, wherever the delete comes from — never to the sender's machine (INV-2), and no more silent -32601 degradation for clients without workspace handlers. When the owner is gone, no connection may destroy the workspace; the deletion proceeds (the session row is the claim) and the response carries an orphan warning that 'session rm' prints.
workspace.status dead code removed, orphan visibility wired. The never-sent workspace.status wire method is replaced by workspace.list (server → host: enumerate the workspaces a host holds under the worktree/clone bases), and toolhost.list gains the per-host workspaces[] — the §5/§11 surface. 'tool-host list' renders them and marks workspaces no live session claims as [orphan], so leaks from (1)/(2) are visible for manual collection (§13). The Executor's WorkspaceStatus method stays as the per-workspace implementation behind the enumeration.
Non-blocking, cheap and safe:
make check is green; rpc/toolhost/agent pass under -race. New tests cover: completion/kill/parent-delete destroying the subagent worktree, delete-from-other-client routing to the owner, owner-gone orphan warning, toolhost.list workspaces[] (claimed + orphan), ListWorkspaces, the workspaceKind fix, and the DestroySubagentWorkspace hook (host pins, INV-9 orphan, shared no-op).
Review round 2 — PR #2 (worktree/clone workspace strategies, issue #1).
Reviewed at
8fe6dab(both commits). Re-verified the round-1 blockers and the new tests;make checkgreen, full suite green under-race(go test -race -count=1 ./...), tree clean.Round-1 blockers — genuinely resolved, not papered over
Subagent workspaces are destroyed.
subagentComplete,KillSubagentandterminateSessionTreeRecall dispatchDestroySubagentWorkspace, routed to the child's own host through the newDestroyWorkspacehook with the spawn-time hostname/user pins verified against the registration (INV-4), bounded to 30s so a wedged host cannot hang completion, and INV-9-guarded without force — a dirty tree is left as an orphan and the finished/killed notice says so. Descendants of a deleted parent are destroyed too (terminateSessionTreeRecwith destroy=true); the root's own workspace keeps the strict INV-9 refusal insession.delete. Verified byTestSubagentCompletionDestroysWorktree,TestSubagentKillDestroysWorktree,TestSubagentWorkspaceDestroyedOnParentDelete,TestDestroySubagentWorkspace— all pass.Caller-host destroy routing uses the owning connection.
callerWSrecords the binding connection per caller-hosted worktree/clone workspace at bind and is dropped when that connection closes;workspaceDestroyConnroutessession.delete's destroy to the owner wherever the delete comes from — a plain CLI with no workspace handlers no longer answers -32601, and the same path string is never removed on another machine (INV-2). Owner gone → the deletion proceeds (the session row is the claim) and the response carries an orphan warning thatsession rmprints. Verified byTestWorktreeStrategyHostCallerDeleteFromOtherClientandTestWorktreeStrategyHostCallerOwnerGone.Dead wire method removed, orphan visibility wired.
MethWorkspaceStatus/WorkspaceStatusParams/Resultare gone from the wire (no references remain);workspace.listis implemented on all three client surfaces (TUI, REPL, headless host) andtoolhost.listgains per-hostworkspaces[], rendered bytool-host listwith an[orphan]marker for paths no session row claims. Verified byTestToolHostListReportsWorkspacesandTestListWorkspaces. TheworkspaceKindlast-marker fix (clone under a base whose own path contains/.bob/wt/) is correct and tested.New observations (non-blocking)
failed(failSubagent: runner error, unresolvable host),timed_outandlost(terminateActivity: deadline expiry, supervisor, reconcile) never attempt the destroy, andtool-host list's[orphan]marker keys on session-row existence, not liveness — so a timed-out child's worktree shows as claimed, invisible as dead weight (the parent's "timed out" notice doesn't mention it either). The workspace is not lost (deleting the child session destroys it with the INV-9 guard; a dirty tree would be refused by an automatic destroy anyway), and SUB-8 is about completion specifically, so this is defensible — but it is exactly the crash/abandon shape §5/§13 say should stay visible for manual collection. Consider destroying best-effort on these transitions too, or marking workspaces of dead sessions[orphan](matching the commands.md wording "no live session claims").session.deletenever callsclearCallerWorkspace— a stalecallerWSentry (conn + ctx) accumulates per deleted caller-hosted worktree/clone session while the owner stays connected; self-cleaning on disconnect, but easy to clear in the delete path.session.deleteafterterminateSessionTreehas already killed the children and destroyed their workspaces; the retry with--forcecompletes the deletion. Pre-existing ordering, now more visible.Issue #1's requirements (strategies validated at set time, per-session resolution, creation at bind on the pinned host, INV-9-guarded destruction, caller routing, subagent lifecycle, orphan visibility) are met; the earlier blockers are fixed with real tests and no new routing/race/deadlock issues found.
VERDICT: ACCEPT
View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.Merge
Merge the changes and update on Forgejo.Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.