Skip to content

buzz-acp: resume channel sessions across a restart, and name the channel on session/new - #6088

Open
jhgaylor wants to merge 2 commits into
block:mainfrom
jhgaylor:buzz-acp/resume-sessions-across-restart
Open

buzz-acp: resume channel sessions across a restart, and name the channel on session/new#6088
jhgaylor wants to merge 2 commits into
block:mainfrom
jhgaylor:buzz-acp/resume-sessions-across-restart

Conversation

@jhgaylor

@jhgaylor jhgaylor commented Aug 17, 2026

Copy link
Copy Markdown

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 fresh session/new. For an agent whose session is its workspace (a sandbox per session, as with the fountain acp gateway) 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/load on 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 pool session/loads the remembered id when the agent advertised loadSession at initialize. Any failure (agent can't load, session gone, error) forgets the id and falls back to session/new — a resume that cannot happen never costs the turn. Leaving a channel and !rotate forget 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_SESSIONS to opt out (previous behavior)

2. _meta.channelId / _meta.channelType on session/new (AcpClient::session_new_with_origin; session_new_full unchanged, delegates with None). The prompt names the channel only as text in [Context]; sessionTitle carries 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/channelType land in _meta and merge with sessionTitle; session_new_full sends no origin; loadSession capability recorded / left false; session/load request shape and tolerance of the pre-response session/update replay; error surfaces as Err.
  • config: state-dir default, explicit, and opt-out.
  • cargo test --lib 788/0, cargo fmt --check, cargo clippy --all-targets clean.

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 loadSession never see session/load; _meta members are ignored by agents that don't read them (as sessionTitle already is).

Related work

Fixes #5342 (channel → session map is in-memory only; every restart opens
session/new and, on Codex, a new cloud thread).

Two open PRs also add a session_store.rs; naming them up front:

# Overlap How this differs
#2633 Durable channel→session map + session/load behind loadSession — same shape as part 1 here Rescoped from a Hermes PR; keyed by (agent command, args); currently in conflict with main and 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.
#5154 "durable ACP continuity" is one item in a 36-file desktop+acp omnibus This is the buzz-acp-only slice with tests.

Adjacent, not overlapping: #4480 (session/resume for live MCP swaps on the
same 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:

  • Replayed history and the observer feed — known gap. send_request emits
    every inbound line as an acp_read observer event before dispatching it, so
    the session/updates an agent replays during session/load reach the
    owner-only observer feed (desktop Activity) as though they were fresh output.
    They never become channel messages (handle_session_update only logs). Fix
    is to suppress observer emission for session/update while a session/load
    is 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.
  • Indeterminate load failures. feat(acp): Hermes runtime discovery and durable session load #2633 distinguishes a JSON-RPC error
    (definitive → forget) from timeout/agent-exit (indeterminate → keep). This
    PR forgets on any Err. Simpler, and the cost of being wrong is one extra
    session/new — but I'm happy to adopt the split if preferred.
  • Cross-process locking. The store is per pubkey; two harnesses on the same
    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

@jhgaylor
jhgaylor requested a review from a team as a code owner August 17, 2026 00:07
jhgaylor added a commit to BinaryBourbon/fountain that referenced this pull request Aug 17, 2026
…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>
@akeemjenkins

Copy link
Copy Markdown

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.

@ScaleLeanChris

Copy link
Copy Markdown

Tested the current head 83eda69a9 end to end on macOS arm64 with a local relay and Hermes Agent ACP v0.20.1.

Live restart result

  1. Created a fresh private stream and fresh sender/agent identities.
  2. Started this PR's buzz-acp against the real hermes-acp binary.
  3. Sent a first mention containing a private codeword. Buzz opened Hermes session 16963d89-2fda-422b-a08a-dfbc474ff7c1, completed one turn, and persisted that exact channel → session mapping.
  4. Terminated the entire buzz-acp process, then started a new one with the same identity and state directory.
  5. Sent a second mention that did not contain the codeword.
  6. The new harness loaded one remembered mapping and issued session/load for the same session ID. Hermes logged Loaded session 16963d89-2fda-422b-a08a-dfbc474ff7c1; this PR logged exactly one session resumed event; the second process completed exactly one agent turn.
  7. Hermes' persisted session contained two user turns and answered CODEWORD:SILVERBEE after restart.

That verifies the production buzz-acp → real Hermes ACP session/new → persisted map → full process restart → session/load path, not just the store/helper tests.

Automated verification

  • cargo fmt --all -- --check: passed
  • cargo clippy -p buzz-acp --all-targets -- -D warnings: passed
  • cargo test -p buzz-acp --lib: 788 passed, 0 failed

Separate existing reply-path blocker

The first live turn also reproduced the known Hermes/Buzz publication gap: Hermes attempted the correct buzz messages send, but its terminal child did not receive BUZZ_PRIVATE_KEY and the CLI returned auth exit 3. That is tracked in #4923 and NousResearch/hermes-agent#78026; it is independent of this PR's session-resume behavior. The ACP response itself completed normally, which is what allowed the restart/resume assertion above.

Two remaining author actions before merge:

  • DCO Check is currently failing for the PR's commit.
  • Please add the requested README/operator note that BUZZ_ACP_STATE_DIR must point at mounted persistent storage in containers. The CLI help explains it, but the crates/buzz-acp/README.md configuration table does not yet list either new setting.

…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>
@jhgaylor
jhgaylor force-pushed the buzz-acp/resume-sessions-across-restart branch from 83eda69 to af535a6 Compare August 17, 2026 04:08
@jhgaylor

jhgaylor commented Aug 17, 2026

Copy link
Copy Markdown
Author

Thanks both — addressed in 9d5c53c:

  • DCO: the original commit is re-signed (af535a6), and the new one carries the trailer.
  • README: added a Session Persistence table under Configuration listing --state-dir / BUZZ_ACP_STATE_DIR and --no-resume-sessions / BUZZ_ACP_NO_RESUME_SESSIONS, the forget-and-fall-back rule, and the operator callout @akeemjenkins asked for: the <cwd>/.buzz-acp default is ephemeral in containers, so point BUZZ_ACP_STATE_DIR at mounted persistent storage (and gateways can key on _meta.channelId in their own store to survive a node move). Also a one-line Restart step in How It Works.

@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>
@jhgaylor
jhgaylor force-pushed the buzz-acp/resume-sessions-across-restart branch from 9d5c53c to 9390060 Compare August 17, 2026 04:12
@jhgaylor

Copy link
Copy Markdown
Author

(Re-signed the docs commit — now 9390060; DCO is green.)

jhgaylor added a commit to BinaryBourbon/fountain that referenced this pull request Aug 17, 2026
jhgaylor added a commit to BinaryBourbon/fountain that referenced this pull request Aug 17, 2026
…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>
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.

[Bug] buzz-acp loses channel sessions on restart and creates duplicate Codex cloud threads

3 participants