buzz-acp: resume channel sessions across a restart, and name the channel on session/new - #6088
buzz-acp: resume channel sessions across a restart, and name the channel on session/new#6088jhgaylor wants to merge 2 commits into
Conversation
…id, forwarded from ACP _meta (#775) A chat harness like buzz-acp keeps its channel→session map in memory and opens `session/new` again for every channel after a restart; through the `fountain acp` gateway that was a new conversation and a new sandbox per restart — every hosted deploy (#774). - `conversations.channel_id` (opaque, client-supplied). `POST /api/conversations` with `channel_id` resumes the latest live conversation for the same user + agent + vault + channel (200, `meta.resumed: true`) instead of opening a new one (201); terminated/failed ones are past resuming and a new one takes over the binding. Without `channel_id` nothing changes. - `fountain acp` reads `_meta.channelId` on `session/new`, forwards it, and hands the resumed conversation back as the session id. - The hosted buzz-acp is built from jhgaylor/buzz@83eda69 (block/buzz#6088: `_meta.channelId` on session/new + session store) via a new optional `buzz-acp.source` the publish workflow honours; release name buzz-acp-v0.5.14-fountain.1. Delete the file and repin to an upstream tag once it merges. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
Operator +1 on this. We run a fleet of single tenant Buzz relays (hachiflow) with managed agents supervised by Buzz Desktop, and harness restarts are not an edge case at that layer, they are routine ops: config changes, desktop relaunches, host reboots, moving a harness between nodes. In one four day stretch our telemetry showed three agents restarting in lockstep seven times, and channel sessions averaging barely more than one turn before being discarded. Every one of those restarts was an agent re-learning its channel from a small context window because the channel to session map lived only in process memory. The forget-and-fall-back rule is the right shape: a resume that cannot happen never costs the turn, so the worst case is exactly today's behavior, and the best case removes the single biggest source of agent amnesia we see in practice. The _meta.channelId addition quietly matters for hosted harnesses too. In a container or pod the state dir is ephemeral unless someone thinks to mount it, so a gateway that keys sessions by (agent pubkey, channelId) in its own store can resume across a node move even when sessions-.json did not survive. One small ask from the containerized side: a README note that BUZZ_ACP_STATE_DIR should point at a mounted volume when the harness does not live on a laptop, since the /.buzz-acp default will silently vanish on every deploy otherwise. Would love to see this land. |
|
Tested the current head Live restart result
That verifies the production Automated verification
Separate existing reply-path blockerThe first live turn also reproduced the known Hermes/Buzz publication gap: Hermes attempted the correct Two remaining author actions before merge:
|
…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>
83eda69 to
af535a6
Compare
|
Thanks both — addressed in
@ScaleLeanChris thanks for the end-to-end run against real Hermes. I was using Openclaw so that's more data |
…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>
9d5c53c to
9390060
Compare
|
(Re-signed the docs commit — now |
…ock/buzz#6088) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…heck and the sync (#782) * chore(buzz-acp): repin the fork to the DCO-signed commit of block/buzz#6088 The PR's single commit was amended with Signed-off-by to satisfy Block's DCO check (83eda69a -> af535a64, identical tree). The old sha is now unreachable from any branch on the fork, so the pin follows it before GitHub can GC it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(buzz-acp): follow the fork to 9390060 (README docs commit on block/buzz#6088) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(acp): settle/1 tolerates a peer that stopped between the alive check and the sync The server stops the peer the moment it reports {:done, _}; :sys.get_state on it can hit noproc. That is the settled state, not a failure — it flaked partition 2 on 2026-08-17. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Problem
A harness restart — a desktop relaunch, a config-change restart, or a hosted harness moved between nodes on a deploy — empties the in-memory channel → session map (
SessionState.sessions), and the next mention in every channel gets a freshsession/new. For an agent whose session is its workspace (a sandbox per session, as with thefountain acpgateway) that discards the channel's memory and files every time. The base prompt tells the agent to "resume silently after a session restart", which assumes the opposite.Change
1.
SessionStore+session/loadon restart. A small JSON file (<state-dir>/sessions-<pubkey>.json, atomic writes) remembers channel → session id. Before opening a new session for a channel with no live one, the poolsession/loads the remembered id when the agent advertisedloadSessionatinitialize. Any failure (agent can't load, session gone, error) forgets the id and falls back tosession/new— a resume that cannot happen never costs the turn. Leaving a channel and!rotateforget the entry. Resumed sessions get fresh delivery state and no zero-usage baseline (their prior usage isn't zero).--state-dir/BUZZ_ACP_STATE_DIR(default<cwd>/.buzz-acp)--no-resume-sessions/BUZZ_ACP_NO_RESUME_SESSIONSto opt out (previous behavior)2.
_meta.channelId/_meta.channelTypeonsession/new(AcpClient::session_new_with_origin;session_new_fullunchanged, delegates withNone). The prompt names the channel only as text in[Context];sessionTitlecarries the channel name and is empty for DMs. 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 isn't persistent — had nothing machine-readable to key on. Absent for heartbeat sessions.Tests
session_store: round-trip across reopen, remove, malformed file treated as empty, disabled store.acp:channelId/channelTypeland in_metaand merge withsessionTitle;session_new_fullsends no origin;loadSessioncapability recorded / left false;session/loadrequest shape and tolerance of the pre-responsesession/updatereplay; error surfaces asErr.config: state-dir default, explicit, and opt-out.cargo test --lib788/0,cargo fmt --check,cargo clippy --all-targetsclean.Not covered: a scripted end-to-end through the pool's get-or-create path — the pool has no scripted-agent test harness today; happy to add one if you'd like it as part of this.
Compatibility
Default-on, but strictly additive on the wire: agents that don't advertise
loadSessionnever seesession/load;_metamembers are ignored by agents that don't read them (assessionTitlealready is).Related work
Fixes #5342 (channel → session map is in-memory only; every restart opens
session/newand, on Codex, a new cloud thread).Two open PRs also add a
session_store.rs; naming them up front:session/loadbehindloadSession— same shape as part 1 heremainand fork CI never ran there. This one is smaller, default-on with--no-resume-sessions, keyed per agent pubkey, and adds part 2 (_meta.channelId/channelType). If #2633 lands first I'll drop the store and rebase part 2 onto it.buzz-acp-only slice with tests.Adjacent, not overlapping: #4480 (
session/resumefor live MCP swaps on thesame session id — argues resume over load to dodge the 60s replay timeout on
long histories; worth weighing as the resume verb here), #6045 (durable
pending-event journal across restart — complementary; happy to align the
state-dir convention if it lands first), #4721 (identity receipts, explicitly
excludes restart continuity).
Points raised in #2633's review that apply here as well, and where this PR
stands on them:
send_requestemitsevery inbound line as an
acp_readobserver event before dispatching it, sothe
session/updates an agent replays duringsession/loadreach theowner-only observer feed (desktop Activity) as though they were fresh output.
They never become channel messages (
handle_session_updateonly logs). Fixis to suppress observer emission for
session/updatewhile asession/loadis outstanding, as feat(acp): Hermes runtime discovery and durable session load #2633 does; flagging rather than hiding it — happy to add
it to this PR if reviewers want it here rather than as a follow-up.
(definitive → forget) from timeout/agent-exit (indeterminate → keep). This
PR forgets on any
Err. Simpler, and the cost of being wrong is one extrasession/new— but I'm happy to adopt the split if preferred.key and state dir would clobber each other. Atomic writes only, no lock, on
the assumption that one pubkey = one harness process. Say the word if that
doesn't hold for hosted deployments.
🤖 Generated with Claude Code