Skip to content

🤖 feat: simplify persistent sub-agent lifecycle - #3825

Merged
ammario merged 57 commits into
mainfrom
feat/persistent-subagent-experience
Aug 11, 2026
Merged

🤖 feat: simplify persistent sub-agent lifecycle#3825
ammario merged 57 commits into
mainfrom
feat/persistent-subagent-experience

Conversation

@ammar-agent

@ammar-agent ammar-agent commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator

Summary

Makes sub-agents durable, reusable child workspaces with one consistent public lifecycle:

active → inactive → removed

A report or task_stop makes a child inactive without deleting its workspace. task_send_message steers active work or reawakens an inactive child under the same stable task ID and workspace context. task_retitle updates its short, friendly reusable role name. task_remove is the only irreversible cleanup operation.

The left sidebar now shows sub-agents only while they are active; stale runtime signals cannot resurrect terminal rows. The chat transcript's sub-agent decoration is the canonical persistent hierarchy and remains available for inactive children. Redundant private-continuation wake prompts are suppressed once the stable child report/failure is in parent history, while remaining machine-authored background-work prompts render as compact expandable events.

Background

The previous task API mixed execution state with workspace retention:

  • task_terminate destructively removed active sub-agents;
  • task_workspace_lifecycle exposed archive/unarchive/worktree concepts;
  • sticky and preserveSubagentsUntilArchive created multiple retention classes;
  • completed children required a different operation to continue;
  • task-like workspace titles became stale when a persistent child received new assignments;
  • inactive children could accumulate without prompting the parent to retain or remove them deliberately;
  • stopping useful work merely to reconcile a turn marked the child interrupted rather than completed.

The new contract treats every user-owned sub-agent as a persistent specialist with a stable public identity and a parent-managed lifecycle.

Implementation

Uniform child identity and continuation recovery

  • Keeps one stable public task ID equal to the child workspace ID.
  • Persists completed/stopped user-owned children until explicit removal.
  • Keeps internal wst_* continuation handles private.
  • Routes list/await/send operations through the stable child identity, including nested children whose direct parent owns the continuation record.
  • Validates canonical ISO timestamps while recovering the newest continuation after crashes.
  • Replays a stopped queued child's preserved initial brief when its first continuation starts.
  • Mirrors only the terminal settlement that actually wins concurrency races.

Public task operations

  • task_stop — cancels active work non-destructively and leaves unfinished children interrupted. Parent guidance now distinguishes cancellation from asking useful work to finalize normally.
  • task_send_message — steers queued/running work or reawakens an inactive child in the same workspace, preserving the child's current model/thinking settings.
  • task_retitle — changes a persistent child’s friendly role label, such as Reviewer or Simplicity Auditor, without changing stable identity or filesystem paths.
  • task_remove — irreversibly deletes inactive child workspaces, rejects active children, and removes nested children deepest-first.
  • task_list — keeps private continuation handles hidden and adds event-driven housekeeping guidance when it encounters inactive user-owned children.
  • Removes task_workspace_lifecycle and task_terminate from the advertised/executable toolset while retaining compatibility renderers for historical transcripts.

Lifecycle, cleanup, and concurrency

  • Parents reconcile active descendants before finishing a turn.
  • After consuming a terminal report, parents decide whether to retain a reusable specialist or remove a one-shot/obsolete child.
  • Before completing a large task or PR, parents are guided to list reported/interrupted children and clean up stale ones deepest-first.
  • Compaction/restart recovery rediscovers inactive children without deleting them automatically.
  • Initial creation, reawakening, archive, and direct removal share a task-tree lifecycle lock so ancestors cannot disappear while child work starts.
  • Reawakened children count once toward parallel quotas rather than once as a child and again as a continuation.
  • Explicit stops supersede pending terminal wake-ups; intentional background work retains one-shot terminal attention.
  • Persistent-child reports/failures supersede the private continuation handle's duplicate wake, while ordinary workspace turns and continuations without a current report still direct the model through task_await.

Naming and UI

  • Parent/system/tool guidance requests short, friendly reusable role names instead of ordinary task-summary chat titles.
  • Adds a task_retitle transcript card, tool icon, schema/bridge registration, Storybook state, and shared-transcript output preservation.
  • task_list cards render inactive-child cleanup guidance.
  • Inactive/reported/interrupted child rows disappear from the left sidebar automatically, including terminal workflow members and selected inactive children; stale live store state cannot reintroduce a completed row.
  • Active, queued, and reawakened children remain visible.
  • Background-work control prompts use a compact, expandable machine-event component instead of a full user-message bubble; user-authored lookalikes remain ordinary escaped user messages.
  • The transcript sub-agent decoration remains the canonical nested hierarchy for persistent children and reports terminal continuation outcomes accurately.

Retention compatibility and simplification

  • Removes sticky from task inputs, listing, and runtime behavior while retaining inert legacy fields for downgrade compatibility.
  • Keeps the legacy preserveSubagentsUntilArchive field canonicalized to true for older builds but removes it from runtime/UI decisions.
  • Workflow-owned workers remain transient because their workflow journal owns durable output.
  • Centralizes active workspace-turn status checks, continuation lookup, and WorkspaceService lifecycle-lock dispatch, reducing duplicated lifecycle branching.

