🤖 feat: restore task_workspace_lifecycle with archive/unarchive for peer workspaces - #3940
Conversation
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 035ad4d751
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Security Review
Here are some automated security review suggestions for this pull request.
Reviewed commit: 035ad4d751
Only the user who started this review can view the report in Codex.
ℹ️ About Codex security reviews in GitHub
This is an experimental Codex feature. Security reviews are triggered when:
- You comment "@codex security review"
- A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review
Once complete, Codex will leave suggestions, or a comment if no findings are found.
|
Addressed all four review findings in 743b93b:
|
|
@codex review |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 743b93b203
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Addressed all five round-2 findings in ef03f32:
|
|
@codex review |
The tool was removed from the executable toolset by #3825 as collateral of the sub-agent lifecycle consolidation, leaving orchestrating agents unable to archive peer workspaces created via task(kind="workspace"). Restore it with only the reversible verbs: - Live input schema (TaskWorkspaceLifecycleToolInputSchema) exposes only archive/unarchive; the historical args schema stays intact so old transcripts still parse. delete_worktree/remove stay non-invocable; task_remove remains the sole irreversible verb. - taskService: restore archiveOwnedWorkspaceTurnWorkspace + helpers from 88580ca^ and add the previously unimplemented unarchiveOwnedWorkspaceTurnWorkspace. Authorization uses durable workspace-turn ownership records (taskHandleStore.isWorkspaceOwnedBy). Unarchive never interrupts active turns, even as defense-in-depth. - Register tool + availability + PTC bridging; remove from explore/plan/ desktop agent allowlists; preserve output in shared transcripts. - Frontend untouched: renderer/result schema survived the removal.
…-policy guard - Serialize workspace-turn handle persistence with owned-workspace archive via the shared workspaceLifecycleLocks (archived re-check at persist time), so a follow-up can no longer slip between archive's active-turn check and its stream stop. Lock order mutex -> lifecycle lock is acyclic: the archive path never acquires the task mutex. - Archive/unarchive active-turn checks now also cover turns OWNED BY the target workspace (nested delegation); interrupt_active settles those too. - When interrupt_active is set without acknowledged paths, preflightArchive runs BEFORE any interruption so a lossy-snapshot confirmation cannot leave work terminated but the workspace unarchived. - Model-facing archive fails closed under the 'Delete checkout' worktree archive behavior (would delete the checkout without user confirmation).
…flight, sink-enforced delete guard, settled-turn tolerance - createWorkspaceTurn persists handles under sorted lifecycle locks for BOTH owner and target, so archiving a peer serializes against that peer starting nested turns; a nested turn racing its owner's archive is refused. - Archive refuses when the target has live non-turn activity (user stream, terminal PTYs, desktop session) — interrupt_active covers delegated turns only. New workspaceService.listLiveWorkspaceActivity + terminalService.hasWorkspaceSessions + DesktopSessionManager.has. - preflightArchive now runs before interruption on every interrupt_active archive (not only unacknowledged ones), surfacing blockers like active descendant sub-agents and re-confirming when acknowledged paths no longer cover the fresh untracked set. - Delete-checkout policy enforced at the sink: workspaceService.archive gains forbidWorktreeCheckoutDeletion, checked against the same behavior read that drives snapshot/deletion; that read is now passed to the afterArchive worktree hook so a keep->delete settings flip mid-archive can no longer delete a checkout that was never snapshotted. - Lifecycle interruption skips turns that settled after collection instead of aborting the archive mid-set.
ef03f32 to
81ef051
Compare
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 81ef051d21
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
This comment has been minimized.
This comment has been minimized.
… clean up created workspace on owner_archived refusal
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: daa3e8d0dd
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…sensitive interrupt_active, guard archived-workspace activity admission, suppress disposable cleanup on archive, reject blank acknowledged paths
|
@codex review |
There was a problem hiding this comment.
💡 Codex Security Review
Here are some automated security review suggestions for this pull request.
Reviewed commit: 96ed7d8d2c
Only the user who started this review can view the report in Codex.
ℹ️ About Codex security reviews in GitHub
This is an experimental Codex feature. Security reviews are triggered when:
- You comment "@codex security review"
- A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review
Once complete, Codex will leave suggestions, or a comment if no findings are found.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 96ed7d8d2c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…p/queue admission with archive, pin archive policy through interruption, gate on active workflow runs, defer nested disposable cleanup, serialize unarchive under tree lock
|
@codex review |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6d5c00ba3b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 38ba99cfcd
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…bstones, pre-interruption archive hold - Preserve output dirs on post-dispatch exec throws for all non-host-record runtimes (SSH/Coder, Docker, devcontainer) as fail-closed orphan evidence - Retain background process name reservations after failed non-host spawns and probe record directories through the runtime before reusing a name, so a same-session or post-restart retry can never truncate a survivor's record - Treat DevcontainerRuntime as non-host for every spawn-record probe and scan its bind-mounted record root at archive time with container-namespace PID semantics (running records without an exit marker fail closed) - Tombstone editor-open rollbacks that race ahead of their in-flight recording so an abandoned launch can never commit a sticky durable marker - Arm and validate the archive sink's live-user-activity admission gate BEFORE interrupt_active destroys delegated turns, carrying the hold through the sink
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f9f42bfc61
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Security Review
Here are some automated security review suggestions for this pull request.
Reviewed commit: f9f42bfc61
Only the user who started this review can view the report in Codex.
ℹ️ About Codex security reviews in GitHub
This is an experimental Codex feature. Security reviews are triggered when:
- You comment "@codex security review"
- A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review
Once complete, Codex will leave suggestions, or a comment if no findings are found.
…, editor token fallback - Pre-interruption hold now rechecks streaming (allowed only for a RUNNING delegated turn on the target), in-flight native-terminal/editor opens via their synchronous pending counters, and in-flight forks - Narrow the hold's dispatch check to PREPARING/auto-retry state so a queued delegated turn is attributable by the entry-count comparison instead of always refusing interrupt_active - Pair WorkspaceService.fork() with model archive admission: refuse while the source is archiving and hold a source preflight count the sink's gate checks - Generate editor launch tokens with a guarded randomUUID fallback so built-in editor opens work on non-secure origins
|
@codex review |
There was a problem hiding this comment.
💡 Codex Security Review
Here are some automated security review suggestions for this pull request.
Reviewed commit: df05891479
Only the user who started this review can view the report in Codex.
ℹ️ About Codex security reviews in GitHub
This is an experimental Codex feature. Security reviews are triggered when:
- You comment "@codex security review"
- A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review
Once complete, Codex will leave suggestions, or a comment if no findings are found.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: df05891479
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
… stops Stopping a running dedicated Coder workspace kills any detached background job that survived an unclean Xum exit, and those jobs' spawn records live on the remote host where the host-local crash-orphan scans cannot see them. Model-driven archives (refuseLiveUserActivity) now probe the remote record layout through the runtime before the stop: exit markers and provably dead PIDs settle records; meta-less/torn records, live or recycled PIDs, and unreachable or garbled probes refuse the archive (fail closed). Workspaces the control plane already reports stopped/gone skip the probe (the stop no-ops and no job can be running), and user-mediated archives remain the escape hatch.
|
@codex review |
downloadStagedAttachment read from the checkout through the runtime without any archive admission: a model-driven archive could pass its live-activity gate mid-read, remove a snapshot-managed checkout under the download, or (on a dedicated Coder target) stop the workspace only for the admitted read to reconnect and restart it. Downloads now mirror stageAttachment exactly: synchronous archivingWorkspaces refusal at entry, a preflight admission on the shared attachment-transfer counter observed by the sink gate and the pre-interruption hold, and a persisted archived-state check.
|
@codex review |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ed25881d79
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…en spawn-record dirs
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d9c73b73e2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Security Review
Here are some automated security review suggestions for this pull request.
Reviewed commit: d9c73b73e2
Only the user who started this review can view the report in Codex.
ℹ️ About Codex security reviews in GitHub
This is an experimental Codex feature. Security reviews are triggered when:
- You comment "@codex security review"
- A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review
Once complete, Codex will leave suggestions, or a comment if no findings are found.
…rchive holds to turn correlations
|
@codex review |
|
Codex Review: Didn't find any major issues. 🎉 Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
|
@codex security review |
There was a problem hiding this comment.
💡 Codex Security Review
Here are some automated security review suggestions for this pull request.
Reviewed commit: fe9ecc342a
Only the user who started this review can view the report in Codex.
ℹ️ About Codex security reviews in GitHub
This is an experimental Codex feature. Security reviews are triggered when:
- You comment "@codex security review"
- A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review
Once complete, Codex will leave suggestions, or a comment if no findings are found.
Summary
Restores the
task_workspace_lifecycletool — removed from the executable toolset by #3825 as collateral of the sub-agent lifecycle consolidation — trimmed to the two reversible verbsarchiveandunarchive, scoped strictly to workspace-turn peer workspaces the calling workspace created viatask(kind="workspace").Background
Orchestrating agents can create peer workspaces with
task({ kind: "workspace" })but had no tool to archive them, so loop skills (e.g.issue-triage-loop) that still instruct agents to calltask_workspace_lifecycleaccumulated un-archivable peer workspaces in the sidebar. The Zod result schemas and the transcript renderer (WorkspaceLifecycleToolCall) survived the removal, so this is mostly a resurrection from88580ca7d^—unarchiveis the only genuinely new backend surface (the historical tool never implemented it).Implementation
TaskWorkspaceLifecycleToolInputSchema): onlyarchive/unarchive, noforce. The historical 4-action args schema stays untouched so old transcripts still parse;delete_worktree/removeare not model-invocable —task_removeremains the sole irreversible verb.taskService.ts):archiveOwnedWorkspaceTurnWorkspace+ helpers restored verbatim from88580ca7d^; newunarchiveOwnedWorkspaceTurnWorkspacemirrors it with interruption hard-disabled (defense-in-depth: an archived workspace should never have active turns; if a race surfaces one, unarchive reportsactiveinstead of interrupting).taskHandleStore.isWorkspaceOwnedBy) as the sole source of truth; workspace config tags are hints only.workspaceLifecycleLocksMutexMap wraps the flow;workspaceService.archiveinternally takes the task-tree lifecycle lock for the same key, so an invariant comment forbids calling these helpers while holdingwithTaskTreeLifecycleLock(same-key non-reentrant acquisition would deadlock).baseTools, PTC bridging (BridgeableToolName+RESULT_SCHEMAS),PRESERVE_OUTPUT_TOOLSfor shared transcripts; removed from explore/plan/desktop agent allowlists (+ regenerated builtin agent/skill/docs artifacts). Zero frontend changes.Validation
Live dogfooding in a
dev-server-sandboxinstance (screenshots/video in the workspace transcript):task(kind="workspace")→archivearchived; peer hidden from sidebar, listed under project-page Archived Workspacesinvalid_scopeunarchivebywst_handleunarchived; peer restored to sidebartask(mode:"existing")follow-up post-unarchiverequires_confirmationwith paths → re-call withacknowledged_untracked_paths→archivedTargeted suites: tool layer (6), taskService lifecycle (9, incl. concurrent-handle serialization and the archive→refusal→unarchive round-trip), schema gate rejecting
remove/delete_worktree/force, renderer UI tests (21). The 3 pre-existingtaskService.test.tsfailures reproduce identically on a clean-HEAD probe worktree.Risks
Low-to-moderate, contained to agent-driven workspace lifecycle: the archive path re-reads metadata under the per-target lock (idempotency covered by tests), and misuse surfaces as safe statuses (
invalid_scope/active/requires_confirmation) rather than destructive actions. The riskiest surface — irreversible removal — is deliberately not restored.📋 Implementation Plan
Restore
task_workspace_lifecycle: agent-driven archive/unarchive of workspace-turn peer workspacesProblem
An orchestrating agent can create peer workspaces with
task({ kind: "workspace" })(workspace turns), but has no tool to archive them. The purpose-built tooltask_workspace_lifecycle(added in PR #3633) was removed from the executable toolset by PR #3825 (88580ca7d, "simplify persistent sub-agent lifecycle") as collateral of the sub-agent lifecycle consolidation. Loop skills (e.g.issue-triage-loop) still instruct agents to call it, and orchestrator workflows accumulate un-archivable peer workspaces in the sidebar.Design decision
Restore
task_workspace_lifecycleas an executable tool, trimmed toarchive+unarchive, scoped strictly to workspace-turn targets owned by the calling workspace. Do not overloadtask_remove(its model-facing contract is "irreversible removal of inactive sub-agents"; archive is reversible — mixing them invites destructive model mistakes and bloats a clean result schema).Key facts making this cheap (verified on HEAD and at
88580ca7d^):taskHandleStore.isWorkspaceOwnedBy(ownerWorkspaceId, workspaceId)(HEADsrc/node/services/taskHandleStore.ts:247) checkscreatedWorkspaceflags on durable workspace-turn handle records stored under the owner's session dir (never pruned, so ownership cannot silently expire). Authorization must use these handle records as the sole source of truth. Themux.taskOwnerWorkspaceId/mux.taskHandleId/mux.taskTurnIdtags stamped on created workspaces (src/constants/workspaceTags.ts) are correlation/recovery/UI hints only — config metadata is not an authorization surface.src/common/utils/tools/toolDefinitions.ts~1221:TaskWorkspaceLifecycle*), includingunarchiveaction +unarchived/already_unarchivedstatuses.src/browser/features/Tools/WorkspaceLifecycleToolCall.tsx, registered ingetToolComponent.ts:227, icon inToolPrimitives.tsx:288) → zero frontend work.archiveOwnedWorkspaceTurnWorkspace,resolveOwnedWorkspaceLifecycleTarget, helpers) and tool file (134 lines) + test file (142 lines) are recoverable viagit show '88580ca7d^:<path>'.workspaceService.archive(workspaceId, acknowledgedUntrackedPaths?)(HEADworkspaceService.ts:7237) already handles lifecycle locking, init-abort, active-descendant refusal, and the lossy-snapshot untracked-file confirmation;workspaceService.unarchiveexists (exposed via ORPCworkspace.unarchive,router.ts:4516).["archive","delete_worktree","remove"]—unarchivenever had an implementation; the schema on HEAD is already extended for it.unarchiveOwnedWorkspaceTurnWorkspaceis the only genuinely new backend code.Alternatives considered (rejected)
task_removewith anaction/archive moderequires_confirmation,already_archived), re-muddies #3825's separation of execution state vs retentiontask_archivetool nameissue-triage-loopskill compat; historical transcripts diverge from live toolxum api workspace archivevia bashtask_workspace_lifecycle(chosen)Scope
In scope:
archiveandunarchiveactions for workspace-turn targets (wst_*handle IDs or owned peerworkspaceIds).interrupt_active), untracked-file confirmation round-trip.Out of scope (explicitly not restored):
remove/delete_worktreeactions —task_removestays the only irreversible verb; these can be resurrected later by the same recipe.wst_task IDs stayinvalid_scope.unarchiveinstead;createWorkspaceTurnmode"existing"keeps refusing archived targets).Implementation
Recovery source for all restored code:
git show '88580ca7d^:<path>'.Phase 1 — Schema & tool definition (
src/common/utils/tools/toolDefinitions.ts)Add a narrowed live-input schema next to the existing kept schemas (~line 1249). Do not modify
TaskWorkspaceLifecycleActionSchema(4 actions) orTaskWorkspaceLifecycleToolArgsSchema— both are still referenced by the result schema and the renderer's parsing of historical transcripts (getToolComponent.ts:229,src/common/types/tools.ts:309):Reuses
TaskWorkspaceLifecycleTargetSchema(exactly-one-of taskId/workspaceId superRefine). Dropsforce(only applied to the un-restoredremoveaction). All optional params.nullish()per repo tool-schema convention.Split rationale: the broad historical args schema +
TaskWorkspaceLifecycleToolArgstype (src/common/types/tools.ts:309) stay untouched for historical-transcript parsing and renderer compatibility; the narrowed input schema is whatTOOL_DEFINITIONS.task_workspace_lifecycle.schemaadvertises to models. Export a live input type intypes/tools.tsonly if the tool file needs it — do not repoint the existing type.Add the
TOOL_DEFINITIONSentry (~line 2286, adjacent totask_remove), description rewritten for the trimmed contract:task(kind="workspace");interrupt_active: true;requires_confirmationwith untracked paths → re-call withacknowledged_untracked_paths;task(kind="workspace", mode="existing")follow-ups until unarchived.Register availability: add
"task_workspace_lifecycle"to thebaseToolsarray ingetAvailableTools(~line 3349, next totask_remove).PTC bridging parity (2 lines): add
"task_workspace_lifecycle"to theBridgeableToolNameunion (~3198) andRESULT_SCHEMAS(~3228, →TaskWorkspaceLifecycleToolResultSchema) so RLM/PTC sandbox sessions get the same task-tool surface astask_remove.Phase 2 — Backend restoration (
src/node/services/taskService.ts)No name collisions on HEAD;
coerceNonEmptyString,WORKSPACE_TURN_TASK_TAGS,isWorkspaceArchived,TaskWorkspaceLifecycleToolTargetResultSchemaimport, and theWorkspaceLifecycleResulttype alias (line 213) are already present.Restore verbatim-then-adapt from
88580ca7d^:src/node/services/taskService.ts:private readonly workspaceLifecycleLocks = new MutexMap<string>()(historical line 1200) — a dedicated per-target-workspace lock outsideworkspaceService.archive. Do not wrap the call inwithTaskTreeLifecycleLockexternally:archivealready takes that lock internally for the same key, and a non-reentrant same-key acquisition would deadlock. Implementation invariant (enforce via code comment on the lock field): no code path may acquire the task-tree lifecycle lock and then call these workspace-lifecycle helpers.resolveOwnedWorkspaceLifecycleTarget(ownerWorkspaceId, action, target)(historical 7401) — resolveswst_*handle → workspaceId viataskHandleStore.getWorkspaceTurn(HEAD :173), then gates ontaskHandleStore.isWorkspaceOwnedBy(HEAD :247; records are never pruned, so ownership cannot silently expire); non-wst_taskIds and non-owned workspaceIds →invalid_scope.withWorkspaceLifecycleLock(historical 7388),lifecycleTargetFields(historical ~7454),findWorkspaceLifecycleMetadata(historical ~7473).handleActiveWorkspaceLifecycleTurns(historical 7495) — both callees survive on HEAD:listWorkspaceTurnTasks(:8496) andinterruptWorkspaceTurn(:8512). Filters owner's turns with statusqueued|starting|runningtargeting the resolved workspace; returnsactive+activeTaskIdsunlessinterruptActive, in which case it interrupts each turn.archiveOwnedWorkspaceTurnWorkspace(ownerWorkspaceId, target, options)(historical 7208) — under the lifecycle lock:not_found(metadata absent) /already_archived(idempotent) / active-turn handling /workspaceService.archive(workspaceId, acknowledgedUntrackedPaths); mapskind: "confirm-lossy-untracked-files"→requires_confirmation+paths.workspaceService.archiveerrors surface asstatus: "error"(includes the active-descendant-sub-agent refusal,ACTIVE_DESCENDANT_ARCHIVE_ERROR).unarchiveOwnedWorkspaceTurnWorkspace(ownerWorkspaceId, target)(~30 lines, mirrors archive): resolve + lock →not_found/already_unarchived(viaisWorkspaceArchived) →workspaceService.unarchive(workspaceId)(HEAD :7514,Result<void>) →unarchived|error.interrupt_activeapplies toarchiveonly. An archived workspace should never have active turns (archive refuses while active;createWorkspaceTurnrefuses archived targets), but as defense-in-depth unarchive still runs the active-turn check with interruption hard-disabled: if a race/corruption surfaces an active turn, returnactive— never interrupt on unarchive, even if the caller passedinterrupt_active: true. Document this in the tool description.Phase 3 — Tool file + registration
src/node/services/tools/task_workspace_lifecycle.ts(134 lines historical → ~100 trimmed): keepnormalizeTarget/targetKeydedup andrejectInvalidWorkspaceTaskId(non-wst_→invalid_scope); dispatch onlyarchive→archiveOwnedWorkspaceTurnWorkspaceandunarchive→unarchiveOwnedWorkspaceTurnWorkspace; keep theplanFileOnlythrow (same pattern astask_remove.ts:17); input schema = the new narrowedTaskWorkspaceLifecycleToolInputSchema; results parsed against the keptTaskWorkspaceLifecycleToolResultSchema.src/common/utils/tools/tools.ts: importcreateTaskWorkspaceLifecycleTool(~line 42) and registertask_workspace_lifecycle: wrap(createTaskWorkspaceLifecycleTool(config))(~line 773).src/node/builtinAgents/): addtask_workspace_lifecycletotools.removeinexplore.md(line ~30, alongsidetask_remove),plan.md(line ~23), anddesktop.md(line ~43) so read-only/plan/desktop agents can't mutate workspace lifecycle. Regeneration is automatic: the Make rule$(BUILTIN_AGENTS_GENERATED): src/node/builtinAgents/*.md scripts/generate-builtin-agents.sh(Makefile:246) rebuildsbuiltInAgentContent.generated.ts, andmake typecheck(part ofstatic-check) lists it as a prerequisite — so a stale generated file cannot pass validation. Commit the regenerated file. No changes to the policy engine — it filters dynamically by name.src/common/utils/messages/transcriptShare.ts(~line 133): add"task_workspace_lifecycle"toPRESERVE_OUTPUT_TOOLSso shared transcripts keep target-status rows.src/browser/features/Settings/Sections/TasksSection.agents.ts(~line 62): add to the agent-template remove arrays wheretask_removeappears, for template parity.WorkspaceLifecycleToolCall.tsxalready renders all statuses includingunarchived/already_unarchived(exhaustiveRecord<TaskWorkspaceLifecycleStatus, StatusMeta>, lines 101–125) with existing UI tests.Phase 4 — Tests (behavioral, no tautologies)
Restore + adapt
src/node/services/tools/task_workspace_lifecycle.test.ts(historical file; all utilities —TestTempDir,createTestToolConfig,mockToolCallOptions— survive on HEAD intools/testHelpers.ts):wst_taskId →invalid_scopewithout touching taskService;planFileOnlythrows; unarchive routes to the unarchive method (replaces the historical delete_worktree/remove routing test);acknowledged_untracked_pathsforwarding when the target is specified bytaskId(notworkspaceId) — the tool must pass the full by-workspaceId map so the backend can apply it after handle→workspaceId resolution.taskService.test.ts, alongside existing workspace-turn tests):invalid_scope; owned (createdWorkspace record) → proceeds.already_archived) and unarchive idempotency (already_unarchived).active+activeTaskIds; withinterrupt_active: true→ interrupts then archives.workspaceService.archivereturningconfirm-lossy-untracked-files→requires_confirmationwith paths; second call withacknowledged_untracked_paths→archived.createWorkspaceTurn(mode: "existing")refused; after unarchive → succeeds again (the round-trip that motivates unarchive).wst_*taskId and byworkspaceId(both resolution paths).activeeven wheninterrupt_active: true.toolDefinitions.test.ts): live input schema rejectsremove/delete_worktreeactions and rejectsforce(behavioral gate: irreversible verbs and their escape hatch must not be model-invocable through this tool). Built-in agent remove-lists are covered by codegen + typecheck staleness (above); add an agent-definition test only if an existing suite already asserts remove-lists fortask_remove(follow precedent, don't invent a new tautology).Phase 5 — Validation & dogfooding gate
make static-check+ targeted suites:bun test src/node/services/tools/task_workspace_lifecycle.test.ts src/node/services/taskService.test.ts src/common/utils/tools/toolDefinitions.test.tsand the renderer UI test (WorkspaceLifecycleToolCall.ui.test.tsx) to confirm no schema drift.Dogfooding
Environment:
make dev-server-sandbox(project skilldev-server-sandbox) — the web dev server, not the Electron desktop app. Isolated tempXUM_ROOT, free backend + Vite ports, seededproviders.jsonc/config.json. Run as a monitored background bash task (filter: "ready|listening|localhost|ERROR|EADDRINUSE|failed|Failed",timeout_secs: 1800).This environment is already validated in this workspace (pre-flight run on the unmodified branch): sandbox boots in ~1 min (temp root
/tmp/mux-dev-server-*, backend127.0.0.1:<port>, Vite127.0.0.1:<port>), andagent-browser --session <s> open http://127.0.0.1:<vite-port>/connects headlessly — no Electron, no Xvfb. The app renders the full sidebar and, on the project page, an "Archived Workspaces (N)" section — the exact UI surface that verifies archive state transitions. Seededconfig.jsoncontains real projects; dogfood against a scratch project (or launch withDEV_SERVER_SANDBOX_ARGS="--clean-projects"and add one).agent-browser flow (per
agent-browsercore +dogfoodskills):snapshot -ifor element refs →click/fillby@eNref → re-snapshot after page changes;screenshot <file>.pngfor stills;record start/stop <file>.webmfor videos (per the agent-browser/dogfood skill workflow);errors/consolefor renderer errors after each step.Evidence requirements (per
dogfoodskill: repro-first, evidence per step): screenshots per step AND arecord start/record stopwebm video of the core archive→unarchive flow, paced human-watchably (sleep 1between actions). All evidence attached viaattach_filefor reviewer verification.Script (each step evidenced with an
agent-browserscreenshot, attached viaattach_file):task({kind:"workspace", ...}), wait for its turn to settle, then calltask_workspace_lifecycle({action:"archive", targets:[{workspaceId:...}]})”.invalid_scopein the tool card. Screenshot.requires_confirmationcard listing paths; re-call withacknowledged_untracked_paths→ archived. Screenshot the confirmation card.{action:"unarchive"}→ workspace returns to the sidebar; then atask(kind="workspace", mode="existing")follow-up succeeds. Screenshot.WorkspaceLifecycleToolCallcard renders archived/unarchived/invalid_scope statuses correctly in the real app (not just Storybook).Deliverables: screenshots per step; if a step can't run headless, fall back to the integration-test equivalent and state exactly which steps were verified live vs. by test.
Acceptance criteria
task(kind="workspace")usingtask_workspace_lifecycle({action:"archive"}), and unarchive it with{action:"unarchive"}.wst_) task IDs are refused withinvalid_scope.interrupt_active: true(which interrupts, then archives); unarchive never interrupts regardless ofinterrupt_active.requires_confirmation→acknowledged_untracked_pathsround-trip;forceno longer exists on the input schema.mode:"existing"follow-ups; unarchive restores them.remove/delete_worktreeare not model-invocable through this tool (schema-rejected);task_removebehavior unchanged.task_workspace_lifecyclecalls still render (renderer untouched, result schema unchanged).make static-checkgreen; all Phase 4 suites green; dogfooding evidence captured.issue-triage-loopskill's documented calltask_workspace_lifecycle({action:"archive", targets:[{workspaceId}], interrupt_active:false})works verbatim.Risks & mitigations
workspaceLifecycleLockswrappingworkspaceService.archive's internal task-tree lock): restore the historical ordering exactly; the archive path re-reads metadata after acquiring the lifecycle lock, so stale-metadata races are handled as before. Covered by the idempotency tests.unarchived/already_unarchived; the restored backend emits only statuses in the kept union, andtoolDefinitions.test.tsalready validates the schemas — renderer exhaustiveness (Record<Status, Meta>) fails typecheck if a status is added without UI handling.task_remove: mitigated by the description contrast (reversible vs irreversible) and byinvalid_scopefor sub-agent IDs; watch Codex review for wording feedback.Estimate
~320 net LoC product code (schema ~25, taskService ~180, tool ~100, registration ~15) + ~250 LoC tests. No migrations; upgrade/downgrade safe (config format untouched; archive fields already exist).
Generated with
xum• Model:anthropic:claude-fable-5• Thinking:xhigh• Cost:$53.33