Skip to content

sonny acp: a task opens as the lead thread's child in T3 Code - #352

Merged
lydakis merged 22 commits into
mainfrom
claude/project-thread-tacqoq
Oct 29, 2026
Merged

lydakis merged 22 commits into
mainfrom
claude/project-thread-tacqoq

Conversation

@lydakis

@lydakis lydakis commented Oct 29, 2026

Copy link
Copy Markdown
Owner

George picked "tasks as children of the lead" for how Sonny's tasks show up in T3 Code. Until now, each task opened as its own top-level thread through t3_thread_launch.

What changes

  • A task started while the person's prompt runs now opens through T3's delegate_task with {task: brief, title, clientRequestId: bot}. T3 makes it a child thread, nested under the lead's thread.
    • T3 opens a child in the parent's checkout, so the pending claim is written for the lead thread's folder. The task itself still works in its own worktree.
    • The prompt's answer is held until every task opening in that turn is open. T3 only accepts a child while the parent's run is active.
    • The call has a 30 s timeout. A late call keeps its pending claim and opens no second thread.
  • A turn the lead takes on its own (for example, the lookout waking it) has no T3 run, so T3 refuses with parent_not_active. Sonny then falls back to t3_thread_launch in the task's worktree, as before. That covers any T3 that doesn't offer delegate_task too.
  • T3's completion message ("Delegated task X reached a terminal state. Use task_status…") is answered end_turn without starting a lead turn. The lead already hears of its tasks from the lookout.
  • README and module docs are updated.

Tests

  • test_a_thread_is_its_folders_lead_…:
    • the task is delegated, before the prompt is answered, with the brief
    • the child thread opened in the lead's folder claims the task
    • T3's completion message starts no turn
  • test_a_prompt_that_steers_…: when delegate_task refuses, the task falls back to t3_thread_launch in the worktree.
  • New unit test for t3::is_completion.
  • tests.test_acp, the sonny unit tests, clippy and fmt all pass locally.

No screenshot yet: the nesting shows in T3's sidebar, which I'll check on George's Mac once this is installed.


Generated by Claude Code

claude added 18 commits October 28, 2026 23:36
Sonny run in a cmux tab now makes the project's cmux workspace its home,
as it does a Herdr workspace or a Rex session: each task the lead starts
gets a tab in the lead's pane running Sonny on it, titled with the bot's
name; run_in_pane and show_agent open tabs and splits there; another
project's lead gets a workspace of its own. Every call goes through
`cmux rpc` (system.tree, workspace.create, surface.create, surface.split,
surface.close, tab.action), and the workspace is kept in
~/.agent/sonny/places so the lookout's calls land in it.

cmux already reads the OSC 7501 status and OSC 777 notifications Sonny
writes, so its sidebar shows each task's state and rings the tab that
needs the person. Each Sonny also gives its tab a resume command
(`cmux surface resume set`, `Sonny: NAME`), so cmux reopens it after a
relaunch once the person allows `sonny` commands; quitting clears it.

Agents get a cmux-tabs skill like herdr-panes, and the daemon keeps a
cmux tab's own place variables (CMUX_WORKSPACE_ID, CMUX_SURFACE_ID,
CMUX_TAB_ID, CMUX_PANEL_ID) out of its tools' shells, as it does Herdr's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013jkdGFMvxuKK3qzEb4j48d
T3 Code runs `sonny acp` as an ACP provider, a process per thread. A
thread is its folder's lead, set up as `sonny` sets it up (rules, skills,
tools, approval mode and sandbox, the lookout). Turns and calls become
the thread's updates; a call waiting for the person is asked with
session/request_permission; turns the lead takes on its own (the lookout
waking it) show between prompts, as ACP v2 work the agent started. Each
task the lead starts opens as a T3 thread of its own via T3's
t3_thread_launch tool, and that thread binds to the task.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013jkdGFMvxuKK3qzEb4j48d
Agents' shells run in the daemon, which cmux did not start, so cmux's
default "cmux processes only" mode refuses them. The skill and README now
name the setting that lets them in, and say a workspace the person asks
for is a project's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013jkdGFMvxuKK3qzEb4j48d
… Claude's tool results

- A call whose event carried only the start of its arguments is read from
  history, so a long start_task still opens the task's thread.
- A thread that comes to a bot already waiting for the person is asked
  for those calls.
- A task thread claims the task waiting for its message in its folder; the
  same brief twice in one folder is told apart by the task's name.
- A resumed thread Sonny has no binding for is refused, not made a lead.
- History items go through `responses`, so Anthropic-family bots' tool
  results show and their start_task opens threads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013jkdGFMvxuKK3qzEb4j48d
…tream drops

- A call listed with its arguments cut is shown whole, read from the node
  that planned it, before the person may allow it; when that read fails,
  the person may only deny it.
- When the daemon lets a thread's stream go (follow_lagged, or the
  connection ends), the thread follows its bot again, answers a prompt
  whose turn ended meanwhile, and asks for calls waiting on the person; a
  prompt is failed rather than left waiting while the daemon is away.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013jkdGFMvxuKK3qzEb4j48d
…mes them, services on resume, no missed events, one follower

- A cancel that comes before the prompt's turn is known is kept: the
  prompt is not sent, or its turn is interrupted once it is.
- A task named with its project already (`calc.fix`) is found as
  `start_task` names it, not as `calc.calc.fix`.
- A thread that comes back to a saved bot starts what it needs: the
  lookout, the lead's subscription, and the judge for an `auto` gate.
- A thread whose stream dropped follows on from the last event it
  handled, so what happened meanwhile (a task started) is handled too.
- Closing or replacing a thread stops its follower.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013jkdGFMvxuKK3qzEb4j48d
…y approval page, answers on the live connection, steers by their own turn

- Binding to a running bot keeps the running turn's started calls from the
  replay, so a start_task finishing after the thread came opens its thread.
- A reply whose stream dropped partway sends the rest from the kept
  message instead of stopping at what streamed.
- Approvals are listed page by page.
- A permission answer goes through the thread's connection as it is when
  the person chooses; one not delivered is asked again on reattach.
- A prompt that steers waits on the turn the daemon names for it, and on
  the turn that takes it in once steered; a turn finishing before then no
  longer answers it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013jkdGFMvxuKK3qzEb4j48d
The streamed prefix is settled only by the assistant's words, so a model
that reasons before it answers no longer has its reply said twice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013jkdGFMvxuKK3qzEb4j48d
The claimed prompt is answered here only while it is still this thread's
and waits on no turn; one the follower already answered is left alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013jkdGFMvxuKK3qzEb4j48d
T3 Code sends every prompt as its instructions around the person's words,
then a note of its own (<runtime_info>, <pull_request_linking>) as a
second text block. Joined, the prompt no longer ended in </user_request>,
so nothing was unwrapped: a task's thread could not claim its task (its
brief no longer matched) and opened a lead for the worktree instead,
failing with project_exists; leads read T3 Code's instructions for tools
they do not have.

Each block is now unwrapped on its own and T3 Code's note is left out.
The runtime tests send T3 Code's real two-block shape.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013jkdGFMvxuKK3qzEb4j48d
…lead

session/new and session/resume return a model option (category model)
built from ~/.agent/models, with Default first. A pick before the first
prompt is the model a new lead starts on; a later pick configures the
bot when it is idle, or before its next turn. A bot's thread offers only
its provider family, refuses another with -32602, and shows the bot's
model again through config_option_update when another client changes it.
A task's thread keeps the model its lead gave the task.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013jkdGFMvxuKK3qzEb4j48d
… next idle

Model changes hold the session's switching lock, so overlapping picks
cannot drop a configure that succeeded. A pick made while the bot works
is applied when its turn finishes, not only before the person's next
prompt, so the lookout's turns run it too. The thread refreshes the bot's
model from inspect when a turn ends, rather than taking a turn's model,
which may be that turn's own override.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013jkdGFMvxuKK3qzEb4j48d
While the person's prompt runs, the task a lead starts opens through
T3 Code's delegate_task, as a child thread nested under the lead's, in
the lead's folder (the task works in its own worktree). The prompt's
answer waits until the child is open, so T3 Code's run is still on. A
turn the lead takes on its own has no run to be a child of: delegate_task
refuses, and the task opens with t3_thread_launch as before. T3 Code's
message that a delegated task ended starts no turn; the lookout tells
the lead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013jkdGFMvxuKK3qzEb4j48d
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-29T05:19:07.906642Z 73ac97d New commits
🔒 Security Review ✅ Completed 2026-10-29T05:19:41.786074Z 73ac97d New commits
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@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: 084c19a701

ℹ️ 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 sonny/src/acp.rs Resolved
Comment thread sonny/src/acp.rs Resolved

lydakis commented Oct 29, 2026

Copy link
Copy Markdown
Owner Author

The rust check failed, and the failure isn't this PR's. Main doesn't build since #344: sonny/src/trigger.rs:2377 calls Places::plist, which no longer exists. #351 fixes it. I merged main and ported #351's trigger.rs change unchanged in b759dff, so this branch builds again. The ported change becomes a no-op once #351 lands.


Generated by Claude Code

Answers held while a task's thread opens are kept in a list, so a
second prompt whose turn ends meanwhile cannot drop the first. The hold
ends after 30 s whatever the opening does, fallback launch included; a
thread still opening opens on its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013jkdGFMvxuKK3qzEb4j48d
… last

The answer now waits for the task's thread, whose "Opened" update can
come after the idle one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013jkdGFMvxuKK3qzEb4j48d
@lydakis
lydakis merged commit 970d603 into main Oct 29, 2026
4 checks passed
@lydakis lydakis mentioned this pull request Oct 29, 2026
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