Validation

  • make static-check
  • make typecheck
  • Full TaskService and WorkspaceService suites
  • Task create/await/list/send/retitle/stop/remove suites
  • Tool schema, bridge, built-in agent policy, system-prompt, and transcript-sharing suites
  • Task tool UI/registry/story tests
  • MessageRenderer machine-wake tests and Storybook interaction suite
  • Project sidebar, sidebar grouping/filtering, AgentListItem, and sub-agent decoration suites
  • Focused simplification validation: 713 lifecycle tests passed
  • QuickJS runtime suite in a fresh process, plus 50 repeated coverage runs while classifying a CI-only callback-loss flake

Risks

  • Downgrades: older builds still interpret retained compatibility fields with previous archive-oriented behavior.
  • Internal executions: stable-child routing must remain synchronized with private continuation records.
  • Removal: task_remove is intentionally irreversible, though parent transcript/report evidence may remain.
  • Sidebar discoverability: inactive children intentionally leave the sidebar; users access them through the canonical transcript decoration.
  • Intentional wakes: ordinary workspace turns and continuations that do not deliver a current stable-child report still require a later task_await wake; the transcript presents that control prompt compactly.
  • CI isolation: QuickJS-heavy runtime tests run in fresh Bun processes because the monolithic coverage process can crash or lose asyncified runtime callbacks.

Generated with mux • Model: openai:gpt-5.6-sol • Thinking: xhigh • Cost: $1586.01

@mintlify

mintlify Bot commented Aug 9, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
Mux 🟢 Ready View Preview Aug 9, 2026, 9:02 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@ammar-agent

Copy link
Copy Markdown
Collaborator Author

@codex review

Please review the persistent sub-agent lifecycle, parent-scoped cleanup, and chat decoration experience.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 140059526d

ℹ️ 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".

Comment thread src/common/types/tasks.ts
@ammar-agent
ammar-agent force-pushed the feat/persistent-subagent-experience branch from 1400595 to 56c3bba Compare August 9, 2026 21:11
@ammar-agent

Copy link
Copy Markdown
Collaborator Author

Addressed the Codex P1 downgrade concern: config load now migrates both missing and legacy preserveSubagentsUntilArchive: false values to persisted true through the serialized migration queue before task startup. Added coverage for both on-disk forms and re-ran make static-check.

@ammar-agent

Copy link
Copy Markdown
Collaborator Author

@codex review

Please re-review the downgrade-safe persistence migration and the complete updated diff.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Can't wait for the next one!

Reviewed commit: 56c3bbac19

ℹ️ 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".

@ammar-agent
ammar-agent force-pushed the feat/persistent-subagent-experience branch 2 times, most recently from d082dc4 to fab594e Compare August 9, 2026 21:43
@ammar-agent

Copy link
Copy Markdown
Collaborator Author

@codex review

Please review the one-time retention migration marker, restored opt-out compatibility, workspace-turn ownership check, and complete updated diff at the latest commit.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep it up!

Reviewed commit: fab594e5f3

ℹ️ 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".

@ammar-agent
ammar-agent force-pushed the feat/persistent-subagent-experience branch from fab594e to ec23165 Compare August 9, 2026 21:49
@ammar-agent

Copy link
Copy Markdown
Collaborator Author

@codex review

Please review the final parent-facing lifecycle contract in the system prompt, task/list/terminate/lifecycle tool descriptions, and deep-review skill alongside the implementation.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ec231657e3

ℹ️ 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".

Comment thread src/node/services/systemMessage.ts Outdated
@ammar-agent

Copy link
Copy Markdown
Collaborator Author

Addressed the sticky lifecycle mismatch: parent/system/tool/schema guidance now states that an unarchived sticky child blocks ancestor archive and must receive its own archive/remove action first. This matches the existing preflightArchive/archive guard. Re-ran system-message, tool-definition, sticky lifecycle, and static checks.

@ammar-agent

Copy link
Copy Markdown
Collaborator Author

@codex review

Please re-review the corrected sticky lifecycle contract and complete latest diff.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dd5c583375

ℹ️ 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".

Comment thread src/node/services/taskService.ts
@ammar-agent

Copy link
Copy Markdown
Collaborator Author

Addressed the descendant-orphaning P1: removeOwnedTaskWorkspace now refuses removal while any descendant sub-agent workspace remains, reports the blocking descendant IDs, and the parent/system/tool guidance directs nested removals deepest-first. Added a regression proving parent removal is blocked until child then parent are removed. Static checks and focused lifecycle tests pass.

@ammar-agent
ammar-agent force-pushed the feat/persistent-subagent-experience branch from dd5c583 to a8726bc Compare August 9, 2026 22:06
@ammar-agent

Copy link
Copy Markdown
Collaborator Author

@codex review

Please re-review descendant-safe lifecycle removal and the complete latest diff.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a8726bcd55

ℹ️ 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".

Comment thread src/common/types/tasks.ts
Comment thread src/node/services/tools/task_workspace_lifecycle.ts Outdated
@ammar-agent

Copy link
Copy Markdown
Collaborator Author

Addressed both latest lifecycle findings:

  • direct WorkspaceService.remove(..., force: true) and retention opt-out paths now refuse removal whenever any descendant sub-agent workspace remains, so archived-workspace UI deletion cannot orphan the tree
  • task_workspace_lifecycle now executes targets sequentially in caller order, so a deepest-first nested remove batch deterministically removes child then parent

Added direct-force, retention-opt-out, service lifecycle, and sequential batch regressions. Focused tests, typecheck, and make static-check pass.

@ammar-agent

Copy link
Copy Markdown
Collaborator Author

Addressed Codex thread PRRT_kwDOPxxmWM6YHu6u in 2678ed3f06. The final task_await attention-consumption API now carries consumingWorkspaceId and returns without touching owner-scoped workspace-turn attention when consumer and continuation owner differ. The existing nested-owner regression now invokes both snapshot consumption and this later API, confirming the higher ancestor's pending wake survives the full await path. Task-await tests assert consumer identity propagation for immediate, waited, timeout-race, and error paths. Full TaskService, task_await, typecheck, and make static-check pass.

@ammar-agent

Copy link
Copy Markdown
Collaborator Author

@codex review

Please review the latest head, including full-path owner-scoped attention preservation for your latest P2 finding.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

https://github.com/coder/mux/blob/2678ed3f0670ae1a8f9a50cbafac35fc304c31b9/src/node/services/taskService.ts#L5565
P1 Badge Contain terminal-notification recovery failures

When startup finds a terminal notify_on_terminal workspace turn whose marker is missing, this enqueue writes into the owner’s session directory; a permission or I/O failure rejects recoverTerminalWorkspaceTurnAttentionNotifications() and then TaskService.initialize(), preventing the app from starting. Fresh evidence at the current head is that direct-parent replay is now caught per record, but this subsequent enqueue remains outside that catch. Keep this recovery best-effort as well.

AGENTS.md reference: AGENTS.md:L112-L112

ℹ️ 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".

Comment thread src/node/services/taskService.ts Outdated
---

_Generated with `mux` • Model: `openai:gpt-5.6-sol` • Thinking: `xhigh` • Cost: `$1543.70`_

<!-- mux-attribution: model=openai:gpt-5.6-sol thinking=xhigh costs=1543.70 -->
@ammar-agent

Copy link
Copy Markdown
Collaborator Author

Addressed Codex thread PRRT_kwDOPxxmWM6YHzWz in d9d964605f. Workspace-turn settlement now suppresses the owner-scoped wake only when foregroundWaiterWorkspaceIds contains the continuation owner. A waiter in another workspace, including the persistent child's direct parent, receives its result without consuming the owner's wake. Added a production settlement regression with a root-owned nested continuation and direct-parent foreground waiter; after settlement, the root's workspace-turn attention remains pending or delivered rather than absent. Full TaskService, task_await, typecheck, and make static-check pass.

@ammar-agent

Copy link
Copy Markdown
Collaborator Author

@codex review

Please review the latest head, including owner-specific foreground waiter wake suppression for your latest P2 finding.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d9d964605f

ℹ️ 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".

Comment thread src/node/services/taskService.ts
---

_Generated with `mux` • Model: `openai:gpt-5.6-sol` • Thinking: `xhigh` • Cost: `$1550.23`_

<!-- mux-attribution: model=openai:gpt-5.6-sol thinking=xhigh costs=1550.23 -->
@ammar-agent

Copy link
Copy Markdown
Collaborator Author

Addressed Codex thread PRRT_kwDOPxxmWM6YH48f in 74dd14f46e. Corrected/resettled owner-scoped workspace-turn attention now uses the exact terminal outcome generation (handleId:status:updatedAt), while legacy IDs remain compatible for initial/older notifications. Stale and versioned IDs are deleted independently; consumption creates and delivers both tombstones under the settlement lock and is keyed to the exact returned status + updatedAt. A stale drain can therefore transition only its old ID and cannot swallow the corrected notification. Added a regression that completes the stale legacy drain after corrected replacement and verifies the corrected version remains persisted. Full TaskService, task_await, TerminalAttentionStore, typecheck, and make static-check pass.

@ammar-agent

Copy link
Copy Markdown
Collaborator Author

@codex review

Please review the latest head, including outcome-versioned corrected workspace-turn attention for your P1 finding.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

https://github.com/coder/mux/blob/74dd14f46eba3e3c75d7c41433b6d748591dba78/src/node/services/taskService.ts#L5566-L5570
P1 Badge Recover corrected attention with the outcome version

If the process exits after a self-healed outcome is persisted but before the post-lock corrected notification is enqueued, the stale outcome's delivered/superseded legacy tombstone remains. Startup recovery enqueues only the unversioned workspace_turn:<handleId> ID, so enqueueIfAbsent() finds that tombstone, while recovery still records terminalAttentionNotifiedAt; the corrected outcome then never wakes the idle owner. Use the corrected record's outcome-versioned generation ID in this recovery path as well.

AGENTS.md reference: AGENTS.md:L111-L112

ℹ️ 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".

---

_Generated with `mux` • Model: `openai:gpt-5.6-sol` • Thinking: `xhigh` • Cost: `$1561.58`_

<!-- mux-attribution: model=openai:gpt-5.6-sol thinking=xhigh costs=1561.58 -->
@ammar-agent

Copy link
Copy Markdown
Collaborator Author

Addressed the latest Codex P1 (Recover corrected attention with the outcome version) in 36c712181f. Startup recovery now enqueues terminal workspace-turn attention with the exact handleId:status:updatedAt generation, so a delivered legacy tombstone cannot suppress a corrected self-healed outcome. The recovery marker is written only if the same terminal generation is still current. I also addressed the prior startup-containment observation from the earlier review: notification enqueue/marker work is isolated per record, so a read-only or corrupt owner session cannot reject TaskService.initialize() or block later records. Added regressions for both legacy-tombstone recovery and per-record I/O failure containment. Full TaskService, task_await, typecheck, and make static-check pass.

@ammar-agent

Copy link
Copy Markdown
Collaborator Author

@codex review

Please review the latest head, including outcome-versioned and failure-contained startup recovery for corrected workspace-turn attention.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

https://github.com/coder/mux/blob/36c712181f405f20c8e3eb3cac7936bcf1098e71/src/node/services/taskService.ts#L2560
P1 Badge Contain unreadable attention directories during startup

