Skip to content

fix(buzz-acp): deliver standing context once per session; use claude-agent-acp's system prompt route - #4183

Open
sumit-m wants to merge 3 commits into
block:mainfrom
sumit-m:acp-v1-standing-context
Open

fix(buzz-acp): deliver standing context once per session; use claude-agent-acp's system prompt route#4183
sumit-m wants to merge 3 commits into
block:mainfrom
sumit-m:acp-v1-standing-context

Conversation

@sumit-m

@sumit-m sumit-m commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Problem

Agents whose adapter reports protocolVersion: 1 — Claude and Codex — have no system role, so base_prompt, the persona, team instructions, core memory and the canvas ride in the user message. format_prompt re-sent all of it on every turn.

That is roughly 9 KB of base_prompt.md alone, repeated per turn, plus persona and core. It makes the standing framing both the largest and the most recent text in the window, so it outweighs the conversation it exists to frame and evicts real channel history sooner. Related: #3126, #3242.

Change

1. Deliver standing context once per session

New queue::StandingContext holds base prompt, persona, team instructions, core and canvas, and renders them in one place. Both legacy dispatch paths — the initial message and the batch flush — go through it, so their section set and ordering cannot drift apart.

run_prompt_task tracks delivery with a standing_context_sent flag derived from is_new_session, and format_prompt gates on it. is_new_session comes from the session registry, which is cleared on every invalidation path, so a replacement session re-delivers rather than leaving the agent unbriefed.

2. Give claude-agent-acp a real system prompt

systemPrompt on session/new is not part of ACP — it appears in neither the v1 nor the v2 schema — so gating it on protocol_version >= 2 is a proxy rather than a capability check. has_system_prompt_support already had to special-case goose for exactly this reason.

claude-agent-acp does accept a system prompt, via _meta.systemPrompt, and has since v0.6.0 (2025-10-15, agentclientprotocol/claude-agent-acp#91). The object form merges over the adapter's own claude_code preset with type/preset locked, so { append } extends Claude Code's built-in prompt rather than replacing it — the same semantics as goose's mode: "append".

standing_delivery() replaces the version proxy with a per-adapter decision, consulted by both session/new construction and format_prompt so the two cannot disagree:

Adapter Route
buzz-agent top-level systemPrompt param
claude-agent-acp >= 0.6.0 _meta.systemPrompt = { append }
goose, after a successful probe its custom method
anything else user message, once per session

No version probe was needed. The adapter reports agentInfo.version in the initialize response the harness already makes and already parses for .name, so this is one extra field read rather than a subprocess spawn with its own timeout and failure mode. An absent or unparseable version is treated as unsupported: an unrecognised _meta key is silently ignored, and the failure mode there is an agent running with no Buzz instructions at all.

3. Show the delivered prompt in the activity log

The transcript built its "System prompt" card from the top-level params.systemPrompt only, so an agent taking the _meta route would have rendered no card and its standing context would have been invisible. A new sessionNewSystemPrompt helper reads either route and feeds both through the existing parseSystemPromptSections, so one card renders whichever delivered it.

Behaviour changes worth calling out

  • The legacy initial message now carries the whole block, not just [Base] and the canvas. Currently a legacy agent takes its first action with no persona and no memory.
  • Its section order changed: the canvas was prepended last and so appeared first; it now matches the per-turn order (base, system, team, core, canvas).
  • Heartbeats are gated the same way — only the first tick of a heartbeat session carries [Base]. This is the most separable part of the change; happy to drop it if you would rather keep heartbeats as they are.
  • prepend_canvas_for_legacy is removed and prepend_base_for_legacy becomes prepend_standing_for_legacy. Its doc comment already noted that the gate lived in one place "so the heartbeat and initial-message dispatch paths can't drift apart again"; this extends that to the section set itself.
  • Goose is unaffected. Its extension is version-independent and still probed, so has_system_prompt_support short-circuits before the new gate is consulted.

initialize still requests protocolVersion: 2, and the in-tree comment describing that as squatting ahead of the ACP RFD is unchanged. This only stops that number from standing in for a capability it does not describe.

Codex

codex-acp reports protocolVersion: 1 too and would benefit from the same treatment if it exposes an equivalent route. We have no access to Codex models, so we could not check the adapter's behaviour or test a change, and have left it on the user-message path — which this PR at least makes considerably cheaper.

Verification

Tested live on Windows against a local relay, both paths in order:

  1. User-message path — turn 1 of a new session carried [Base], [System] and [Agent Memory — core]; turn 2 opened at [Context] with none of them.
  2. _meta path — a new session showed no standing sections in the user message at all, and the agent then reproduced base_prompt.md's mentions guidance verbatim (the @Will Pfleger example, partial names failing silently, no formatting on mentions, --mention repeatable, the CLI stopping before send on a non-member), stating it was replying from memory.

cargo test -p buzz-acp --lib — 667 passing. The three failures on this machine are pre-existing on main: keepalive_resets_idle_past_deadline (timing) and two steer tests that fail on Windows because of an unquoted temp path (#3733).

pnpm test 3886 passing, pnpm typecheck and biome check clean.

New coverage: exactly-once delivery across turns, the version floor and both published adapter package names, the wire shape of _meta.systemPrompt asserted on the bytes sent, agentInfo.version parsing, and the activity-log card rendering identically from either route.

sumit-m added 3 commits August 1, 2026 18:53
Protocol-v1 agents received base prompt, persona, team instructions, core
memory and canvas on every turn. Deliver them in the session's first message
only; both legacy paths now render through a shared StandingContext.

Signed-off-by: sumit-m <33051892+sumit-m@users.noreply.github.com>
The adapter has read _meta.systemPrompt since v0.6.0 and reports its version in
initialize, so the capability is knowable without a probe. Replaces the
protocol-version proxy with a per-adapter delivery decision; anything below the
floor falls back to the user-message path.

Signed-off-by: sumit-m <33051892+sumit-m@users.noreply.github.com>
…ity log

The transcript read only session/new's top-level systemPrompt, so agents whose
adapter takes it through _meta.systemPrompt rendered no System prompt card and
their standing context was invisible.

Signed-off-by: sumit-m <33051892+sumit-m@users.noreply.github.com>
@sumit-m
sumit-m requested a review from a team as a code owner August 1, 2026 21:37
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.

1 participant