buzz-acp: tell the agent to start fresh on the session/new that follows !rotate - #6103
buzz-acp: tell the agent to start fresh on the session/new that follows !rotate#6103jhgaylor wants to merge 4 commits into
Conversation
|
Same conflict as #6104 — this branch also collides with (Context: auditing a cluster of concurrent |
…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>
6daa3f5 to
20f34e6
Compare
|
Thanks @mfethe1 — rebased the whole stack (#6088 → #6101 → #6103 → #6104) onto current |
Summary
!rotatedrops the harness's ACP session for a channel, and the next turn sendssession/new. Butsession/newcarries_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!rotateis a no-op.Observed on a hosted harness:
!rotatelogged 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).!rotatepaths (idle and in-flight) callrequest_freshandsession_store.removeup front.session/newfor that channel carries_meta.freshSession: true(SessionOrigin.fresh); the member is absent otherwise. Consumed on that one call.Also fixes a latent bug from #6088: an in-flight
!rotatenever removed the channel from the on-disk session store (only the idle path did), so withBUZZ_ACP_STATE_DIRset, the post-rotate turn couldsession/loadthe 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_metaasserts the member is absent when not rotatedsession_store::fresh_request_is_consumed_once_and_works_when_disabled(new)cargo test -p buzz-acp --lib— 791 passed; clippy + fmt clean@agent !rotateon Fountain → next mention lands in a new conversation🤖 Generated with Claude Code