When any session's terminal-attention directory cannot be read, such as from a permission or I/O error, listPendingOwnerWorkspaceIds() propagates the failure because its per-owner listPending() call only handles ENOENT; this unguarded await then rejects TaskService.initialize() and prevents the app from starting. The new per-record workspace-turn catch does not contain this subsequent startup scan, so isolate failures by owner or wrap the scan and continue.

AGENTS.md reference: AGENTS.md:L112-L112

ℹ️ 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".

Comment thread src/node/services/taskService.ts Outdated
---

_Generated with `mux` • Model: `openai:gpt-5.6-sol` • Thinking: `xhigh` • Cost: `$1574.17`_

<!-- mux-attribution: model=openai:gpt-5.6-sol thinking=xhigh costs=1574.17 -->
@ammar-agent

Copy link
Copy Markdown
Collaborator Author

Addressed Codex thread PRRT_kwDOPxxmWM6YIbAY in 869f0386e1. All newly persisted workspace-turn terminal attention now uses the same exact handleId:status:updatedAt generation in ordinary settlement, corrected resettlement, late notify-on-terminal policy races, and startup recovery. Recovery remains backward-compatible: a legacy notification dedupes only when its outcome matches and its creation time is at/after the current terminal record; stale/different generations get a versioned notification. Added regressions proving ordinary settlement persists the versioned notification, matching legacy ordinary recovery does not double-wake, and corrected legacy recovery still bypasses the stale tombstone. Full TaskService, task_await, typecheck, and make static-check pass.

@ammar-agent

Copy link
Copy Markdown
Collaborator Author

@codex review

Please review the latest head, including aligned outcome-versioned workspace-turn attention across ordinary settlement and recovery.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 869f0386e1

ℹ️ 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".

Comment thread src/node/services/taskService.ts
---

_Generated with `mux` • Model: `openai:gpt-5.6-sol` • Thinking: `xhigh` • Cost: `$1586.01`_

<!-- mux-attribution: model=openai:gpt-5.6-sol thinking=xhigh costs=1586.01 -->
@ammar-agent

Copy link
Copy Markdown
Collaborator Author

Addressed Codex thread PRRT_kwDOPxxmWM6YImA9 in d3c8246f7c. persistNotifyOnTerminalPolicy() now treats a policy-only write on an already-terminal handle as metadata, preserving updatedAt and therefore the terminal outcome generation/direct-parent delivery key. Active handles still update updatedAt normally. Added a regression that backgrounds a terminal handle, verifies its exact outcome timestamp is unchanged, and confirms attention is enqueued under that generation. Full TaskService, task_await, typecheck, and make static-check pass.

@ammar-agent

Copy link
Copy Markdown
Collaborator Author

@codex review

Please review the latest head, including terminal outcome preservation across policy-only notify-on-terminal updates.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Hooray!

Reviewed commit: d3c8246f7c

ℹ️ 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".

@ammario
ammario merged commit 88580ca into main Aug 11, 2026
21 of 22 checks passed
@ammario
ammario deleted the feat/persistent-subagent-experience branch August 11, 2026 15:18
ammario pushed a commit that referenced this pull request Aug 11, 2026
## Summary

Adds deterministic, no-live-LLM validation that persistent sub-agents
remain seamless across chat compaction. Mechanical compact turns no
longer advance child lifecycle state, reawakened continuation
correlation and agent-initiated attribution survive compaction, and the
mock AI runtime now mirrors real stream metadata closely enough for a
true backend integration test.

The PR is also a net maintainability reduction: it removes the unused
generic archive/unarchive/delete-worktree TaskService backend left
behind after persistent sub-agents adopted the public active → inactive
→ removed lifecycle. The supported `task_remove` path now uses one
direct, parent-scoped persistent-child removal implementation.

## Background

The persistent sub-agent foundation from #3825 is merged into `main`. A
child compaction stream was visible directly to `TaskService` and could
be mistaken for the end of delegated work, moving an active child toward
completion recovery. Mock streams also dropped workspace-turn
correlation, preventing high-confidence end-to-end coverage for a
reawakened persistent child.

The old generic lifecycle methods had no production callers; only
TaskService tests exercised them. Historical `task_workspace_lifecycle`
schemas and transcript rendering remain intact for old chats.

## Implementation

- Ignore compact-agent/mode stream ends in `TaskService`; only the
post-compaction delegated follow-up may settle the child or continuation
handle.
- Preserve `agentInitiated` and resolved workspace-turn correlation on
persisted compaction follow-ups and their crash-safe dispatch/retry
path.
- Carry agent identity, thinking level, and mux correlation through mock
stream start, partial/final history, and stream-end events.
- Replace the unused generic workspace lifecycle
resolver/lock/archive/worktree operations with direct persistent-child
removal that verifies scope, rejects active/non-leaf children, persists
idempotency tombstones, and removes under the task-tree lock.
- Delete lifecycle-only tests while retaining focused coverage for
scope, deepest-first removal, active-state rejection, reawakening races,
and idempotent retries.
- Add layered compaction regression coverage from focused unit tests
through a ServiceContainer/IPC integration test.

## Validation

- `make static-check`
- `bun test src/node/services/taskService.test.ts`
- `bun test src/node/services/agentSession.autoCompaction.test.ts
src/node/services/agentSession.workspaceTurnInheritance.test.ts
src/node/services/agentSession.continueMessageAgentId.test.ts
src/node/services/mock/mockAiStreamPlayer.test.ts`
- `bun test src/node/services/tools/task_remove.test.ts`
- `TEST_INTEGRATION=1 bun x jest
tests/ipc/tasks/persistentSubagentCompaction.test.ts --runInBand`
- `git diff --numstat origin/main...HEAD`: **790 additions, 1,239
deletions, net −449 LoC**

No test invokes a live LLM API.

## Risks

Low-to-moderate product risk. Compaction changes are narrowly scoped and
covered end-to-end. Lifecycle deletion removes backend methods with no
production callers; the supported `task_remove` path retains scope,
activity, descendant, race, and idempotency protections. Historical
transcript rendering remains compatible.

---

_Generated with `mux` • Model: `openai:gpt-5.6-sol` • Thinking: `xhigh`
• Cost: `$88.81`_

<!-- mux-attribution: model=openai:gpt-5.6-sol thinking=xhigh
costs=88.81 -->
ThomasK33 added a commit that referenced this pull request Aug 24, 2026
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.
Justinhubbard37 pushed a commit to Justinhubbard37/xum that referenced this pull request Aug 25, 2026
…eer workspaces (coder#3940)

## Summary

Restores the `task_workspace_lifecycle` tool — removed from the
executable toolset by coder#3825 as collateral of the sub-agent lifecycle
consolidation — trimmed to the two reversible verbs `archive` and
`unarchive`, scoped strictly to workspace-turn peer workspaces the
calling workspace created via `task(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 call
`task_workspace_lifecycle` accumulated 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 from `88580ca7d^` — `unarchive` is the only genuinely new
backend surface (the historical tool never implemented it).

## Implementation

- **Narrowed live input schema**
(`TaskWorkspaceLifecycleToolInputSchema`): only `archive`/`unarchive`,
no `force`. The historical 4-action args schema stays untouched so old
transcripts still parse; `delete_worktree`/`remove` are not
model-invocable — `task_remove` remains the sole irreversible verb.
- **Backend** (`taskService.ts`): `archiveOwnedWorkspaceTurnWorkspace` +
helpers restored verbatim from `88580ca7d^`; new
`unarchiveOwnedWorkspaceTurnWorkspace` mirrors it with interruption
hard-disabled (defense-in-depth: an archived workspace should never have
active turns; if a race surfaces one, unarchive reports `active` instead
of interrupting).
- **Authorization** uses durable workspace-turn ownership records
(`taskHandleStore.isWorkspaceOwnedBy`) as the sole source of truth;
workspace config tags are hints only.
- **Lock layering**: a dedicated per-target `workspaceLifecycleLocks`
MutexMap wraps the flow; `workspaceService.archive` internally takes the
task-tree lifecycle lock for the same key, so an invariant comment
forbids calling these helpers while holding `withTaskTreeLifecycleLock`
(same-key non-reentrant acquisition would deadlock).
- **Registration**: tool factory, `baseTools`, PTC bridging
(`BridgeableToolName` + `RESULT_SCHEMAS`), `PRESERVE_OUTPUT_TOOLS` for
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-sandbox` instance (screenshots/video in
the workspace transcript):

| Flow | Result |
|---|---|
| `task(kind="workspace")` → `archive` | `archived`; peer hidden from
sidebar, listed under project-page Archived Workspaces |
| Archive a non-owned workspace | `invalid_scope` |
| `unarchive` by `wst_` handle | `unarchived`; peer restored to sidebar
|
| `task(mode:"existing")` follow-up post-unarchive | succeeds (refused
while archived) |
| Untracked file in worktree → archive | `requires_confirmation` with
paths → re-call with `acknowledged_untracked_paths` → `archived` |

Targeted 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-existing `taskService.test.ts`
failures 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.

---

<details>
<summary>📋 Implementation Plan</summary>

# Restore `task_workspace_lifecycle`: agent-driven archive/unarchive of
workspace-turn peer workspaces

## Problem

An orchestrating agent can create peer workspaces with `task({ kind:
"workspace" })` (workspace turns), but has **no tool to archive them**.
The purpose-built tool `task_workspace_lifecycle` (added in PR coder#3633)
was removed from the executable toolset by PR coder#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_lifecycle` as an executable tool, **trimmed to
`archive` + `unarchive`**, scoped strictly to workspace-turn targets
owned by the calling workspace. Do **not** overload `task_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^`):

- Ownership tracking **already exists**:
`taskHandleStore.isWorkspaceOwnedBy(ownerWorkspaceId, workspaceId)`
(HEAD `src/node/services/taskHandleStore.ts:247`) checks
`createdWorkspace` flags 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.** The `mux.taskOwnerWorkspaceId` /
`mux.taskHandleId` / `mux.taskTurnId` tags stamped on created workspaces
(`src/constants/workspaceTags.ts`) are correlation/recovery/UI hints
only — config metadata is not an authorization surface.
- The Zod schemas survive on HEAD
(`src/common/utils/tools/toolDefinitions.ts` ~1221:
`TaskWorkspaceLifecycle*`), including `unarchive` action +
`unarchived`/`already_unarchived` statuses.
- The transcript renderer survives on HEAD
(`src/browser/features/Tools/WorkspaceLifecycleToolCall.tsx`, registered
in `getToolComponent.ts:227`, icon in `ToolPrimitives.tsx:288`) → **zero
frontend work**.
- The deleted backend (`archiveOwnedWorkspaceTurnWorkspace`,
`resolveOwnedWorkspaceLifecycleTarget`, helpers) and tool file (134
lines) + test file (142 lines) are recoverable via `git show
'88580ca7d^:<path>'`.
- `workspaceService.archive(workspaceId, acknowledgedUntrackedPaths?)`
(HEAD `workspaceService.ts:7237`) already handles lifecycle locking,
init-abort, active-descendant refusal, and the lossy-snapshot
untracked-file confirmation; `workspaceService.unarchive` exists
(exposed via ORPC `workspace.unarchive`, `router.ts:4516`).
- Historical gap: the pre-removal action enum was only
`["archive","delete_worktree","remove"]` — **`unarchive` never had an
implementation**; the schema on HEAD is already extended for it.
`unarchiveOwnedWorkspaceTurnWorkspace` is the only genuinely new backend
code.

### Alternatives considered (rejected)

| Approach | Why rejected | Net LoC |
|---|---|---|
| Extend `task_remove` with an `action`/archive mode | Semantic mismatch
(irreversible vs reversible), result-schema bloat
(`requires_confirmation`, `already_archived`), re-muddies coder#3825's
separation of execution state vs retention | ~+120 |
| New `task_archive` tool name | Requires new renderer + registration
wiring; breaks `issue-triage-loop` skill compat; historical transcripts
diverge from live tool | ~+260 |
| Out-of-band `xum api workspace archive` via bash | Requires reachable
server + auth token inside worktree workspaces (typically absent); no
ownership scoping — any workspace could archive anything; no transcript
card | 0 (unsupported) |
| **Restore trimmed `task_workspace_lifecycle` (chosen)** | Mostly
resurrection; schemas + renderer already on HEAD; skill-compatible |
**~+320 product code** (plus ~200 test) |

## Scope

In scope:
- `archive` and `unarchive` actions for workspace-turn targets (`wst_*`
handle IDs or owned peer `workspaceId`s).
- Ownership enforcement, active-turn handling (`interrupt_active`),
untracked-file confirmation round-trip.
- Tool registration (definitions, factory map, availability lists),
tests, dogfooding.

Out of scope (explicitly not restored):
- `remove` / `delete_worktree` actions — `task_remove` stays the only
irreversible verb; these can be resurrected later by the same recipe.
- Archive of sub-agent (agent-task) children — coder#3825 deliberately
replaced that with the active→inactive→removed lifecycle; non-`wst_`
task IDs stay `invalid_scope`.
- Auto-unarchive-on-send for workspace turns (explicit `unarchive`
instead; `createWorkspaceTurn` mode `"existing"` keeps refusing archived
targets).
- UI changes.

## Implementation

Recovery source for all restored code: `git show '88580ca7d^:<path>'`.

### Phase 1 — Schema & tool definition
(`src/common/utils/tools/toolDefinitions.ts`)

1. **Add a narrowed live-input schema** next to the existing kept
schemas (~line 1249). Do **not** modify
`TaskWorkspaceLifecycleActionSchema` (4 actions) or
`TaskWorkspaceLifecycleToolArgsSchema` — 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`):

   ```ts
   export const TaskWorkspaceLifecycleToolInputSchema = z.object({
action: z.enum(["archive", "unarchive"]).describe(/* reversible
archive/unarchive only */),
targets: z.array(TaskWorkspaceLifecycleTargetSchema).min(1).describe(/*
wst_* taskId or owned workspaceId */),
interrupt_active: z.boolean().nullish().describe(/* archive only */),
acknowledged_untracked_paths: z.record(z.string(),
z.array(z.string())).nullish(),
   }).strict();
   ```

Reuses `TaskWorkspaceLifecycleTargetSchema` (exactly-one-of
taskId/workspaceId superRefine). Drops `force` (only applied to the
un-restored `remove` action). All optional params `.nullish()` per repo
tool-schema convention.

Split rationale: the broad historical args schema +
`TaskWorkspaceLifecycleToolArgs` type (`src/common/types/tools.ts:309`)
stay untouched for historical-transcript parsing and renderer
compatibility; the narrowed input schema is what
`TOOL_DEFINITIONS.task_workspace_lifecycle.schema` advertises to models.
Export a live input type in `types/tools.ts` only if the tool file needs
it — do not repoint the existing type.

2. **Add the `TOOL_DEFINITIONS` entry** (~line 2286, adjacent to
`task_remove`), description rewritten for the trimmed contract:
- reversible archive/unarchive of full workspaces this workspace created
via `task(kind="workspace")`;
- scoped by durable workspace-turn ownership records — cannot act on
arbitrary user workspaces or sub-agent children;
   - active workspace turns are refused unless `interrupt_active: true`;
- archive may return `requires_confirmation` with untracked paths →
re-call with `acknowledged_untracked_paths`;
- archived targets refuse `task(kind="workspace", mode="existing")`
follow-ups until unarchived.

3. **Register availability**: add `"task_workspace_lifecycle"` to the
`baseTools` array in `getAvailableTools` (~line 3349, next to
`task_remove`).

4. **PTC bridging parity** (2 lines): add `"task_workspace_lifecycle"`
to the `BridgeableToolName` union (~3198) and `RESULT_SCHEMAS` (~3228, →
`TaskWorkspaceLifecycleToolResultSchema`) so RLM/PTC sandbox sessions
get the same task-tool surface as `task_remove`.

### Phase 2 — Backend restoration (`src/node/services/taskService.ts`)

No name collisions on HEAD; `coerceNonEmptyString`,
`WORKSPACE_TURN_TASK_TAGS`, `isWorkspaceArchived`,
`TaskWorkspaceLifecycleToolTargetResultSchema` import, and the
`WorkspaceLifecycleResult` type alias (line 213) are already present.

Restore verbatim-then-adapt from
`88580ca7d^:src/node/services/taskService.ts`:

1. `private readonly workspaceLifecycleLocks = new MutexMap<string>()`
(historical line 1200) — a dedicated per-target-workspace lock
**outside** `workspaceService.archive`. Do not wrap the call in
`withTaskTreeLifecycleLock` externally: `archive` already 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.**
2. `resolveOwnedWorkspaceLifecycleTarget(ownerWorkspaceId, action,
target)` (historical 7401) — resolves `wst_*` handle → workspaceId via
`taskHandleStore.getWorkspaceTurn` (HEAD :173), then gates on
`taskHandleStore.isWorkspaceOwnedBy` (HEAD :247; records are never
pruned, so ownership cannot silently expire); non-`wst_` taskIds and
non-owned workspaceIds → `invalid_scope`.
3. `withWorkspaceLifecycleLock` (historical 7388),
`lifecycleTargetFields` (historical ~7454),
`findWorkspaceLifecycleMetadata` (historical ~7473).
4. `handleActiveWorkspaceLifecycleTurns` (historical 7495) — both
callees survive on HEAD: `listWorkspaceTurnTasks` (:8496) and
`interruptWorkspaceTurn` (:8512). Filters owner's turns with status
`queued|starting|running` targeting the resolved workspace; returns
`active` + `activeTaskIds` unless `interruptActive`, in which case it
interrupts each turn.
5. `archiveOwnedWorkspaceTurnWorkspace(ownerWorkspaceId, target,
options)` (historical 7208) — under the lifecycle lock: `not_found`
(metadata absent) / `already_archived` (idempotent) / active-turn
handling / `workspaceService.archive(workspaceId,
acknowledgedUntrackedPaths)`; maps `kind:
"confirm-lossy-untracked-files"` → `requires_confirmation` + `paths`.
`workspaceService.archive` errors surface as `status: "error"` (includes
the active-descendant-sub-agent refusal,
`ACTIVE_DESCENDANT_ARCHIVE_ERROR`).
6. **New** `unarchiveOwnedWorkspaceTurnWorkspace(ownerWorkspaceId,
target)` (~30 lines, mirrors archive): resolve + lock → `not_found` /
`already_unarchived` (via `isWorkspaceArchived`) →
`workspaceService.unarchive(workspaceId)` (HEAD :7514, `Result<void>`) →
`unarchived` | `error`. **`interrupt_active` applies to `archive`
only.** An archived workspace should never have active turns (archive
refuses while active; `createWorkspaceTurn` refuses 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, return `active` — never interrupt on unarchive, even if the caller
passed `interrupt_active: true`. Document this in the tool description.

### Phase 3 — Tool file + registration

1. **Restore `src/node/services/tools/task_workspace_lifecycle.ts`**
(134 lines historical → ~100 trimmed): keep
`normalizeTarget`/`targetKey` dedup and `rejectInvalidWorkspaceTaskId`
(non-`wst_` → `invalid_scope`); dispatch only `archive` →
`archiveOwnedWorkspaceTurnWorkspace` and `unarchive` →
`unarchiveOwnedWorkspaceTurnWorkspace`; keep the `planFileOnly` throw
(same pattern as `task_remove.ts:17`); input schema = the new narrowed
`TaskWorkspaceLifecycleToolInputSchema`; results parsed against the kept
`TaskWorkspaceLifecycleToolResultSchema`.
2. **`src/common/utils/tools/tools.ts`**: import
`createTaskWorkspaceLifecycleTool` (~line 42) and register
`task_workspace_lifecycle:
wrap(createTaskWorkspaceLifecycleTool(config))` (~line 773).
3. **Agent allowlists** (`src/node/builtinAgents/`): add
`task_workspace_lifecycle` to `tools.remove` in `explore.md` (line ~30,
alongside `task_remove`), `plan.md` (line ~23), and `desktop.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) rebuilds `builtInAgentContent.generated.ts`, and `make
typecheck` (part of `static-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.
4. **`src/common/utils/messages/transcriptShare.ts`** (~line 133): add
`"task_workspace_lifecycle"` to `PRESERVE_OUTPUT_TOOLS` so shared
transcripts keep target-status rows.
5. **`src/browser/features/Settings/Sections/TasksSection.agents.ts`**
(~line 62): add to the agent-template remove arrays where `task_remove`
appears, for template parity.
6. **Frontend**: zero changes — `WorkspaceLifecycleToolCall.tsx` already
renders all statuses including `unarchived`/`already_unarchived`
(exhaustive `Record<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 in `tools/testHelpers.ts`):

