Skip to content

[Workspace] system-prompt anchor never reaches protocol-v1 harnesses: Claude Code and Codex lose absolute-cwd grounding on all platforms #3126

Description

@kcao-gss

Describe the bug

The [Workspace] system-prompt section never reaches protocol-v1 (legacy-framing) harnesses on any platform. Because upstream ACP's current protocol version is 1, that includes the shipping Claude Code and Codex adapters — so most real Buzz agent runs lose the absolute-cwd grounding, not an edge case.

workspace_section() (pool.rs:1165-1177) has exactly one producer, framed_system_prompt (pool.rs:1137-1158), whose output is consumed by two routes only:

  1. pool.rs:828-839session/new, wrapped in session_new_system_prompt (pool.rs:185-195):
    if is_goose || protocol_version < 2 { None } else { prompt }
    For a v1 agent the combined prompt is built and then discarded.
  2. pool.rs:841-858 — Goose's session_set_goose_system_prompt extension, which is version-independent, so Goose is fine.

The legacy path assembles its own prompt and structurally cannot render the section: prepend_base_for_legacy (pool.rs:1090-1101, called at pool.rs:1593 initial message and pool.rs:1731 heartbeat) emits [Base] and [Channel Canvas] only, and takes no cwd argument. Per-turn assembly in format_prompt (queue.rs:1429-1443) is the same, and FormatPromptArgs (queue.rs:1352-1375) has no cwd field at all.

Why protocol-v1 is the live path, not a relic. Buzz requests protocolVersion: 2 (acp.rs:126) and stores the response, defaulting to 1 when absent (lib.rs:3775-3776, lib.rs:3864: unwrap_or(1)). The v2 request is self-described as non-standard — acp.rs:540-541:

// Requesting version 2 is an intentional temporary pin — we are squatting on ACP v2 ahead of the upstream ACP RFD. Revisit when that RFD merges.

and again in buzz-agent/src/lib.rs:280-283. Upstream ACP is at version 1, and the spec requires an agent that does not support the requested version to answer with the latest it does support. So a spec-compliant upstream adapter answers 1.

Against KNOWN_ACP_RUNTIMES (discovery.rs:66-198):

Runtime Gets [Workspace]? Why
Claude Code (claude-agent-acp, discovery.rs:101-132) No negotiates v1 → has_system_prompt_support false (pool.rs:173-183) → legacy framing
Codex (codex-acp, discovery.rs:133-165) No same
Goose (discovery.rs:67-100) Yes version-independent extension (pool.rs:845); lost only if the probe caches Some(false) (pool.rs:849-855)
Buzz Agent (discovery.rs:166-197) Yes pins PROTOCOL_VERSION = 2 (buzz-agent/src/config.rs:3), min(requested, 2) (buzz-agent/src/lib.rs:284)

Bounding the impact honestly: [Base] is still delivered on the legacy path — queue.rs:1431 per turn, pool.rs:1593 initial, pool.rs:1731 heartbeat — and it is default-on (lib.rs:1539-1544, opt out only via --no-base-prompt / BUZZ_ACP_NO_BASE_PROMPT, config.rs:409-410). So base_prompt.md:84-100's Workspace Layout table, REPOS/ and OUTBOX/ included, plus the "never run find over $HOME" line at :100, do arrive. What is lost is only the absolute-path anchor ("Your absolute working directory is <cwd>... this is where you already are"). Same shape as #3124, much wider blast radius. Expected symptom is a probabilistic uptick in path-guessing and wasted exploration, not a functional failure.

Steps to reproduce

Code-level, no harness needed:

$ rg -n 'cwd' crates/buzz-acp/src/queue.rs | rg -n 'FormatPromptArgs' -A2
# FormatPromptArgs has no cwd field -> the per-turn legacy path cannot render [Workspace]

workspace_section has one caller (pool.rs:1154), reachable only from create_session_and_apply_model (pool.rs:820); neither legacy assembly point calls it.

End-to-end: run a managed Claude Code or Codex agent on any platform and inspect the prompt the harness receives (desktop transcript observer, or the harness log). Expect [Base] with no [Workspace] block. Compare against a buzz-agent agent on the same host, which does get it.

Expected behavior

Protocol-v1 harnesses receive the same workspace anchor as v2 harnesses.

Version and platform

Logs / additional context

Origin — an omission, not a decision. workspace_section came in with 1011cea2 ("fix(desktop): ground agent workspace, migrate legacy nest, configurable repos_dir", #1194, 2026-06-23) — the only commit git log -S workspace_section -- crates/buzz-acp/ returns. Inside the crate it touched only pool.rs and base_prompt.md; it did not touch queue.rs, and within pool.rs it did not touch prepend_base_for_legacy — the diff sat in framed_system_prompt, then inside an explicit if agent.protocol_version >= 2 { … } else { … }, and the legacy arm was left alone. All seven tests added target framed_system_prompt/workspace_section in isolation. The doc comment added at pool.rs:1151-1153 reasons about persona-only agents and the / fallback but never mentions protocol version. prepend_base_for_legacy predates it (f0858824, #981, 2026-06-12; later 73cc31cc #1277, 3b9b371f #1755, 64b8fea6 #2034) and no commit in that set considers [Workspace]. (Inference from diffs and messages: never added, rather than consciously excluded.)

Fix shape — three insertion points, one shared renderer:

  1. queue.rs:1352-1375 — add cwd: Option<&'a str> to FormatPromptArgs; pool.rs:1771-1784 already has ctx.cwd in scope.
  2. queue.rs:1429-1432 — push the workspace section ahead of base_section(bp), matching framed_system_prompt's ordering.
  3. pool.rs:1090-1101 — add a cwd parameter for the initial-message (pool.rs:1593) and heartbeat (pool.rs:1731) call sites. Missing the heartbeat site causes exactly the drift the pool.rs:1088-1089 doc comment says the helper exists to prevent.

workspace_section is private in pool.rs and would need to move to a shared module or be re-exported.

Risks worth weighing before doing it:

  • Per-turn re-injection is realformat_prompt runs on every batch flush. But [Base] (~150 lines) plus [System], [Team Instructions], [Agent Memory — core], and [Channel Canvas] are already re-sent every legacy turn. [Workspace] is one ~70-word paragraph, well under 1% of existing per-turn overhead.
  • Preserve the Some(base_prompt) coupling. framed_system_prompt emits [Workspace] only when a base prompt exists (pool.rs:1154-1157); a persona-only legacy agent must not get an anchor with no layout to go with it — the case pool.rs:1151-1153 warns about.
  • MAX_SYSTEM_PROMPT_BYTES does not apply (the legacy path uses no system prompt). The observer size trimmer (queue.rs:1396-1402) handles a new section gracefully, but it adds a leaf to the desktop "Prompt context" panel — worth a visual check.

Relationship to #3124 — independent, neither subsumes the other. #3124 fixes whether the renderer returns Some (the POSIX-only predicate), which helps callers that already invoke it. This issue is that the legacy path never invokes it. A Windows Claude Code agent is broken for both reasons; a macOS Claude Code agent for this one alone. They could land in one PR — the natural implementation here is "call the predicate-fixed workspace_section from the legacy assembly points" — but closing one must not close the other.

Open question for maintainers: given the acp.rs:540-541 note about squatting on v2 ahead of the upstream RFD, is the intended long-term answer to fix the legacy path, or to converge on upstream versioning so the v1/v2 split disappears? That choice decides whether this is worth fixing directly or is better absorbed into the RFD follow-up.

Duplicate search: searched issues for "workspace prompt", "OUTBOX", and PRs for "base_prompt workspace" — none found. Nearest is #2334 (base prompt shown as session title in harness UIs), a display concern.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions