🤖 feat: add workspace turn task handles#3600
Conversation
|
@codex review |
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 42d66960cd
ℹ️ 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".
42d6696 to
239e536
Compare
|
Addressed the Codex findings in the latest push:
Validation after the fixes:
|
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 239e536577
ℹ️ 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".
239e536 to
e49aed4
Compare
|
Addressed the second Codex round in the latest push:
Validation after this round:
|
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e49aed4e3c
ℹ️ 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".
e49aed4 to
ad8e1b3
Compare
|
Addressed the third Codex round in the latest push:
Validation after this round:
|
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ad8e1b3787
ℹ️ 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".
ad8e1b3 to
a8f27aa
Compare
|
Addressed the fourth Codex round in the latest push:
Validation after this round:
|
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a8f27aa49e
ℹ️ 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".
a8f27aa to
3fd4b4b
Compare
|
Addressed the fifth Codex round in the latest push:
Validation after this round:
|
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3fd4b4b2fd
ℹ️ 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".
3fd4b4b to
ebddde6
Compare
|
@codex review Addressed the latest finding:
Validation:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0aefdf33dd
ℹ️ 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".
|
@codex review Addressed the latest finding:
Validation:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b2c9458adf
ℹ️ 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".
|
@codex review Addressed the latest findings:
Validation:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 896b61b12a
ℹ️ 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".
|
@codex review Addressed the latest finding:
Validation:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9d72dc2113
ℹ️ 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".
|
@codex review Addressed the startup auto-retry race by treating pending/queued/preparing workspace turns as live during stale-handle sweeps, with a regression test covering |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 945f617180
ℹ️ 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".
|
@codex review Addressed the workflow-result metadata issue by filtering assistant stream metadata to the workspace-turn correlation marker only. Added a startup auto-retry regression test proving workflow-result metadata is not stamped onto the assistant stream while workspace-turn metadata still propagates. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 704b373b9b
ℹ️ 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".
|
@codex review Addressed both latest findings:
Validation: targeted TaskService regressions, startup metadata tests, WorkspaceService pending-turn test, and |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 564c236a1d
ℹ️ 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".
|
@codex review Addressed the deterministic retry-signal finding. AgentSession now exposes a stream-error recovery decision promise that resolves once a recovery retry is visibly preparing or once terminal auto-retry scheduling/abandonment has completed; TaskService awaits that signal before deciding whether a recoverable workspace-turn error is still live or should settle. Validation: targeted TaskService regressions, targeted AgentSession startup metadata regressions, WorkspaceService pending-turn regression, and |
|
Codex Review: Didn't find any major issues. Swish! 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". |
## Summary Long-lived auto-cleanup PR maintained by the **Auto-Cleanup Agent**. Each pass lands at most one extremely low-risk, behavior-preserving cleanup drawn from recently merged `main` activity, then advances the checkpoint below. The prior long-lived PR (coder#3559) merged into `main`; this is its successor. Auto-cleanup checkpoint: 2af29df ## This pass Considered the one new `main` commit since the previous checkpoint — coder#3603 (`feat: add built-in loop skill`). That change adds the advertised `loop` built-in skill (`src/node/builtinSkills/loop.md`), regenerates `builtInSkillContent.generated.ts` (generated — off-limits), and extends the agent-skills discovery test expectations. The skill-content/generated surface offers no safe hand-edit, so the cleanup was drawn from the production sibling of that area: the skill-discovery service `agentSkillsService.ts`. Landed one behavior-preserving cleanup in `agentSkillsService.ts`: `readSkillDescriptorFromDir` open-coded the same invalid-skill diagnostic push — `options?.invalidSkills?.push({ directoryName, scope, displayPath: skillFilePath, message, hint })` — at six failure sites (missing/unreadable SKILL.md, SKILL.md-is-a-directory, oversized file, read failure, invalid descriptor, parse failure). Those six sites share identical identity fields (`directoryName` / `scope` / `displayPath`) and differ only in `message` + `hint`. All six now delegate to a single function-local `pushInvalidSkill(message, hint)` closure that captures the function-constant identity fields. Behavior-preserving: the pushed object shape and every per-site `message`/`hint` are unchanged, and the closure short-circuits on `options?.invalidSkills` exactly as the inline calls did. ## Branch contents (net diff against `main`) <details> <summary>Cleanups carried by this PR (not yet merged)</summary> - **`agentSkillsService.ts` — dedupe invalid-skill diagnostics.** coder#3603's new built-in skill lives in the agent-skills discovery area; `readSkillDescriptorFromDir` open-coded the same `options?.invalidSkills?.push({ directoryName, scope, displayPath: skillFilePath, message, hint })` at six failure sites that share identical identity fields and differ only in `message`/`hint`. All six now delegate to a single function-local `pushInvalidSkill(message, hint)` closure. Behavior-preserving (identical pushed shape, messages, hints, and `options?.invalidSkills` short-circuit). - **`workspaceGoalService.ts` — dedupe session file path resolution.** The three private accessors `getFilePath` / `getHistoryFilePath` / `getBoardFilePath` each open-coded the same `assert(...non-empty workspaceId)` guard and `path.join(getSessionDir(...), <FILE_CONST>)`. All three now delegate to a single `resolveSessionFilePath(workspaceId, fileName)` helper. Behavior-preserving (identical guard + join; accessor names and callers unchanged). - **`task.ts` — dedupe plan-agent explore-only error.** The plan-agent restriction error `In the plan agent you may only spawn agentId: "explore" tasks.` was open-coded twice (the workspace-turn guard and the per-launch agent-id guard). Both now reference a single module-level `PLAN_AGENT_EXPLORE_ONLY_ERROR` constant. Behavior-preserving (identical string output). - **`task_await.ts` — dedupe workspace-turn error fallback.** coder#3600's new workspace-turn await paths open-coded the fallback error message `"Workspace turn failed"` three times (`<record>.error ?? "Workspace turn failed"`). All three now reference a single module-level `WORKSPACE_TURN_DEFAULT_ERROR` constant. Behavior-preserving (identical string output). - **`goalToolUtils.tsx` / `SetGoalToolCall.tsx` — dedupe turn-count pluralization.** coder#3595's new goal toolcards open-coded `` `${n} turn${n === 1 ? "" : "s"}` `` in both `formatGoalTurns` and the set_goal toolcard's `formatOptionalTurnCap`. Both now call a shared `pluralizeTurns(count)` helper. Behavior-preserving (identical string output). - **`heartbeat.ts` — dedupe heartbeat tool success result construction.** coder#3596's new `heartbeat` tool open-coded the same `{ success: true, action, configured, settings, summary }` success object three times (`get` / `set` / `unset`), each re-calling `summarize({ action, settings })`. All three now route through a single `buildSuccessResult(action, settings)` helper that derives `configured` from `settings`. Behavior-preserving: the per-branch `configured` values (`settings != null`, `false`, `true`) all equal `settings != null` given the non-null `Ok` payload type for `set`. - **`taskService.ts` — dedupe startup recovery candidate listing.** coder#3594's `TaskService.initialize` open-coded the `awaiting_report` / `running` candidate filters twice (initial computation and post-interrupt refresh). Both call sites now reuse a single `listStartupRecoveryCandidates(sourceConfig)` closure returning `{ awaitingReportTasks, runningTasks }`, matching the existing `listQueuedTasks` pattern. Behavior-preserving. - **`ChatPane.tsx` — dedupe send-queued-immediately in-flight guard clear.** coder#3592 introduced `sendQueuedImmediatelyInFlightRef` to block duplicate send-now attempts; releasing that guard ("clear it only if it still points at this attempt") was open-coded twice — once after a failed interrupt result and once in the `catch` arm. Both call sites now invoke a single callback-local closure `clearInFlightGuardIfCurrent()`. Behavior-preserving: the failed-result site's combined `!interruptResult.success && ref === id` becomes `if (!interruptResult.success) clearInFlightGuardIfCurrent()` (logically identical); the `catch` site is a verbatim substitution. - **`WorkflowRunner.ts` — dedupe `parallelAgents` child-failure abort handling** into a single batch-local closure `applyChildFailureToBatch(error)`, shared by the per-run task closure and the bulk-create path. - **`ChatInput/index.tsx` — dedupe edit-mode attachment toast** string `"Attachments cannot be added while editing a message."` into a single module-level constant `EDIT_MODE_ATTACHMENT_ERROR_MESSAGE`, replacing three inline copies across the add-attachment / paste / drop guards. </details> <details> <summary>Prior passes (landed via coder#3559, merged)</summary> - Replaced an inline terminal-status check in `WorkflowService.interruptRunOnAbort` with the `isTerminalWorkflowRunStatus` helper. - Deduplicated the `workspaceId`-only metadata JSON Schema repeated by three id-targeted host actions in `workspaceHostActions.ts` into `WORKSPACE_ID_ONLY_INPUT_SCHEMA`. - Extracted a shared `BlockedBadge` component in `WorkflowDefinitionToolCall.tsx`. - Deduplicated the repeated `argsSchema.properties must be an object` error string in `workflowArgs.ts`. - Deduplicated mocked-module specifier strings in `useModelsFromSettings.test.ts`. - Removed a redundant `toggleWorkflowExpanded` wrapper in `WorkflowRunToolCall.tsx`. - Deduplicated `SAVE_AUTOMATION_ERROR_MESSAGE` / `REMOVE_AUTOMATION_ERROR_MESSAGE` in `AutomationModal.tsx`. - Replaced a duplicated `"wfr_"` literal in `WorkflowRunner.ts` with `isWorkflowRunTaskId()`. </details> ## Validation - `make static-check` — ESLint, both `tsgo` typecheck configs, and Prettier all pass on the rebased tree. The only failing step is `fmt-shell-check`, which requires `shfmt` (not installed in this environment); no shell scripts were touched, so CI (which has `shfmt`) is unaffected. - `bun test src/node/services/agentSkills/agentSkillsService.test.ts` — 20 pass, 0 fail (covers skill discovery + the invalid-skill diagnostics path this pass deduped); `builtInLoopSkill.test.ts` — 1 pass. --- _Generated with `mux` • Model: `anthropic:claude-opus-4-8` • Thinking: `xhigh`_ <!-- mux-attribution: model=anthropic:claude-opus-4-8 thinking=xhigh --> --------- Co-authored-by: mux-bot[bot] <264182336+mux-bot[bot]@users.noreply.github.com>
Summary
Adds full-workspace task handles behind
task(kind="workspace"), letting a parent turn launch a normal workspace turn, await its final assistant message, list it, interrupt it non-destructively, and reprompt the same owner-created workspace with a fresh handle.Background
The existing
tasktool was tied to sub-agent workspaces andagent_reportlifecycle semantics. Full workspace turns need durable await/interrupt handles without setting sub-agent fields or making those workspaces disposable descendants by default.Implementation
TaskHandleStoreforworkspace_turnrecords under the owner session.task,task_await,task_list, andtask_terminateschemas/handlers forwst_...handles.TaskService.createWorkspaceTurn, stream-end/error finalization, foreground waiter hardening, owner-scoped existing-workspace reprompts, and non-destructive interruption.muxMetadatathroughAIService.streamMessage()so final stream events correlate back to the handle.Validation
workflow_run simplify --base origin/main --fixapplied a hardening commit before squashing.bun test src/common/utils/tools/toolDefinitions.test.ts src/node/services/taskHandleStore.test.ts src/node/services/tools/task.test.ts src/node/services/tools/task_await.test.ts src/node/services/tools/task_list.test.ts src/node/services/tools/task_terminate.test.ts src/node/services/taskService.test.ts src/node/services/aiService.test.ts --test-name-pattern 'workspace task|workspace-turn|muxMetadata|routeProvider|task_await|task_list|task_terminate|TaskHandleStore'.bun test src/node/services/aiService.test.ts --test-name-pattern 'muxMetadata|routeProvider'.MUX_ESLINT_CONCURRENCY=1 make static-check.Dogfooding
task(kind="workspace")end-to-end.wst_24dfb4ba9efor workspace73594a39a5;task_awaitcompleted with final textfixed workspace turn completed.73594a39a5, returnedwst_fc14f6a93a, and completed with final textfixed workspace turn reprompt completed./home/coder/.mux-tmp/workspace-turn-dogfood-fixed/.Risks
This touches core task orchestration and stream settlement. Regression risk is moderate around task waits and workspace lifecycle, mitigated by focused tests for schema validation, owner scoping, stream completion/error handling, foreground wait backgrounding, disposable cleanup, and tool handler behavior.
Pains
Dogfooding exposed a correlation gap:
muxMetadatawas persisted on the user message but not propagated into initial stream metadata, so final stream events could miss handle correlation. This PR fixes propagation and keeps an active-handle fallback for resilience.📋 Implementation Plan
Implementation Plan: Generalized Task Handles for Full Workspace Turns
Goal
Make the
taskabstraction capable of launching and managing more than sub-agent workspaces. A caller should be able to start a normal full workspace turn, receive a stable task handle, latertask_awaitthat handle to get the final assistant turn-complete message, interrupt it through task tooling, and later reprompt a workspace it created earlier by passing that existing workspace ID back through the task abstraction. This must work without conflating full workspaces with sub-agentagent_reportlifecycle semantics.Evidence and current-state facts
Verified through direct repo inspection and Explore reports:
src/node/services/tools/task.tscurrently parsesagentId/subagent_type, then callsTaskService.create({ kind: "agent", ... })for each single,n, orvariantslaunch.src/node/services/taskService.tscurrently definesTaskKind = "agent";TaskService.create()persists a child workspace entry withparentWorkspaceId,taskStatus,taskPrompt,agentId/agentType, and starts it viaworkspaceService.sendMessage().agent_report:TaskService.handleStreamEnd()looks foragent_report/ implicit report,finalizeAgentTaskReport()persists subagent report artifacts in ancestor session dirs, resolves waiters, and may clean up the child workspace.src/node/services/tools/task_await.tsalready multiplexes several stable handle families: descendant agent task IDs,bash:background processes, andwfr_...workflow runs.src/node/services/tools/task_terminate.tsalready dispatches by ID kind: workflow runs are interrupted, bash tasks are terminated through the background process manager, and agent task IDs callterminateDescendantAgentTask().stream-start/stream-endthroughAIService;StreamEndEventincludesworkspaceId,messageId, finalparts, and metadata insrc/common/orpc/schemas/stream.ts.WorkspaceService.create()accepts creation-timetags, which gives an atomic way to tag created workspaces for task-handle recovery.Recommendation
Implement a durable workspace-turn task handle as a sibling of current sub-agent task IDs, not as a special case of sub-agent
parentWorkspaceId/taskStatus.Hard invariant: workspaces created for
workspace_turnhandles remain normal workspaces. They must not setparentWorkspaceId,taskStatus,agent_reportlifecycle fields, sub-agent patch metadata, or cleanup-on-report behavior. The handle record owns await/stop state; the workspace remains user-visible and inspectable.The key design rule: a workspace task handle identifies one assistant turn in one workspace, not the workspace forever. A full workspace can accumulate multiple turn handles over time when the owner reprompts it. Return both for each turn:
{ "taskId": "wst_abc123", "workspaceId": "ws_789", "status": "running" }taskId/ handle ID: stable await/stop target.workspaceId: the normal full workspace the user can inspect and continue using.Approach comparison and LoC estimates
Recommended approach: durable
workspace_turnhandlesNet product LoC estimate: ~950–1,450 LoC.
Includes schemas, handle store, owner-scoped existing-workspace targeting, stream correlation, task tool/task_await/task_list/task_terminate integration, unit tests, and dogfood helpers. This is more code than the minimal route, but keeps sub-agent and full-workspace semantics cleanly separated.
Not recommended: reuse sub-agent workspace fields with
taskKind: "turn"Net product LoC estimate: ~350–600 LoC.
This would extend
TaskKindto"agent" | "turn", persist a child workspace withparentWorkspaceId, suppressagent_report, and finalize on stream-end. It is tempting because it reuseswaitForAgentReport(), but it overloads fields whose existing meaning is “descendant sub-agent that owesagent_reportand can be cleaned up.” It risks accidental auto-recovery prompts, parent auto-resume coupling, patch artifact assumptions, and destructivetask_terminatebehavior for what should be a durable full workspace.Data model
Add a workspace-turn task handle record
Create a small durable store, likely under the caller workspace session dir:
src/node/services/taskHandleStore.ts(orsrc/node/services/workspaceTaskHandleStore.ts).~/.mux/sessions/<ownerWorkspaceId>/task-handles/<handleId>.json.Proposed record:
Do not create records for current sub-agent tasks immediately unless useful for uniform lookup. Existing agent task IDs can remain backed by config/session report artifacts, and
task_awaitcan resolve them via current paths.Ownership for reprompting existing workspaces
A workspace-turn handle is per-turn, but the owner also needs durable permission to target the same full workspace again. Use an owner-scoped check rather than
parentWorkspaceId:tags["mux.taskOwnerWorkspaceId"] === ownerWorkspaceId, orworkspace_turnrecord withcreatedWorkspace: trueand the sameworkspaceId.WorkspaceTurnTaskHandleRecordwith a newhandleIdandturnId,createdWorkspace: false, and the sameworkspaceId.workspace.mode: "existing"is accepted only for workspaces owned/created by the caller workspace under the rules above. Foreign workspaces returninvalid_scope/ schema-level tool error without leaking transcript content.Ownership tag security and provenance
Treat workspace ownership tags as internal capability/recovery metadata, not as freely trusted user metadata.
mux.taskOwnerWorkspaceId,mux.taskHandleId, andmux.taskTurnIdare minted only byTaskService.createWorkspaceTurn()when it intentionally creates an owned workspace.mux.task*ownership tags into unrelated workspaces. If a workspace is forked outside the workspace-turn creation path, strip these tags or ignore them for ownership.mux.task*namespace or makeTaskHandleStore.isWorkspaceOwnedBy()ignore user-mutated task tags.TaskHandleStore.isWorkspaceOwnedBy()should prefer owner handle records. Tag-only ownership is a recovery path: log the recovery, verify the tagged workspace still exists, and materialize a recovered ownership record before allowing reprompt.Workspace tags for recovery
When
task(kind: "workspace", workspace: { mode: "new" | "fork" })creates a full workspace, create it with tags:For later existing-workspace reprompts, keep the original owner tag stable. Do not overwrite it per turn; each new prompt gets its own handle record.
This lets startup/recovery code rediscover created workspaces even if handle persistence races with workspace creation. Do not rely only on tags for await results; the handle record remains source-of-truth for status and final message.
Reconciliation rules:
sendMessagefails: mark the handleerrorand keep/delete the workspace according tocreatedWorkspace+ rollback policy.task_await(timeout_secs: 0).Public tool contract
taskinput schemaKeep existing sub-agent behavior as the default. Add a discriminant that does not break old calls:
For
kind: "subagent":agentId/subagent_type,n,variants,model,thinking,isolation, foreground/background behavior.For
kind: "workspace", support creating a new full workspace and reprompting owner-created existing workspaces:Recommended MVP defaults:
workspace.modedefaults to"new"/ fork-from-current-project behavior.workspace.mode: "existing"requiresworkspace.workspaceIdand only targets a workspace previously created/owned by the caller workspace.WorkspaceService.sendMessage(..., { requireIdle: true, startStreamInBackground: true, ... })so the handle maps to one immediate turn instead of an ambiguous queued future message.disposable: falseby default so a “full workspace task” remains inspectable after interruption/completion. For existing-workspace reprompts, forcecreatedWorkspace: falseanddisposableWorkspace: falseregardless of the input.n/variantsfor workspace tasks in the first PR. Ifkind === "workspace"andnorvariantsare present, return a clear schema/refinement error. Workspace fanout can be a follow-up after handle semantics are stable.taskresult schemaExtend queued/running/completed schemas to allow workspace fields:
Avoid duplicating the same final-message content in tool output. For workspace-turn results,
reportMarkdownis the default model-facing text projection of the final assistant message.finalMessageRefcarries durable identity plus compact bounded metadata only; it does not include rawCompletedMessagePart[]by default. If no final text exists, use a short non-empty fallback such as “Workspace turn completed without final text output.”task_awaitresult schemaAdd optional fields to completed results while preserving current completed shape for sub-agent, bash, and workflow results:
MVP context-budget rule: a workspace-turn await result must not include both full
reportMarkdowntext and rawCompletedMessagePart[]. The default response returnsreportMarkdown+ compactfinalMessageRef. A future explicit detail mode may fetch raw parts, but when it does, it must suppress or truncatereportMarkdownso there is only one substantive copy of the final message in context.Stop/terminate contract
MVP decision: extend
task_terminateforworkspace_turnhandles with workflow-like interruption semantics, not destructive deletion.workflowruns remain interrupted/resumable.workspace_turncallsaiService.stopStream(workspaceId, { abandonPartial: false }), marks the handleinterrupted, wakes waiters, and returnsstatus: "interrupted"with a note that the full workspace is preserved.task_terminatemay interrupt/preserve durable work for workflow and workspace-turn handles instead of discarding it.task_stopas a follow-up only if product wants clearer vocabulary; do not block the MVP on a new tool.Internal architecture
Add a resolver layer for awaitable handles
Introduce a small resolver module used by
task_await,task_list,task_terminate, and futuretask_stop:Resolution rules:
bash:stays bash.wfr_stays workflow run.wst_or a persisted record match resolves workspace-turn handle.Scope rules:
record.ownerWorkspaceId === callerWorkspaceIdor when the owner is an ancestor if nested ownership is intentionally supported. For MVP, require exact owner match unless there is a strong reason to support descendants.TaskHandleStore.isWorkspaceOwnedBy(callerWorkspaceId, workspaceId)succeeds via tags or priorcreatedWorkspace: truerecords.invalid_scopeinstead of leaking existence/details for foreign handles or foreign workspaces.Turn-completion correlation
The handle must correlate with one specific assistant turn. Do not guess “the next stream-end in that workspace,” and do not assume every low-level
stream-endevent is semantically “turn complete” until verified.Preferred implementation:
handleId + turnIdbefore callingsendMessage().taskHandleId,turnId, and the resultingmessageIdonce known. Use typed internal options or typedmuxMetadata; avoid untyped ad-hoc blobs.stream-endfires only after the final assistant message is durable, it can be used. If it can fire at intermediate tool-step boundaries, add a new event fromAgentSessionafter the final assistant message is committed.StreamEndEvent.partsas an optimization. The durable source of truth is the committed child workspace chat message identified bymessageId; use history lookup bymessageIdwhen needed.workspaceId + turnId/taskHandleIdso stream errors and aborts can mark the correct handle even if terminal error events do not carry full metadata.Fallback if explicit metadata plumbing is too large: capture the correlated
stream-startevent and persistmessageId, guarded by a per-workspace launch lock and timeout. This is more race-prone and should be avoided for the first serious implementation.Workspace-turn lifecycle
Create a new method, e.g.
TaskService.createWorkspaceTurn(args):parseTaskAiOverridesbehavior.handleIdwith a stable prefix such aswst_plusconfig.generateStableId().turnId.workspace.mode: "new" | "fork", create a normal workspace throughWorkspaceService.create()using current project/runtime/trunk defaults, or a focused helper if fork-from-current-workspace is required.workspace.mode: "existing", validate thatworkspaceIdexists, is owner-scoped to the caller, and is currently idle.starting/runningbefore starting the stream. For existing-workspace reprompts, setcreatedWorkspace: falseand preserve the existing workspace tags/config.workspaceService.sendMessage(workspaceId, prompt, options, { startStreamInBackground: true, requireIdle: true, agentInitiated: true, ...correlation }).messageIdonce available and return running handle metadata immediately for background mode; for foreground mode, call the same await path astask_await.errorand surface the error.completed.errororinterrupted, wake waiters.Awaiting workspace-turn handles
Add
TaskService.waitForWorkspaceTurn(handleId, options)or implement directly in aWorkspaceTurnTaskAdapter.Behavior:
timeout_secs === 0, return current snapshot without waiting.handleIdand resolve on handle finalization.running/startingstatus with elapsed time.runningbut no stream is active, reconcile:turnId/messageIdcan be found in history, markcompleted.runningand optionally return a note that the workspace turn may need manual resume.error.Parent delivery semantics
MVP decision: do not auto-resume the parent for background workspace-turn completion.
taskcall ortask_awaitis waiting, return the result directly.task_listandtask_awaitby ID; the original task result already gave the handle.<mux_subagent_report>because trusted-subagent semantics are not identical.<mux_workspace_turn_result>can be a follow-up if dogfooding shows discoverability is poor, but it should not be part of the MVP unless product explicitly wants parent auto-handoff.Concurrency policy
MVP decision: workspace-turn handles count against the same global task parallelism budget used for agent tasks (
maxParallelAgentTasks) unless implementation uncovers a strong reason for a separate setting.Implementation notes:
startingandrunning.File-by-file implementation plan
Phase 1 — Types, schemas, and store
Files:
src/common/utils/tools/toolDefinitions.tssrc/common/types/stream.tsor existing message/stream type exports if neededsrc/node/services/taskHandleStore.ts(new)src/node/services/taskService.test.tsSteps:
kind: "subagent" | "workspace"while preserving old task args.TaskHandleStorewith:upsert(record)get(ownerWorkspaceId, handleId)list(ownerWorkspaceId, statuses?)update(ownerWorkspaceId, handleId, mutator)isWorkspaceOwnedBy(ownerWorkspaceId, workspaceId)for existing-workspace reprompt scope checkslistWorkspaceTurns(ownerWorkspaceId, workspaceId)for history/debugging of repeated promptsQuality gate:
make typecheckif types are touched broadly.Phase 2 — Workspace-turn creation
Files:
src/node/services/tools/task.tssrc/node/services/taskService.tssrc/node/services/workspaceService.tsif a creation helper is neededsrc/node/services/tools/task.test.tssrc/node/services/taskService.test.tsSteps:
Split
createTaskToolexecution into sub-agent and workspace branches after validation.Add
TaskService.createWorkspaceTurn()instead of overloadingTaskService.create(kind: "agent").In
createWorkspaceTurn():mode: "new" | "fork": normal new/forked workspace from the parent project.mode: "existing", validate ownership withTaskHandleStore.isWorkspaceOwnedBy()and reject busy targets with a clear idle-required error.requireIdle: true.Foreground
run_in_background: falseuses the same wait primitive with a reasonable timeout/backgrounding behavior matching current task tool semantics.Return
workspaceIdin all workspace task results.Return
invalid_scope/ clear tool errors for attempts to reprompt arbitrary workspaces that were not created by the caller workspace.Quality gate:
task.test.tsfor tool arg dispatch.taskService.test.tsfor create path and failure rollback.Phase 3 — Turn-completion correlation and finalization
Files:
src/node/services/workspaceService.tssrc/node/services/agentSession.tssrc/node/services/aiService.tsif stream metadata originates theresrc/common/orpc/schemas/stream.tsif event schema needs explicit fieldssrc/node/services/taskService.tssrc/node/services/taskService.test.tsSteps:
taskHandleId,turnId) either as explicit internal options or as typedmuxMetadata.stream-startand terminal events preserve enough correlation to match the handle; ifstream-endis not equivalent to assistant-message-committed, add a dedicated committed/turn-complete event fromAgentSession.parentWorkspaceIdchecks:workspaceId + turnId/taskHandleId.messageIdas source of truth; use eventpartsonly as a fast path.reportMarkdownby concatenating text parts or using a non-empty fallback.finalMessagemetadata and bounded parts/message reference.completedand resolve waiters.aborted/ user stop =>interrupted.errorwith message.runninghandles, at least enough fortask_await(timeout_secs: 0)to report a useful status and not hang.Quality gate:
Phase 4 — Await/list/stop/terminate integration
Files:
src/node/services/tools/task_await.tssrc/node/services/tools/task_list.tssrc/node/services/tools/task_terminate.tssrc/node/services/tools/task_stop.tsif a separate stop tool is later desiredsrc/common/utils/tools/toolDefinitions.tssrc/node/services/tools/Steps:
task_await:task_idscan includewst_...handles,reportMarkdown,workspaceId,messageId,finalMessage, elapsed time, and refetch note.task_listto show active/completed workspace-turn handles withhandleKind,workspaceId, status, title, and createdAt.task_terminateforworkspace_turn:interruptedwith a note that the workspace remains available.task_terminatedocs to describe kind-specific semantics.Quality gate:
task_await.test.ts,task_list.test.ts, andtask_terminate.test.ts. Addtask_stop.test.tsonly in a follow-up that adds the separate tool.Phase 5 — UX polish and discoverability
Files:
src/node/services/taskService.tssrc/browser/features/Tools/TaskToolCall.tsxsrc/browser/features/Tools/TaskToolCall.test.tsxsrc/browser/features/Tools/TaskToolCall.stories.tsxSteps:
workspaceId,messageId, andhandleKindin task tool cards.task_listoutput is clear enough that background workspace-turn handles are discoverable without parent auto-resume.<mux_subagent_report>.Quality gate:
Testing strategy
Unit and integration tests
Add/extend:
src/common/utils/tools/toolDefinitions.test.tsor existing schema tests:agentId,n/variantsrejected for workspace kind in MVP,workspaceId/finalMessage.src/node/services/taskHandleStore.test.ts:isWorkspaceOwnedBy()succeeds for caller-created workspaces and fails for foreign workspaces,mux.task*tags alone do not grant ownership to another workspace.src/node/services/tools/task.test.ts:createWorkspaceTurn,workspace.mode: "existing"passesworkspaceIdthrough for owner-created targets,src/node/services/taskService.test.ts:workspaceId, newturnId, andcreatedWorkspace: false,src/node/services/tools/task_await.test.ts:reportMarkdownand rawCompletedMessagePart[],wst_...handles,timeout_secs: 0snapshot,src/node/services/tools/task_terminate.test.ts:src/browser/features/Tools/TaskToolCall.test.tsxif result rendering changes.Regression tests for existing behavior
Explicitly keep or add tests proving:
parentWorkspaceId,taskStatus,agent_reportrecovery fields, or subagent patch metadata,agent_reportrecovery prompt is sent for a workspace-turn handle,messageId,taskcalls withoutkindstill requireagentIdand use sub-agent flow,agent_report,task_awaitis unchanged,mux.task*ownership tags,task_terminateremains destructive for current sub-agent task IDs.Dogfooding plan
Use an isolated Mux root/workspace so the run is reproducible and safe.
Setup
Scenario A — background full workspace task
taskIdwithwst_prefix andworkspaceId.task_await([taskId])from parent.workspaceId,messageId, compactfinalMessageRef, andreportMarkdownwithout also duplicating rawCompletedMessagePart[].Scenario B — reprompt the created workspace
workspaceIdfrom Scenario A and invoketask(kind: "workspace", workspace: { mode: "existing", workspaceId }, ...)from the original parent workspace.taskId/turnIdbut the sameworkspaceId.workspaceIdfrom an unrelated workspace and verifyinvalid_scope/ clear rejection without transcript leakage.Scenario C — interruption
task_terminate([taskId])from the parent (ortask_stoponly if a follow-up adds it).task_await(timeout_secs: 0)returnsinterrupted,agent_reportrecovery prompt appears.Scenario D — restart recovery
task_await([taskId], timeout_secs: 0).Scenario E — scope guard
task_await([workspaceTurnTaskId]).invalid_scopewithout leaking final message content.Validation commands
During implementation, run targeted tests after each phase, then final validation:
If local resource constraints hit ESLint worker OOM, use the known repo workaround:
Acceptance criteria
task(kind: "workspace", ...)and receive a stabletaskIdplus a normalworkspaceId.workspaceIdback withworkspace.mode: "existing"and get a new stable turn handle for the same full workspace.invalid_scope/ clear tool error.agent_reportcompletion semantics.task_await([taskId])returns the final assistant turn-complete message as a single substantive payload: defaultreportMarkdowntext plusmessageId/compactfinalMessageRefmetadata, without also duplicating rawCompletedMessagePart[]in the same output.task_await(timeout_secs: 0)after context compaction or app restart.<mux_subagent_report>.Key risks and mitigations
taskHandleId/turnIdthrough internal send metadata; avoid “next stream-end” inference.workspace_turnkind; do not setparentWorkspaceIdjust to reuseagent_reportcode.reportMarkdown+ compactfinalMessageRefby default, not full text plus raw parts; any future raw-parts mode must suppress/truncate the text projection.reportMarkdown.task_terminateinterrupts/preserves workspace-turn workspaces by default; separatetask_stopis follow-up vocabulary only.requireIdle: true; queued existing-workspace turns can be a follow-up with explicit queue correlation.mux.task*tags as reserved internal recovery metadata; prefer handle records, strip/ignore tags on generic copies, and test forged tags.invalid_scopefor foreign handles.n/variantsfor workspace tasks.Rollout strategy
mode: "existing"policies if product wants cross-workspace delegation,messageId,Generated with
mux• Model:openai:gpt-5.5• Thinking:xhigh• Cost:$93.61