# feat\(server\): delegate_task to a linked environment · PR \#16719 · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/pull/16719)

Repository: [pingdotgg/t3code](https://git.cafe/pingdotgg/t3code)

Visibility: public

State: open

Source repository: pingdotgg/t3code

Source branch: t3code/peer/remote-delegate

Target branch: t3code/peer/links-ui

Head commit: 70a2a7bdd25d1c3729a864517727914fc3b01d2b

[Discussion and reviews](https://git.cafe/pingdotgg/t3code/pull/16719)

[Commits](https://git.cafe/pingdotgg/t3code/pull/16719/commits?format=markdown)

[Files changed](https://git.cafe/pingdotgg/t3code/pull/16719/diffs?format=markdown)

## Description

---

Part of cross-environment orchestration. An agent can now delegate a task to a linked environment and get woken by its result, the same way a local subagent wakes it.

**How it works**
- **The call.** `delegate_task` takes `target.environmentId` and, optionally, `target.projectId`. Without a project, it picks the project there with the same repository `canonicalKey` as this thread's. Both sides resolve the identity with `RepositoryIdentityResolver`, because a project's cached identity is blank until enrichment runs. If none or several match, it fails with `invalid_request` and lists the candidates.
- **Launch there.** The child is an ordinary thread on the other side. `RemoteDelegation` launches it through `PeerForwarding`, and uses a key derived from `clientRequestId` and the caller's namespace, so a retry hits the same thread there via #16654.
  - The provider and model inherit from the parent, as for a local task. A different provider there needs `target.model`.
  - Modes the caller asks for are checked against the parent. Omitted modes are left to the other side, which inherits the parent's modes capped by the link's access. A narrower link (say auto-accept-edits under a full-access parent) still takes the task instead of refusing it.
- **Record here.** A new internal command, `delegated_task.remote.request`, records the task on the parent: node, subagent row and turn item, with `remoteChild: {environmentId, threadId, label}` instead of a local child thread. `childThreadId` is null for these tasks; it becomes nullable in the MCP result, and every reader handles that.
- **Completion.** A follower waits on the remote thread (in ≤50 s pieces, per #16684), reads its last reply, and dispatches `delegated_task.remote.complete`. That command reuses `planDelegatedCompletionDelivery` and `offerDelegatedCompletionDeliveries`, so the parent's live run claims the result and wakes exactly as it does for a local child. A second report is refused, so the first result stands.
- **Failure handling.** A revoked or expired link fails the task with the reason. Anything else, such as the other machine asleep or a dropped route, backs off up to 5 minutes and tries again.
- **Restart.** At startup the follower re-follows every open remote task (`ProjectionStore.getOpenRemoteDelegatedTasks`). Restart recovery (`ProviderRuntimeRecoveryService`) no longer cancels a remote task as abandoned provider work.
- **Task tools.**
  - `task_status`: answers from the row.
  - `mode=wait`: wakes on the parent's `subagent.updated`.
  - `task_cancel`: interrupts the thread there, then completes the task as cancelled.
- **Lineage across environments.** The launch carries `delegatedFrom: {environmentId, threadId, title}`. Like `linkOrigin`, only the server keeps it: `thread.create` drops it unless the launch is stamped with a link origin, and a client cannot set it.
  - **Parent (web).** The inline task card and a Lineage row in the thread panel say "on box". They open the thread there when this client is connected to that environment, and are plain labels otherwise.
  - **Child (web).** The timeline opens with "Subagent of <parent> on <environment>", with "Open parent thread" when that environment is connected. The parent's live title is used once the client sees it.
  - **Mobile.** Subagent rows say "on box" and open the remote thread when the environment is known. The thread header reads "Subagent of <parent>".
  - **Thread lists.** A client connected to both machines lists the child under its parent, as it does a local subagent, not as a thread of its own. The web sidebar and its project ordering, mobile's thread lists and notifications leave it out. When the child, or one of its own subagents there, asks for approval or input, the parent here is flagged and shows the "Subagent needs …" banner. A child whose parent the client can't list (that machine switched off, or the parent gone) stays listed on its own, so its work is never out of reach.
- **Docs.** `docs/user/remote-access.md` and `docs/internals/remote.md` get a short note each.

**Verification**
- `peer/RemoteDelegation.test.ts` runs the laptop's real orchestrator (replay harness, SQLite), real toolkit and `RemoteDelegation` against the box's real `/mcp` behind real OAuth, on a real socket. The box's thread store is modelled, and the test finishes its thread. Three tests:
  - **Delegate and complete.** Delegating launches in the box project with the matching repository. A retry with the same key launches and records nothing new. When the box thread finishes, the task completes with its reply, the parent's live run claims it (`delegatedCompletion.delivery.taskIds`), the parent is offered a wake, and `task_status` reports the result.
  - **Restart.** The laptop delegates and is torn down. The task finishes on the box meanwhile. A new laptop on the same database starts the follower, which completes the task.
  - **Cancel and revoke.** `task_cancel` interrupts the box thread and completes the task as cancelled. A second task fails with "no longer accepts this link" once the box revokes the link.
- `orchestration-v2/RemoteDelegatedTask.test.ts` covers the deciders directly:
  - A replayed request records one task, with no local thread.
  - The task is open until it completes, then it isn't.
  - Completion claims the delivery and offers one wake.
  - A late second report is refused.
- Mutation-checked: each of these fails its test when removed:
  - the parent wake after completion;
  - the delivery plan (writing the bare task);
  - first-result-wins;
  - re-following at startup;
  - the open-task query's result filter;
  - failing on revocation (it would retry forever);
  - cancel's interrupt there;
  - the retry key;
  - the remote turn item.
- Also ran all of `mcp`, `peer` and `cli`, plus `ProviderRuntimeRecoveryService`, `DelegatedCompletionDelivery`, `ThreadManagementService`, `ProviderTurnControlService`, `ProjectionStore` and `Orchestrator`: 49 files, 491 tests. Contracts `orchestratorMcp` and `orchestrationV2`: 40 tests. All passed. Server, contracts, client-runtime, web and mobile typecheck clean, and lint is clean on the changed lines.

**Review fixes** (bots plus three rounds of adversarial review by GPT 6.1 Sol and Claude Fable 5.1)
- **The task there gets what a local child gets.** The role prompt, and the parent's provider and model unless the caller names others. Modes the caller omits are left to the other side, which caps the parent's modes at the link's access.
- **Its own run, read whole.** The launch's run id is kept on `remoteChild.runId`. The follower waits on that run and takes the result from that run's last reply, paging through the whole thread and reading a long reply to its end (`itemId` + `textOffset`). A later run's reply is never taken as the result.
- **Retries and keys.** The local record's command id carries the provider session's namespace, matching the forwarder's launch key, so a reused `clientRequestId` from another session is a new task on both sides. Each completion attempt has its own command id, and a report refused because the task already ended counts as done.
- **Terminal failures end the task.** A revoked or expired link, a link forgotten here, or a thread or run gone there fails the task with the reason, instead of retrying forever.
- **Stopping stops the whole thread there.** Cancel, and the parent's Stop, send `t3_thread_interrupt` with `stop: true` (#16684), which also holds the thread's queue there and stops the tasks it delegated. A cancel after the result still stops follow-up work there.
- **A stop owed survives.** `remoteChild.stoppedThere` records that nothing is left to stop there. A task cancelled or stopped here without it is followed again after a restart, and the follower keeps sending the stop until it lands. A run that ends there while the parent's Stop lands here doesn't count as stopped (`stoppedThere: "ran-out"` vs `"stopped"`), so its queued work is still stopped. A stop the other side refuses on mode grounds, say after the parent was lowered, ends instead of retrying.
- **Smaller fixes.**
  - A failed local record interrupts the thread it just launched.
  - The follower reaches the peer with this environment's id.
  - Matching a project there pages through every project.
  - A local `target.projectId` naming another project is refused.
  - Delegating reads the one link it needs without probing every peer.
- **Tests.** `RemoteDelegation.test.ts` went from 4 to 15 tests. They cover:
  - a 45,000-character reply and a later run's reply present;
  - namespacing;
  - the thread gone there and the link forgotten here;
  - cancel without a link, and cancel after the result;
  - the parent's Stop, while the box is unreachable, across a restart, while the result is being read, and when the box refuses it.

  Each was mutation-checked.
- **Client tests.** `threadShell.test.ts` runs both environments' shell atoms: the child is left out of navigation while its parent is listed, and listed again when the parent is gone or its machine is switched off; an approval from the child's own subagent flags the parent and fills its banner. Each rule was mutation-checked. `Sidebar.logic.test.ts` covers the sidebar filter.
- **Deferred:** recording the task here before launching it there, to cover a crash or lost response between the two without a retry. That's #17051, with a design. Today a failed record interrupts the launched thread, and a retry with the same `clientRequestId` replays the launch and records it.

**Demo: link, delegate, follow the lineage** (dev servers on this PR's head, hostnames *laptop* and *box*, real Codex agents)

Steps, in order:
1. Create a pairing link on the box.
2. Link it from the laptop's Settings → Connections.
3. The box lists the laptop as a client it can revoke.
4. A laptop agent gets a plain prompt ("delegate this to my box environment…"), with no model or modes.
5. It calls `delegate_task` with only `target.environmentId`. The task runs on the box under the link's auto-accept-edits, while the parent has full access.
6. The parent wakes with the box's result. Only the box's `pricing.ts` changed.
7. With the box added as an environment on the laptop client, the "on box" card opens the box's thread, which shows "From T3 Code · laptop" and "Subagent of …". "Open parent thread" returns.

Pairing codes are blurred.

https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/7edb274b45c1a49f/peer-delegation-demo.mp4

| Parent on the laptop: task card and Lineage say "on box" | Child on the box, opened from the laptop: "Subagent of" and "From T3 Code · laptop" |
|---|---|
| ![Laptop parent thread: the delegated task card and the Lineage row both read on box · Done, and the parent's reply reports the box's change](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/3fd97090d3123d78/parent-lineage.png) | ![Box child thread opened from the laptop client: a Subagent of divider names the laptop parent, and the header badge reads From T3 Code · laptop](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/73d6243f1d7f8ae7/child-subagent-of.png) |

The first take of this demo found two gaps, which this PR now fixes:
- The agent's first two `delegate_task` calls failed. One had no `target.model` (`target_required`). The other used the parent's full access, which was broader than the link's auto-accept-edits (`runtime_mode_escalation_denied`). The model and modes now inherit as described above, and a new test covers both: `a task named only by environment inherits the parent's model, within the link`. Restoring the old mode handling fails that test with the same `runtime_mode_escalation_denied` the demo hit.
- The child's divider showed the title the parent had at delegation, before the parent renamed itself. It now uses the parent's live title when this client can see it.

**End to end, two real servers** (`t3 serve` built from this PR's head, `d1f9fbd237`, so nothing above it in the stack)
- A *laptop* and a *box* on one machine, each with its own data directory and a project cloned from one bare origin. Linked with `t3 environment link … --access auto` and a pairing code from the box. A real Claude Sonnet 5.5 agent on the laptop calls `delegate_task` with `target.environmentId` set to the box, and the child is a real Claude turn there.
- **Delegate and complete.** The child edited `math.ts` in the box's checkout and replied `BOX DONE cups …/box-repo  M math.ts`. The task went from `running` to `completed` in 13 s with that reply, and the parent woke with `PARENT GOT BOX DONE …`. The laptop's checkout stayed clean. The box's thread carries the link's origin ("From T3 Code · cups").
- **Restart mid-task.** The laptop was stopped 1 s after delegating and restarted 9 s later, while the box child was still running a 60 s command. The follower picked the task up at startup. The task completed when the box run did (21:06:40), and the parent woke with `PARENT GOT BOX SURVIVED RESTART.`
- **Cancel.** With the box child mid-command, the parent agent called `task_cancel`, which returned `cancel_requested`. The task became `cancelled` and the box run `interrupted` at the same moment (21:07:47), and the parent reported `PARENT GOT Cancelled.`
- **Revoke.** With a child running, `t3 auth session revoke` on the box. The laptop's next poll failed the task in under a minute with "The linked environment no longer accepts this link. It may have been revoked there; link it again.", the parent woke with that reason, and `t3 environment list` shows it as the link's last error.

![Laptop: the parent wakes with the box's result. Box: the child that ran there, labelled From T3 Code · cups, with its math.ts change](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/f9b90b3bdfa4b15f/complete-pair.png)

![task_cancel interrupts the run on the box, and a revoked link fails the task with the reason](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/4d4a17851d4b1191/cancel-revoke-pair.png)

Opus 5.5 via Claude Code.

🤖 Generated with [Claude Code](https://claude.com/claude-code)


<!-- devin-review-badge-begin -->

---

<a href="https://app.devin.ai/review/pingdotgg/t3code/pull/16719" target="_blank"><picture><source media="(prefers-color-scheme: dark)" srcset="https://static.devin.ai/assets/gh-devin-review-dark.svg?v=4"><img src="https://static.devin.ai/assets/gh-devin-review-light.svg?v=4" alt="Devin Review"></picture></a>
<!-- devin-review-badge-end -->




## Closes discussions

- [#19309 Let MCP choose the environment for t3_thread_launch and delegate_task](https://github.com/pingdotgg/t3code/discussions/19309) (with #16684)


