Skip to content

buzz-acp: tell the agent to start fresh on the session/new that follows !rotate - #6103

Open
jhgaylor wants to merge 4 commits into
block:mainfrom
jhgaylor:buzz-acp/rotate-requests-fresh-session
Open

buzz-acp: tell the agent to start fresh on the session/new that follows !rotate#6103
jhgaylor wants to merge 4 commits into
block:mainfrom
jhgaylor:buzz-acp/rotate-requests-fresh-session

Conversation

@jhgaylor

@jhgaylor jhgaylor commented Aug 17, 2026

Copy link
Copy Markdown

Stacked on #6088 and #6101 — the diff shows their commits until they merge; this PR's own change is the last commit (6daa3f5).

Summary

!rotate drops the harness's ACP session for a channel, and the next turn sends session/new. But session/new carries _meta.channelId (#6088), and an agent or gateway that keys its own state by channel — fountain acp's channel-bound conversations — hands the same conversation straight back. On such an agent !rotate is a no-op.

Observed on a hosted harness: !rotate logged as consumed (invalidated=1), and the next turn resumed the same conversation.

Change

  • SessionStore::request_fresh(channel) / take_fresh(channel) — remember that the owner rotated the channel (in-memory; independent of whether the store is persisted).
  • Both !rotate paths (idle and in-flight) call request_fresh and session_store.remove up front.
  • The next session/new for that channel carries _meta.freshSession: true (SessionOrigin.fresh); the member is absent otherwise. Consumed on that one call.
  • README documents the member and what a channel-keyed gateway must do with it.

Also fixes a latent bug from #6088: an in-flight !rotate never removed the channel from the on-disk session store (only the idle path did), so with BUZZ_ACP_STATE_DIR set, the post-rotate turn could session/load the rotated session.

The consuming side for Fountain is BinaryBourbon/fountain#788.

Test plan

  • session_new_with_origin_sends_fresh_session_after_rotate (new), session_new_with_origin_sends_channel_id_and_type_in_meta asserts the member is absent when not rotated
  • session_store::fresh_request_is_consumed_once_and_works_when_disabled (new)
  • cargo test -p buzz-acp --lib — 791 passed; clippy + fmt clean
  • Prod: @agent !rotate on Fountain → next mention lands in a new conversation

🤖 Generated with Claude Code

@mfethe1

mfethe1 commented Aug 18, 2026

Copy link
Copy Markdown

Same conflict as #6104 — this branch also collides with main at the same spot in crates/buzz-acp/src/lib.rs (a function this branch's stack adds right after event_mentions_agent, next to workflow_attributed_author which landed on main since this branch was opened). Same trivial resolution: keep both. One rebase of the stack past current main should clear it on both PRs at once.

(Context: auditing a cluster of concurrent buzz-acp reliability PRs from block/buzz#5888.)

jhgaylor and others added 4 commits August 17, 2026 22:45
…nel on session/new

A harness restart — a desktop relaunch, a config-change restart, or a hosted
harness moved between nodes on a deploy — emptied the in-memory channel →
session map, and the next mention in every channel got a fresh session/new.
For an agent whose session *is* its workspace (a sandbox per session, as with
the `fountain acp` gateway) that discarded the channel's memory and files each
time; the base prompt tells the agent to "resume silently after a session
restart", which assumes the opposite.

Two changes:

* `SessionStore`: a small JSON file (`<state-dir>/sessions-<pubkey>.json`,
  atomic writes) remembering channel → session id. Before opening a new
  session for a channel with no live one, the pool `session/load`s the
  remembered id when the agent advertised `loadSession` at initialize; any
  failure forgets the id and falls back to `session/new`, so a resume that
  cannot happen never costs the turn. Leaving a channel and `!rotate` forget
  the entry. `--state-dir` / `BUZZ_ACP_STATE_DIR` (default `<cwd>/.buzz-acp`),
  `--no-resume-sessions` / `BUZZ_ACP_NO_RESUME_SESSIONS` to opt out.

* `session/new` now carries `_meta.channelId` and `_meta.channelType`
  (`AcpClient::session_new_with_origin`; `session_new_full` is unchanged and
  delegates). The prompt names the channel only as text; an agent or gateway
  that wants to key sessions by channel — to resume the same conversation
  after this harness restarts even when its own state directory is not
  persistent — had nothing machine-readable to key on. Absent for heartbeat
  sessions.

Tests: store round-trip/malformed/disabled; client sends channelId/type and
merges into existing _meta; loadSession capability recorded; session/load
request shape and tolerance of the pre-response replay; error surfaces;
config defaults and opt-out. Not covered here: a scripted end-to-end through
the pool's get-or-create path (the pool has no scripted-agent test harness
today).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Jake Gaylor <jhgaylor@gmail.com>
…container volume note

Reviewers asked for the README configuration table to list the two settings
introduced with session resumption, and for an operator note that the default
<cwd>/.buzz-acp state dir is ephemeral in containers and must be pointed at
mounted persistent storage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Jake Gaylor <jhgaylor@gmail.com>
Desktop and mobile insert a mention into the message body as literal text
("@Fountain Maintainer !rotate") alongside the p tag, but
is_owner_control_command required content.trim() to equal the command
exactly. So a mentioned command fell through to the agent as a prompt, and
a bare command had no p tag and was dropped — the commands were unreachable
from every product surface (already noted in
docs/welcome-kickoff-silent-failures.md §5).

Match the command when it is the whole content, or when it is the last or
first token with only @name / nostr: mention text on the other side. Content
that continues past the command is still forwarded as an ordinary message.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Jake Gaylor <jhgaylor@gmail.com>
…ws !rotate

!rotate drops the harness's ACP session for a channel, and the next turn
sends session/new. But session/new carries _meta.channelId, and an agent or
gateway that keys its own state by channel — fountain acp's channel-bound
conversations — hands the same conversation straight back. On such an
agent !rotate was a no-op: observed on the hosted Fountain Maintainer,
where a !rotate logged as consumed and the next turn resumed the same
conversation.

The harness now remembers the rotate (SessionStore::request_fresh) and the
next session/new for that channel carries _meta.freshSession: true, which
the agent must honour by not resuming. The member is absent otherwise.

Also: an in-flight !rotate never removed the channel from the on-disk
session store (only the idle path did), so with a state dir the post-rotate
turn could session/load the rotated session. Both paths now forget it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Jake Gaylor <jhgaylor@gmail.com>
@jhgaylor
jhgaylor force-pushed the buzz-acp/rotate-requests-fresh-session branch from 6daa3f5 to 20f34e6 Compare August 18, 2026 02:47
@jhgaylor

Copy link
Copy Markdown
Author

Thanks @mfethe1 — rebased the whole stack (#6088#6101#6103#6104) onto current main. Resolved exactly as you described: kept both workflow_attributed_author and control_command_content_matches in crates/buzz-acp/src/lib.rs. buzz-acp fmt/clippy/tests are green locally and GitHub now reports all four as mergeable.

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