- Tool layer: forwards owner/target/options to the scoped taskService
API; dedupes duplicate targets; non-`wst_` taskId → `invalid_scope`
without touching taskService; `planFileOnly` throws; unarchive routes to
the unarchive method (replaces the historical delete_worktree/remove
routing test); `acknowledged_untracked_paths` forwarding when the target
is specified by `taskId` (not `workspaceId`) — the tool must pass the
full by-workspaceId map so the backend can apply it after
handle→workspaceId resolution.
- taskService layer (in `taskService.test.ts`, alongside existing
workspace-turn tests):
- non-owned workspaceId → `invalid_scope`; owned (createdWorkspace
record) → proceeds.
- archive idempotency (`already_archived`) and unarchive idempotency
(`already_unarchived`).
- active turn → `active` + `activeTaskIds`; with `interrupt_active:
true` → interrupts then archives.
- `workspaceService.archive` returning `confirm-lossy-untracked-files` →
`requires_confirmation` with paths; second call with
`acknowledged_untracked_paths` → `archived`.
- archive → `createWorkspaceTurn(mode: "existing")` refused; after
unarchive → succeeds again (the round-trip that motivates unarchive).
- unarchive addressed by `wst_*` taskId and by `workspaceId` (both
resolution paths).
- unarchive never interrupts: with a (synthetic) active turn present,
unarchive returns `active` even when `interrupt_active: true`.
- Schema tests (`toolDefinitions.test.ts`): live input schema rejects
`remove`/`delete_worktree` actions and rejects `force` (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 for `task_remove` (follow
precedent, don't invent a new tautology).

### Phase 5 — Validation & dogfooding gate

1. `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.ts` and the renderer UI test
(`WorkspaceLifecycleToolCall.ui.test.tsx`) to confirm no schema drift.
2. Live dogfooding (see next section) before declaring done.

## Dogfooding

Environment: `make dev-server-sandbox` (project skill
`dev-server-sandbox`) — the **web** dev server, not the Electron desktop
app. Isolated temp `XUM_ROOT`, free backend + Vite ports, seeded
`providers.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-*`, backend `127.0.0.1:<port>`, Vite
`127.0.0.1:<port>`), and `agent-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. Seeded `config.json` contains real
projects; dogfood against a scratch project (or launch with
`DEV_SERVER_SANDBOX_ARGS="--clean-projects"` and add one).

agent-browser flow (per `agent-browser` core + `dogfood` skills):
`snapshot -i` for element refs → `click`/`fill` by `@eN` ref →
re-snapshot after page changes; `screenshot <file>.png` for stills;
`record start/stop <file>.webm` for videos (per the
agent-browser/dogfood skill workflow); `errors`/`console` for renderer
errors after each step.

Evidence requirements (per `dogfood` skill: repro-first, evidence per
step): **screenshots per step AND a `record start`/`record stop` webm
video** of the core archive→unarchive flow, paced human-watchably
(`sleep 1` between actions). All evidence attached via `attach_file` for
reviewer verification.

Script (each step evidenced with an `agent-browser` screenshot, attached
via `attach_file`):

1. In the sandbox app, create a parent workspace on a scratch project;
send it a prompt instructing: “create a peer workspace via
`task({kind:"workspace", ...})`, wait for its turn to settle, then call
`task_workspace_lifecycle({action:"archive",
targets:[{workspaceId:...}]})`”.
2. Verify: peer workspace appears in the sidebar → after archive it
disappears from the active sidebar and shows as archived on the project
page. Screenshot both states.
3. Negative check: instruct the agent to archive a workspace it did not
create → expect `invalid_scope` in the tool card. Screenshot.
4. Untracked-file confirmation: create the peer workspace, drop an
untracked file in its worktree (with snapshot archive behavior enabled),
archive → expect `requires_confirmation` card listing paths; re-call
with `acknowledged_untracked_paths` → archived. Screenshot the
confirmation card.
5. Unarchive: instruct `{action:"unarchive"}` → workspace returns to the
sidebar; then a `task(kind="workspace", mode="existing")` follow-up
succeeds. Screenshot.
6. Transcript card sanity: confirm the `WorkspaceLifecycleToolCall` card
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

1. A parent agent can archive a peer workspace it created via
`task(kind="workspace")` using
`task_workspace_lifecycle({action:"archive"})`, and unarchive it with
`{action:"unarchive"}`.
2. Non-owned workspaces, arbitrary user workspaces, and sub-agent
(non-`wst_`) task IDs are refused with `invalid_scope`.
3. Active workspace turns block archive unless `interrupt_active: true`
(which interrupts, then archives); unarchive never interrupts regardless
of `interrupt_active`.
4. Lossy-snapshot archive requires the `requires_confirmation` →
`acknowledged_untracked_paths` round-trip; `force` no longer exists on
the input schema.
5. Archived targets refuse `mode:"existing"` follow-ups; unarchive
restores them.
6. `remove`/`delete_worktree` are not model-invocable through this tool
(schema-rejected); `task_remove` behavior unchanged.
7. Historical transcripts with old `task_workspace_lifecycle` calls
still render (renderer untouched, result schema unchanged).
8. Explore/plan/desktop agents do not receive the tool; plan mode throws
at execute as defense-in-depth.
9. `make static-check` green; all Phase 4 suites green; dogfooding
evidence captured.
10. The `issue-triage-loop` skill's documented call
`task_workspace_lifecycle({action:"archive", targets:[{workspaceId}],
interrupt_active:false})` works verbatim.

## Risks & mitigations

- **Lock layering** (`workspaceLifecycleLocks` wrapping
`workspaceService.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.
- **Schema drift since removal**: the kept result schema gained
`unarchived`/`already_unarchived`; the restored backend emits only
statuses in the kept union, and `toolDefinitions.test.ts` already
validates the schemas — renderer exhaustiveness (`Record<Status, Meta>`)
fails typecheck if a status is added without UI handling.
- **Model confusion with `task_remove`**: mitigated by the description
contrast (reversible vs irreversible) and by `invalid_scope` for
sub-agent IDs; watch Codex review for wording feedback.
- **coder#3825 intent**: the removal rationale was sub-agent retention-class
cleanup; this restoration keeps sub-agents out of scope entirely, so it
does not reintroduce the mixed lifecycle coder#3825 eliminated.

## 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).

</details>

---

_Generated with `xum` • Model: `anthropic:claude-fable-5` • Thinking:
`xhigh` • Cost: `$53.33`_

<!-- mux-attribution: model=anthropic:claude-fable-5 thinking=xhigh
costs=53.33 -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants