Skip to content

Fix six rough edges from a day of running bots - #318

Merged
lydakis merged 8 commits into
mainfrom
claude/project-thread-qqlcnj
Oct 29, 2026
Merged

lydakis merged 8 commits into
mainfrom
claude/project-thread-qqlcnj

Conversation

@lydakis

@lydakis lydakis commented Oct 29, 2026

Copy link
Copy Markdown
Owner

Requested by George · project thread

Six fixes from a day of running bots, one commit each.

What changes

  1. Every Claude bot Sonny runs keeps its prompt cache for an hour. Before this, tasks from start_task were created without --cache-ttl, and leads made before the hour-long default were still on 5 minutes. So any gap over 300 s re-read the whole context uncached. Now tasks start with 1h, and when Sonny opens an existing Claude bot whose TTL differs from the policy, it moves the bot to 1h with the new configure field cache_ttl. The protocol version is unchanged.
  2. Triggers keep working when the store file is replaced. A trigger pinned the store's identity, which hashes the file's inode. A copied or restored store made every fire fail with store_mismatch, and trigger ls and the background view filtered those triggers out. Triggers now pin the store's lineage, which survives a copy. Sonny also restamps any trigger plists still carrying the old identity when it starts. A fire that is skipped or fails attaches one line to its reply-to bot (or else its target), so the agent finds out without starting a turn.
  3. An empty reply ends a turn. Before, an empty completion was accepted only after a tool call. A lead that read a wake, called no tool and decided there was nothing to say failed with empty_completion. Now any empty completion completes with empty text.
  4. The approver can read a write whose preview was cut before its path. A model may write {"content": …, "path": …}. Past the 2 KiB preview the path was gone, and an ungated write has no planning node to read back, so every later gated call that turn was escalated as "the call is too long to show". tool_started now names the file of a cut write or edit, as it already did for apply_patch. The prompts read keeps those paths ahead of a call it cuts at its byte budget. When a write still names nothing, the escalation says that, instead of blaming the call under review.
  5. A task that resolves itself still reports. The resolve_task description now says the reply after the call is what the lead reads. Before, a task could call it and then end with nothing, so the lead's wait got empty text.
  6. history reads a turn by its handle. Every tool hands out turn:BOT/N with store-wide numbers, but history's turn counts one bot's turns from 1. The tool now takes handle, and turn_not_in_history says how each one is numbered.

Checked

  • cargo fmt --check, cargo clippy --workspace --all-targets -D warnings and cargo test --workspace pass.
  • New runtime tests, each confirmed to fail without its fix where that applies:
    • test_configure_moves_a_bot_to_the_hour_long_cache
    • test_a_fire_finds_its_store_after_the_file_was_replaced
    • test_an_empty_reply_ends_a_turn_after_a_tool_call_and_after_a_prompt
    • test_an_ungated_write_cut_before_its_path_still_names_its_file
    • the history handle cases in test_siblings
    • the attached missed-fire line in test_trigger
  • New unit tests: Sonny cache policy for tasks and existing bots, trigger restamping, and paths from cut writes and edits.
  • Full runtime suite: see CI.
  • Hot path: tool_started now parses the arguments of a cut write or edit once. That is one serde_json read of a call that already arrived whole, the same work a cut patch already did. The prompts read parses only the one call it cuts at its budget.

No screenshots: nothing here changes what Sonny draws.


Generated by Claude Code

Sonny gave the 1-hour cache only to bots it made from the terminal. Tasks
started with start_task never got it, and leads made before that change
stayed on five minutes, so every idle gap over five minutes reread the
whole context.

Tasks now start with --cache-ttl 1h for anthropic/ and bedrock/ models,
and Sonny moves a bot it opens to that lifetime when it has another.
The daemon's configure takes cache_ttl for that.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013jkdGEx3HPXnzgjwn75aHw
An empty reply to a prompt failed as empty_completion. Sonny's lookout asks
a lead to end with an empty reply when a note needs nothing, so a lead that
decided that without calling a tool failed and spent a full context.
Saying nothing is the bot's choice; the turn completes with empty text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013jkdGEx3HPXnzgjwn75aHw
run, wait and the history listing name turns with store-wide handles
(turn:BOT/N), while history's turn counts a bot's turns from 1, so a
handle's number read another turn or none. history now takes handle in
place of bot and turn, and turn_not_in_history says how turns are
numbered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013jkdGEx3HPXnzgjwn75aHw
A task called resolve_task and ended its turn with nothing, so its lead's
wait read empty text and had to look elsewhere for the result. The tool
now says the reply after it is what the lead reads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013jkdGEx3HPXnzgjwn75aHw
A trigger named its store by the daemon's store identity, which hashes the
file's inode, so a store put back from a copy failed every fire with
store_mismatch, and the triggers dropped out of the lists that filter by
the same id. Triggers now name the store's lineage, which a copy keeps;
Sonny rewrites triggers made with the old identity once it opens on that
store.

A fire the daemon took no turn for (skipped while the bot works, or
failed) now tells the reply agent, else the agent it messages, by attach,
so a missed run is heard of in that agent's next model call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013jkdGEx3HPXnzgjwn75aHw
A write whose content comes before its path, as a model may order it,
was cut at its 2 KiB preview before the path. An ungated one has no node
to read back, so every later gated call in the turn was escalated as
"too long to show". tool_started now names the file of a cut write or
edit as it does a patch's, the prompts read keeps those paths ahead of a
call it cuts at its byte budget, and a write that still names nothing is
said to be that, not the call being judged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013jkdGEx3HPXnzgjwn75aHw
@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:27:58.997923Z be223da New commits
🔒 Security Review ✅ Completed 2026-10-29T04:29:06.174239Z be223da 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.

@lydakis lydakis self-assigned this Oct 29, 2026

@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: 1bb9b60975

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/store/db.rs Resolved
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013jkdGEx3HPXnzgjwn75aHw
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013jkdGEx3HPXnzgjwn75aHw
@lydakis
lydakis merged commit 510c16d into main Oct 29, 2026
4 checks passed
lydakis pushed a commit that referenced this pull request Oct 29, 2026
Main does not build since #344 renamed Places::plist to Places::job
while #318's restamp still called the old name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013jkdGHCyGRHdT5WYDPKVFY
lydakis pushed a commit that referenced this pull request Oct 29, 2026
#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
lydakis pushed a commit that referenced this pull request Oct 29, 2026
#344 renamed Places::plist to Places::job while #318 added a call to the
old name, so main stopped compiling.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013jkdGCoLcHQMmGw188SP8o
lydakis added a commit that referenced this pull request Oct 29, 2026
#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.


Claude-Session: https://claude.ai/code/session_013jkdGF2ShRt6WjfWgiPdZF

Co-authored-by: Claude <noreply@anthropic.com>
lydakis added a commit that referenced this pull request Oct 30, 2026
…oal clear clears (#397)

* Goals: keep an agent working until a check command passes

The goal tool sets an agent, or a task it made, a goal: what done looks
like and a shell command that exits 0 once it is. The lookout runs the
check each time the agent's turn completes; while it fails, the agent
gets a turn with its exit code and the end of its output, and its lead
hears nothing. The goal ends once the check passes, the budget is spent,
two turns it started call no tool, or the agent gives up with blocked,
and the lead hears that last turn with how it ended. No judge, nothing in
the daemon.

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

* Goals: the lead-reaction test's lookout gets its goals

* Goals: answer Codex's review

- A check's output is read as it prints, keeping only its end.
- A turn's goal is read fresh as the turn ends, so one a lead set or
  cleared meanwhile counts.
- One check at a time per agent; a turn that ends while one runs is
  plain news.
- A task is the caller's by its creator's id, not its name.
- A turn waiting for its check keeps the replay cursor before it, so a
  restarted lookout runs the check again.
- The check runs without the provider keys in ~/.agent/env.
- The macOS skip is back on the trigger test, not on note_for.

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

* Triggers: restamp reads the job file by its new name

Main does not build since #344 renamed Places::plist to Places::job
while #318's restamp still called the old name.

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

* Goals: answer Codex's second review

- A goal left by an agent deleted while the lookout was away is dropped
  when another agent with that name ends a turn.
- A goal's next turn is not news while it runs, as its end is.
- A goal cleared or set again while its check ran is plain news, even
  when that check passed.
- Listing goals shows a lead its tasks' goals, whoever set them.

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

* Goals: a turn its check holds is not judged, and a goal given up while its check ran ends so

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

* Goals: a goal set again counts its turns without a tool from 0

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

* Goals: a dropped connection stops the checks under way, which the replay runs again

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

* Goals: a stopped check takes what it started with it, and a replayed turn is not counted twice

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

* Goals: the lead hears how a goal ended in the lookout's words only

The reason an agent gives up for, and a check it set itself, are its own
text; the lead reads them in the turn or with the goal tool.

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

* Goals: a lookout giving way stops its checks, and only a heard task's turn waits on one

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

* Goals: a check runs where its turn worked, and how a goal ended survives a later turn's news

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

* Goals: a check goes without every provider key an agent's shell does

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

* Goals: tests for a goal its lead sets, a turn that ends mid-check, and a lookout that gives way mid-check

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

* Goals: a turn the lead's wait read still says how its goal ended

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

* Goals: the held-turn test asks due() as main's lookout has it

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

* Goals: a check under way keeps the replay cursor before its turn, watched or not

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

* Goals: a wait that reads a turn leaves how its goal ended to tell

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

* Goals: a trigger passing a turn's answer on leaves how its goal ended to tell

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

* Goals: deleting an agent stops its check

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

* Goals: a goal set again while its last one is checked is checked afresh

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

* Goals: how a goal ended is news even while the lead waits on its turn

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

* Daemon companions: commands the daemon keeps running for as long as it runs

`agent companion add/rm/ls` keeps a command in the store that every daemon
on it starts once it listens, restarts with backoff and stops at shutdown.
Sonny's lookout and approver now run as companions, so they share the
daemon's life cycle whether or not a window is open.

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

* Companions: log setup off the runtime thread, the whole group stopped, a removed one waited for, and Sonny's take store and socket from the daemon

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

* Companions: idle-exit counts from the last client's activity, a companion's whole group is no client, and a log that is no file fails at once

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

* Companions: a session is a companion's or not once, as it opens

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

* Sonny: /goal sets the agent a goal by hand, checked at once when it is idle

`/goal CHECK` keeps the agent the window shows working until CHECK exits 0
in its folder; `/goal` shows its goal and `/goal clear` clears it. With no
turn running, the goal file carries the agent's last turn as `check_after`
and the lookout runs the check on its next tick, as if that turn had just
completed, so a failing check starts the agent's goal turn at once.

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

* Goals: /goal starts an agent with no turn yet, and a check-at-once mark stays until its check says

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

* Goals: a check settles only what is its own goal's, by when it was set

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

* Goals: /goal WHAT goes to the agent as typed, which sets its own goal

George picked the Codex shape: the person's words reach the agent as their
message, and the agent sets the goal and its check with its goal tool,
whose description now says so. /goal shows the goals of the agent and its
tasks, and /goal clear clears its own, through the tool's own call. The
person-set check and check-at-once machinery is gone.

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

---------

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