Skip to content

Sonny: fix main's build; restamp edits a systemd trigger too - #351

Merged
lydakis merged 1 commit into
mainfrom
claude/project-thread-7ays9a
Oct 29, 2026
Merged

lydakis merged 1 commit into
mainfrom
claude/project-thread-7ays9a

Conversation

@lydakis

@lydakis lydakis commented Oct 29, 2026

Copy link
Copy Markdown
Owner

Main has not compiled since #318 and #344 landed together. restamp (from #318) read the definition through Places::plist, and #344 renamed that to Places::job (E0599).

  • restamp now uses places.job(...).
  • On a systemd unit it rewrites the quoted "--store-id" "ID" pair in ExecStart=. The plist's <string>ID</string> edit does not match unit text. On a plist the edit is unchanged.
  • restamp_at_start no longer returns early off macOS, and it loads through trigger::schedule. This matches refresh after Sonny: triggers on Linux, kept by systemd's user manager #344.
  • The restamp test now runs against both a launchd world and a systemd world.

cargo test -p sonny trigger (39 pass), cargo clippy -p sonny --all-targets -D warnings and cargo fmt --check are clean.


Generated by Claude Code

#318's restamp still read the definition through Places::plist, which
#344 renamed to Places::job, so main did not compile. The store id it
rewrites now has a unit form too, and restamp runs at start on Linux.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013jkdGF2ShRt6WjfWgiPdZF
@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-29T04:56:26.208907Z 488cadb PR opened
🔒 Security Review ✅ Completed 2026-10-29T04:57:28.229982Z 488cadb PR opened
ℹ️ 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.

lydakis pushed a commit that referenced this pull request Oct 29, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013jkdGFMvxuKK3qzEb4j48d
@lydakis
lydakis merged commit e21fa0c into main Oct 29, 2026
4 checks passed
lydakis added a commit that referenced this pull request Oct 29, 2026
…309)

Bots take `unsandboxed`, command prefixes (`mcpx`, `docker compose`) that run outside their sandbox without asking, when a plain script runs nothing else. Sonny reads `[sonny] unsandboxed` from ~/.agent/config.toml for its leads and tasks, and in `full` mode tasks run unconfined, as their lead does. Carries #351's fix for main's build (restamp uses Places::job).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013jkdGHN4UHiWFxnggwKHoV
lydakis added a commit that referenced this pull request Oct 29, 2026
* Lay Sonny's work out in cmux

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

* Leave a Sonny in a Herdr pane to Herdr when cmux hosts Herdr

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

* Sonny: `sonny acp`, Sonny as a T3 Code provider

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

* cmux-tabs: point agents at cmux's Automation mode when it refuses them

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

* sonny acp: whole call arguments, waiting approvals, claims by folder, 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

* sonny acp: approve only what was shown whole; follow again when the stream 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

* sonny acp: stop before the turn is known, task names as start_task names 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

* sonny acp: calls in flight on bind, the rest of a dropped reply, every 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

* sonny acp: a reasoning item leaves the streamed reply as it is

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

* sonny acp: a task thread that came as its turn ended answers once

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

* sonny acp: read the person's words out of each block T3 Code sends

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

* sonny acp: T3 Code's model picker lists your models and switches the 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

* sonny acp: one model change at a time, and a pick waits for the bot's 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

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

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

* Port #351's fix for main's build (trigger restamp)

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

* sonny acp: every held prompt is answered, and none waits past 30 s

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

* test_acp: the steered prompt's idle comes before its held answer, not 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

---------

Co-authored-by: Claude <noreply@anthropic.com>
lydakis added a commit that referenced this pull request Oct 29, 2026
* Lay Sonny's work out in cmux

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

* Leave a Sonny in a Herdr pane to Herdr when cmux hosts Herdr

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

* Sonny: `sonny acp`, Sonny as a T3 Code provider

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

* cmux-tabs: point agents at cmux's Automation mode when it refuses them

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

* sonny acp: whole call arguments, waiting approvals, claims by folder, 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

* sonny acp: approve only what was shown whole; follow again when the stream 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

* sonny acp: stop before the turn is known, task names as start_task names 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

* sonny acp: calls in flight on bind, the rest of a dropped reply, every 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

* sonny acp: a reasoning item leaves the streamed reply as it is

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

* sonny acp: a task thread that came as its turn ended answers once

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

* sonny acp: read the person's words out of each block T3 Code sends

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

* sonny acp: T3 Code's model picker lists your models and switches the 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

* sonny acp: one model change at a time, and a pick waits for the bot's 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

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

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

* Port #351's fix for main's build (trigger restamp)

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

* sonny acp: every held prompt is answered, and none waits past 30 s

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

* test_acp: the steered prompt's idle comes before its held answer, not 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

* T3 Code opens Sonny threads again: the model option carries `id`

T3 Code finds the model option by `id` even on protocol version 2,
whose schema names it `configId`. Since the model picker landed, Sonny
sent only `configId` on version 2, so T3 threw opening every session:
no prompt reached Sonny, and the picker probe failed to "Default".
The option now carries both names, and a pick may name it by either.

A failed daemon connection is no longer kept for the process's life,
so a daemon restarted under a running T3 is reached on the next thread.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
lydakis added a commit that referenced this pull request Oct 29, 2026
…ead leaves the lead's model alone (#363)

* Lay Sonny's work out in cmux

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

* Leave a Sonny in a Herdr pane to Herdr when cmux hosts Herdr

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

* Sonny: `sonny acp`, Sonny as a T3 Code provider

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

* cmux-tabs: point agents at cmux's Automation mode when it refuses them

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

* sonny acp: whole call arguments, waiting approvals, claims by folder, 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

* sonny acp: approve only what was shown whole; follow again when the stream 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

* sonny acp: stop before the turn is known, task names as start_task names 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

* sonny acp: calls in flight on bind, the rest of a dropped reply, every 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

* sonny acp: a reasoning item leaves the streamed reply as it is

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

* sonny acp: a task thread that came as its turn ended answers once

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

* sonny acp: read the person's words out of each block T3 Code sends

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

* sonny acp: T3 Code's model picker lists your models and switches the 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

* sonny acp: one model change at a time, and a pick waits for the bot's 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

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

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

* Port #351's fix for main's build (trigger restamp)

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

* sonny acp: every held prompt is answered, and none waits past 30 s

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

* test_acp: the steered prompt's idle comes before its held answer, not 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

* T3 Code opens Sonny threads again: the model option carries `id`

T3 Code finds the model option by `id` even on protocol version 2,
whose schema names it `configId`. Since the model picker landed, Sonny
sent only `configId` on version 2, so T3 threw opening every session:
no prompt reached Sonny, and the picker probe failed to "Default".
The option now carries both names, and a pick may name it by either.

A failed daemon connection is no longer kept for the process's life,
so a daemon restarted under a running T3 is reached on the next thread.

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

* T3 Code: models as /model lists them, an effort picker, and a new thread leaves the lead's model alone

- The model picker lists what the providers list now, as Sonny's /model
  does, with ~/.agent/models' notes and the models only it names; a
  listing that takes over five seconds leaves the file's. A file written
  before a model came out no longer hides it.
- An Effort option (category thought_level) beside the model: the bot's
  family's efforts, taken at once when idle and with the next turn when
  not, as a model is.
- A pick made before a thread has a bot is for a lead Sonny makes. T3 Code
  puts its last pick to every thread it opens, so a new thread in a folder
  whose lead was there already moved that lead, every other thread's too,
  to it.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
lydakis added a commit that referenced this pull request Oct 29, 2026
* Lay Sonny's work out in cmux

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

* Leave a Sonny in a Herdr pane to Herdr when cmux hosts Herdr

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

* Sonny: `sonny acp`, Sonny as a T3 Code provider

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

* cmux-tabs: point agents at cmux's Automation mode when it refuses them

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

* sonny acp: whole call arguments, waiting approvals, claims by folder, 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

* sonny acp: approve only what was shown whole; follow again when the stream 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

* sonny acp: stop before the turn is known, task names as start_task names 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

* sonny acp: calls in flight on bind, the rest of a dropped reply, every 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

* sonny acp: a reasoning item leaves the streamed reply as it is

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

* sonny acp: a task thread that came as its turn ended answers once

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

* sonny acp: read the person's words out of each block T3 Code sends

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

* sonny acp: T3 Code's model picker lists your models and switches the 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

* sonny acp: one model change at a time, and a pick waits for the bot's 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

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

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

* Port #351's fix for main's build (trigger restamp)

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

* sonny acp: every held prompt is answered, and none waits past 30 s

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

* test_acp: the steered prompt's idle comes before its held answer, not 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

* T3 Code opens Sonny threads again: the model option carries `id`

T3 Code finds the model option by `id` even on protocol version 2,
whose schema names it `configId`. Since the model picker landed, Sonny
sent only `configId` on version 2, so T3 threw opening every session:
no prompt reached Sonny, and the picker probe failed to "Default".
The option now carries both names, and a pick may name it by either.

A failed daemon connection is no longer kept for the process's life,
so a daemon restarted under a running T3 is reached on the next thread.

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

* T3 Code: models as /model lists them, an effort picker, and a new thread leaves the lead's model alone

- The model picker lists what the providers list now, as Sonny's /model
  does, with ~/.agent/models' notes and the models only it names; a
  listing that takes over five seconds leaves the file's. A file written
  before a model came out no longer hides it.
- An Effort option (category thought_level) beside the model: the bot's
  family's efforts, taken at once when idle and with the next turn when
  not, as a model is.
- A pick made before a thread has a bot is for a lead Sonny makes. T3 Code
  puts its last pick to every thread it opens, so a new thread in a folder
  whose lead was there already moved that lead, every other thread's too,
  to it.

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

* sonny README: T3 Code's pickers as #363 left them

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
@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