Commit Graph

154 Commits

Author SHA1 Message Date
Will Pfleger
5a4b2d3dcc fix(desktop): show readable Claude Code model names and descriptions (#8053)
Claude Code agents' model dropdown showed raw IDs (`opus[1m]`,
`claude-fable-5-1[1m]`, `haiku`) and its effort choices showed
`low`/`xhigh`. Claude Code's ACP adapter labels config options and
choices with `name`, but Buzz read `displayName`, which no adapter
sends.

**Adapter labels and descriptions**
- `normalize_agent_models` reads `name`, keeps each model's
`description`, and uses the model option's `currentValue` as the agent's
default model (ahead of the unstable `currentModelId`).
- `parse_config_options` / `parse_option_values` read `name` for option
and choice labels, so effort shows "Low"/"Xhigh".
- The create (`PersonaModelCombobox`) and edit (`PersonaDropdownField`)
model pickers show the description as a muted second line, through both
option builders (`modelDropdownOptions` and
`buildTemplateModelDropdownOptions`).

**Generated names for full Claude model IDs**
- Claude Code's own names drop versions ("Sonnet" for
`claude-sonnet-5`). The desktop's Databricks label grammar already
parses a bare Claude ID once its wrapper is removed, so that body parse
is exposed as `generateBareModelLabel`; Databricks entry points and
outputs are unchanged.
- For the `claude` runtime, `claudeCodeModelLabel` formats full IDs and
keeps a `[1m]` suffix as "(1M context)". Short names (`opus`, `haiku`,
`opus[1m]`), unknown bracket suffixes, and IDs containing any non-ASCII
character keep the adapter's name.
- This applies to the model dropdowns in the create and edit dialogs and
the Agent defaults editor. The "Default model (…)" row appears in the
edit dialog and the Agent defaults editor; the create dialog still
requires an explicit model and shows no Default row. Model labels on
agent cards are unchanged.
- In the edit dialog, the Default row keeps the discovered label (e.g.
"Default model (Opus)") unless a global default model is set, in which
case it shows that model. When the adapter's default is its own
`default` entry (e.g. "Default (recommended)"), the row reads plain
"Default model" and keeps that entry's description.

**`models` debug command**
- The harness's readable `models` output reads option and choice labels
from `name` and the option id from `id` (falling back to `configId`), so
it no longer prints raw IDs for Claude Code. JSON output is unchanged.

| Model ID | Before | After |
|---|---|---|
| `opus[1m]` | `opus[1m]` | Opus · Opus with 1M context · … |
| `claude-fable-5-1[1m]` | `claude-fable-5-1[1m]` | Claude Fable 5.1 (1M
context) · Fable 5.1 · … |
| `claude-sonnet-5` | `claude-sonnet-5` | Claude Sonnet 5 · Sonnet 5 · …
|
| `claude-sonnet-4-6` | `claude-sonnet-4-6` | Claude Sonnet 4.6 ·
Previous Sonnet version |
| `claude-sonnet-4-6[1m]` | `claude-sonnet-4-6[1m]` | Claude Sonnet 4.6
(1M context) · Sonnet 4.6 for long sessions · … |
| `haiku` | `haiku` | Haiku · Haiku 4.5 · … |

🤖

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-10-02 19:28:59 +00:00
Will Pfleger
cc528dbe05 feat(agents): request summarized Claude thinking and label empty rows (#8051)
Claude Code agents on Opus showed Thinking rows that opened to nothing.
Opus returns no readable thinking unless Claude Code is asked for
summaries, and Buzz never asked. Sonnet returns readable thinking either
way.

**Request summaries from Claude Code.** `session_new_full` now adds
`_meta.claudeCode.options.extraArgs: {"thinking-display": "summarized"}`
for the Claude adapter. claude-agent-acp spreads `extraArgs` into the
CLI's arguments; 0.36.1, 0.60.0 and 0.66.0 all read the same shape. The
value is merged into the existing `_meta`, so `systemPrompt` and
`sessionTitle` are kept. `session_new_full` is the only place Buzz sends
`session/new`, so agent sessions and model discovery both get it.

**Only for a CLI that accepts the flag.** `--thinking-display` first
shipped in Claude Code 2.1.94, and older CLIs exit on unknown flags,
which would fail every session start. When the adapter is spawned, Buzz
runs `$CLAUDE_CODE_EXECUTABLE --version` once with a 5-second limit,
reading at most 256 bytes of output and killing its whole process group
on timeout, and sends the flag only on 2.1.94 or newer. The flag is left
off, and sessions start exactly as before, when:

- the CLI is older than 2.1.94;
- `CLAUDE_CODE_EXECUTABLE` is unset (the adapter then runs its own
bundled CLI, which Buzz can't inspect) or empty;
- the binary hangs, fails, or prints anything other than a clean
`major.minor.patch` version;
- `BUZZ_ACP_LAUNCH_PREFIX` is set, since the wrapper can change which
CLI the adapter actually runs.

 The result is logged once at debug level.

**Label rows that still have no text.** A Thinking row whose text is
empty or only whitespace keeps its header and shows a muted "No readable
reasoning tokens", with nothing to expand. This covers models or CLIs
that still return no readable thinking. If text arrives later in the
same row, it renders as an expandable section as before.
`ThoughtActivity` is the only component that draws Thinking rows.

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Hayt <211b96e6a2b7f45fd4047988976c7bbbeeda0c15f3ae7b32eec20834b5a55118@buzz.block.builderlab.xyz>
2026-10-02 18:46:47 +00:00
Logan Johnson
133fb98692 fix(acp): steer running turns with edited mentions (#6132)
🤖
## Summary
Some agents can take a new message into a turn that is already running.
This is called "native steering." Before this PR, an edit that arrived
during a turn always cancelled the turn and started it again. Now the
edit is steered into the running turn, like any other new message. The
steered edit still points at the original message and its thread, so the
agent replies in the right place.

- The steer text carries the edit's route: `Edit of: …`, plus the
original's thread root.
- **Steer only within the same reply thread.** A native steer keeps the
running turn's `<context>`, so the reply goes where that `<context>`
points. The queue now records each running turn's reply thread. A
message is steered natively only when it replies in that same thread.
Under channel sessions one session spans threads, so an edit for another
thread (or another top-level message) takes the cancel-and-restart path.
Its re-prompt carries the message's own full `<context>`, with channel,
project, and profile data. The same rule applies to ordinary steered
messages, which had the same gap before this PR.
- **Per-thread sessions:** the edit steers only a turn running in the
same session as its original message. An edit in thread A is never added
to a turn running in thread B. If no turn is running in that session,
the edit waits in the queue as its own item.
- **Channel sessions** (the default): the channel has one session. A
running turn receives the edit natively only when the edit replies in
that turn's thread.
- If native steering is not available, or the transport fails or the
agent rejects the steer, the existing cancel-and-restart fallback runs.
If the agent returns another error, or the turn has already finished,
the edit goes back to the queue for normal delivery. In every case the
queued edit keeps its route.

## Details
- #4741 looks up the original message once, before the edit is queued.
This PR passes that stored route to the existing steer path, and uses
the same session the edit was queued under. No new lookup or preparation
step is needed.
- Removal, re-add, and fallback all use the existing lifecycle for
withheld steers.

### Stack
**3 of 3**. #6131 is merged; this PR is now based on `main`.

### Related issue
Part of #2540 (fixed by #6131). This PR adds the running-turn behavior.

### Testing
- `cargo test -p buzz-acp --all-targets` passes. The one test that fails
when the git setting `nostr.keyfile` is set is described in #4741.
- Tests check that the steer text carries the original's route, and that
the listener gives the steer path the same route and session it queues.
- End-to-end tests send a routed edit through the listener's steer
decision under channel sessions. An edit in the running turn's thread,
and an edit of the running top-level message, steer natively with no
cancel. An edit for thread B during a thread-A turn, and an edit of
another top-level message, send no native steer and take cancel+merge.
Removing the same-thread check makes both cross-thread tests fail.

---------

Signed-off-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
2026-10-02 13:24:22 -04:00
Logan Johnson
47654215eb fix(acp): wake agents for mentions added by edits (#6131)
🤖
## Summary
Today an agent does not wake up when someone edits an existing message
to @mention it. With this PR, adding the mention in an edit is enough to
reach the agent. The agent then works on the original message, as #4741
set up.

- Agents now receive message edits by default. Before, they subscribed
only to new messages (and a few other event types).
- The desktop app puts a mention tag on an edit only for people whom
that edit newly mentions. So an agent wakes once, for the new mention.
It does not wake again when the same message is edited later. This PR
also fixes one case in the desktop app: when the original also mentioned
someone whose profile was not loaded, every mention counted as new, so a
typo fix woke the agent again. Now the recipients that the app knows
from the original always count as already notified.
- This default applies in normal mode, to channels the agent joins while
it is running, and in setup mode.
- If you give an explicit `--kinds` list, it is used without change.
- Sessions do not change in this PR. An edit uses the session that #4741
chooses: with per-thread sessions, the original message's thread; with
channel sessions, the channel.

## Details
- The change adds the edit event kind (40003) to one shared default
list. The startup rules, the relay subscription filters, and the setup
rules all use that list.
- An agent accepts an event only if both the relay subscription and its
local rules allow it. The normal listener's startup rules now come from
one function, `startup_subscription_rules`. The harness and the tests
both call it, so the tests check the rules that actually run.

### Stack
**2 of 3**, based on #4741. Next: #6132.

### Related issue
Fixes #2540.

### Testing
- `cargo test -p buzz-acp --all-targets` passes. The one test that fails
when the git setting `nostr.keyfile` is set is described in #4741.
- Admission tests run on the rules the harness installs, for the normal
listener and for setup mode. An edit that mentions the agent is
accepted. An edit that does not mention it is rejected.
- I removed kind 40003 from each of the two defaults, one at a time.
Each time, these tests failed.
- Config tests check that edits are in the default kinds for startup
channels and for channels joined later.
- Desktop regression: `@Agent @Missing User please chek` edited to
`check`, with only the agent's profile loaded, sends no new
notification. It fails on the old code.

---------

Signed-off-by: npub1em3jmyn4vu57urqf03txrwreccvejvwdy5c4er8nnrwt7rc4tncscs3ssu <cee32d92756729ee0c097c5661b879c6199931cd25315c8cf398dcbf0f155cf1@buzz.block.builderlab.xyz>
Signed-off-by: Larry <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz>
Signed-off-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Co-authored-by: npub1em3jmyn4vu57urqf03txrwreccvejvwdy5c4er8nnrwt7rc4tncscs3ssu <cee32d92756729ee0c097c5661b879c6199931cd25315c8cf398dcbf0f155cf1@buzz.block.builderlab.xyz>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
2026-10-02 09:50:02 -04:00
Logan Johnson
e4681e44a7 fix(acp): wake quiet hosts for respawns and queued retries (#7459)
## Problem

With `--no-presence --no-typing --heartbeat-interval 0`, a crashed ACP
transport can leave work queued indefinitely. Background respawn results
were only drained before `select!`; neither their arrival nor queue
retry eligibility woke the host. A respawn-only wake is insufficient:
replacement initialization normally precedes the independent queue
backoff deadline.

## Change

- Select on respawn completion, reusing the existing return-to-pool and
dispatch paths.
- Select on the earliest queued retry throttle, including
already-expired deadlines, only while the pool is ready and has idle
capacity.
- Give the existing 30-second maintenance/refill check its own wake
source. Circuit cooldown/refill policy is unchanged; it no longer
depends on presence/typing traffic.
- Disable closed respawn receivers without spinning or disabling timers;
prioritize shutdown.
- Keep retry backoff/budget, batching, timestamps, dedup, delivery
state, and intentional host max-turn cap unchanged. Held/no-slot
dispatch releases clear expired retry throttles through the existing
`mark_complete` path.

No reconnect/gateway changes or adapter retry engine. Based directly on
main, independent of other ACP plumbing work. Closest related open PR
found: #7317 (usage-limit retry policy); this is a scheduling fix, not
that policy change.

## Validation

- `cargo test -p buzz-acp`: 920 library tests + 9 integration tests
pass.
- Five new Rust tests exercise the production wake helper and queue
boundary: respawn before/after retry eligibility, backoff preservation,
one-shot redelivery, empty/removed/held/in-flight scopes, closed
receivers, and shutdown cancellation.
- `cargo clippy -p buzz-acp --all-targets --all-features -- -D
warnings`, `cargo fmt --all -- --check`, `just file-size-check`, and
`git diff --check`: pass.
- Actual standalone stock host binary built from this tree +
installed/generated Pi adapter fixture, pinned adapter source
`801d23374a588fd1154bb3da8783f6dcc9084e15` and upstream pi-acp 0.0.33.
Loopback-only synthetic relay/provider; no live agent/config changes.
- All optional presence/typing/heartbeat wakes disabled; 2.5-second
adapter inactivity budget, 45-second callback bound, host hard cap
unchanged.
- Fast replacement: initialization 1.347s after requeue, retry eligible
at 4.447s. Automatic original-batch recovery, one saved worker terminal
and one callback, five dispatch cycles.
- Delayed replacement: initialization 9.118s after requeue, retry
eligible at 4.157s. Same recovery invariants, four dispatch cycles.
- Both use exactly one relay input; both settle within 2.84s of the
later of replacement/eligibility (including a 1.5s duplicate-observation
window). This also rejects waiting for the 30s maintenance tick.
- Same fixture against freshly built unmodified main
`3c7f288c60d67df78577b237e27c3dfc8831aaa1`: expected failure at the
45-second callback bound.

Full workspace/Desktop/mobile `just ci` not run locally; no Tauri files
touched. Relevant ACP package checks above are complete.

## Review / integration gate

Draft for independent review. The fixture uses the pinned pre-repair
adapter to isolate this host defect, not concurrent adapter
shutdown-fencing changes. Composed testing with the final adapter
candidate and independent review remain required before claiming the
shared lifecycle fix complete. No activation, merge, direct main push,
or incident replay performed. Existing all-agents-dead exit and max-turn
safety policies remain unchanged.

Signed-off-by: Logan Johnson <loganj@squareup.com>
2026-10-02 09:19:33 -04:00
Salman Mohammed
2e8b8d9a92 Include thread roots in agent activity events (#8029)
Include the canonical thread root in turn-start and liveness payloads so
clients can distinguish concurrent threads under thread conversation
context. Exercise liveness propagation through production prompt dispatch.

Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Codex <codex@openai.com>
Signed-off-by: Salman Mohammed <smohammed@squareup.com>
2026-10-01 18:50:50 -04:00
Will Pfleger
d1b7da45a2 feat(acp): add BUZZ_GIT_IDENTITY switch for agent commit identity (#8024)
🤖 This adds an operator switch for whose identity an agent's commits
carry, and removes the reason agents were clearing their git identity
env vars. Replaces #6177.

Since #7819, every agent authors and signs commits as its nostr identity
through `GIT_CONFIG_*` env vars, which beat repo and global config in
every shell the agent runs. Operators had no way to opt out of that.

**`BUZZ_GIT_IDENTITY=agent|user`.** It's read once in
`GitEnvironment::install`. Unset or `agent` keeps today's behavior.
`user` keeps only relay git auth (the credential helper and keyfile) and
installs no identity, no `git-sign-nostr` and no signing config, so git
uses the operator's own config. It also drops inherited `GIT_CONFIG_*`
entries for the agent identity and signing keys,
`author.*`/`committer.*`, and `include.path`/`includeIf.*.path`, so a
nested agent in `user` mode can't pick up its parent agent's identity.
Any other value fails startup with an error naming the variable and both
allowed values. The resolved mode is forwarded in the managed env to MCP
servers, which `buzz-agent` starts with a cleared env, so a nested
`buzz-acp` launched from an MCP shell keeps the operator's choice
instead of falling back to `agent`.

**Known limitation:** in `user` mode, an operator whose own git config
signs with `git-sign-nostr` has commits refused with a key-mismatch
error, because the agent shell's `BUZZ_PRIVATE_KEY` (needed by the
`buzz` CLI) loads ahead of `nostr.keyfile` and NIP-GS requires the
signer to exit when the loaded key doesn't match `-u`. Nothing is signed
with the wrong key. SSH and GPG signing are unaffected.

**Desktop reserves `GIT_CONFIG*`.** User env overrides are layered onto
the spawn command after the relay credential helper, so a single
`GIT_CONFIG_COUNT=0` used to silently drop it. `GIT_CONFIG` and every
`GIT_CONFIG_*` name are now reserved keys.

**`session_new_forwards_complete_git_block_without_duplicate_names` no
longer reads the runner's env.** Run inside an agent session, it picked
up the session's own inherited `GIT_CONFIG_*` and failed. Agents worked
around that by unsetting `GIT_CONFIG_COUNT`, and any commit made in that
shell fell back to the human's git identity and signing key. The test
now hides the runner's `GIT_CONFIG*` and `BUZZ_GIT_IDENTITY` under
`ENV_LOCK` and restores them afterwards.

**Agent instructions.** `base_prompt.md` and the nest `AGENTS.md` now
say the runtime sets the agent's commit identity and signing, and that
agents shouldn't override them with `user.*` config, `-c user.*`,
`--author` or another signing key. Trailers are added only when the
repository or the person the agent works for requires them. If a
repository requires a different author, the agent tells the operator,
who can switch to `BUZZ_GIT_IDENTITY=user`, instead of overriding the
identity. The nest `AGENTS.md` scopes this to agents Buzz runs through
its stock `buzz-acp` harness, and notes that custom harnesses get only
relay git auth and control their own attribution. `NEST_AGENTS_VERSION`
goes from 5 to 6.

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-10-01 17:04:33 -04:00
Logan Johnson
d56ed75424 fix(acp): add edit field to native-steer fallback test event (#8022)
🤖
## Summary
The Rust test build on `main` is broken. Two PRs each passed on their
own, but together they do not compile: #4741 added a required `edit`
field to queued relay events, and #7568 (merged later) added a test that
builds a queued event without that field. Every Rust CI job (Unit Tests,
Rust Lint, Windows Rust) now fails on `main` and on any PR that rebases
onto it.

This adds `edit: None` to that one test event. `None` means "not an edit
of an earlier message", which is what the test intends. No production
code changes.

### Related issue
Breakage introduced by #7568 on top of #4741. Searched open PRs for an
existing fix: none found.

### Testing
- `cargo clippy -p buzz-acp --all-targets -- -D warnings` passes; `cargo
fmt --check` passes.
- CI on this PR is the authoritative test run.

Signed-off-by: Logan Johnson <loganj@squareup.com>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
2026-10-01 17:01:51 +00:00
Logan Johnson
514a9a86d3 fix(acp): interrupted frame writes close the sole writer; fixed native-steer refusal reasons (#7568)
🤖

## Summary

- **A partly written message can no longer corrupt the stream between
Buzz and an agent.** Buzz runs each coding agent as a child process and
sends it one JSON message per line over the agent's stdin pipe.
Previously, if such a write was cut off mid-line — a concurrent control
operation won the race, its deadline expired, the task was aborted, or
the pipe failed partway — the partial line stayed in the pipe and the
next message was written directly behind it, leaving a single corrupted
line on the agent's input that no parser can read. Now the connection's
single stdin writer is owned by the write itself: it is handed back only
after the whole line, its newline, and a flush have all completed. An
abandoned or failed write closes the pipe instead, so nothing can ever
append to a partial line — later operations get a clean broken-pipe
transport error, which the existing retry and retirement policy already
handles.
- Intentionally unchanged: cancelling a prompt whose message was fully
written works exactly as before, and an ordinary application-level error
reply from the agent (for example, a JSON-RPC "bad input" error) does
not close the transport — the next prompt reuses the same connection.
- **Refused mid-run steers now log why, in fixed words.** While an agent
task is running, Buzz can try to steer it — hand additional instructions
to the running task — and falls back to the existing cancel-and-merge
behavior when that is not possible. When a steer is refused before
anything is written to the agent, the log now records one fixed reason
instead of an opaque dump of the whole error: `task_absent` (no running
task owns the scope), `sender_absent` (the task has no steer channel
installed), `mailbox_full` (the one-slot steer mailbox already holds a
steer), or `mailbox_closed` (the task's steer receiver is gone).
Before-write refusals are now distinguishable from failures after a
steer was accepted, which keep carrying the underlying transport error.
Behavior is unchanged — same fallback, request ownership, and return
values; only the log field changes.

### Related issue

N/A

### Testing

- **19 new tests.** In `crates/buzz-acp/src/acp_frame_writer.rs` (8):
interrupting the write at the body or the newline boundary, or hitting
an I/O failure, must close the writer and make every later write fail; a
completed write keeps the writer; a real OS-pipe test interrupts a 300
KB prompt mid-body and asserts that cleanup and later requests fail with
no frame appended; another real-pipe test asserts a completed prompt
still cancels normally; an application-error reply leaves the connection
reusable. In `crates/buzz-acp/src/pool.rs` (7): each fixed refusal label
is produced by `send_steer` for its real cause, an admitted steer
reaches the read loop, the labels round-trip without drift, and
post-admission failures never classify as admission refusals. In
`crates/buzz-acp/src/lib.rs` (4): the real `try_native_steer` is driven
through each refusal branch with a real pool, queue, and signed event,
and the emitted fallback log is captured and pinned — the exact fixed
reason, exact message and channel, no request or error content, and the
unchanged fallback return; forcing the production reason to a wrong
label or deleting it fails all four. A mutation check (removing the
writer-ownership transfer) fails the frame tests.
- **Full local `just ci` at `45e00ec82` — all lanes passed**, including
the buzz-acp 947 unit + 9 integration tests, desktop and mobile suites,
fmt, clippy, web, security, and build lanes.
- **Previous head `5276ef375` added only a one-line test fix.** Windows
Clippy flagged the `wait_for` test helper as dead code because its only
callers are Unix-only tests; it now carries the matching `#[cfg(unix)]`
guard. No production code or assertions changed. Re-verified locally at
this head: `cargo fmt --check`, `cargo clippy -p buzz-acp --all-targets
-- -D warnings`, and the 8 frame-writer tests pass. No full local rerun
— the full matrix ran remotely at this head.
- **Remote CI at `5276ef375` was green** — [run
34530106246](https://github.com/block/buzz/actions/runs/34530106246): 42
jobs passed, 20 conditionally skipped, 0 failed, including the Windows
Rust lane (the guard's purpose) and the desktop core lane; Docker image,
desktop release-candidate, and DCO checks pass. The automated security
review over `00209076c…5276ef375` reported no findings.
- **Current head `2224ec228` adds the fallback-log regression tests.**
Rebased onto `main` at `44c1cc7df` and extended with a test-only commit:
the four `lib.rs` tests above now pin the production fallback log.
Verified locally at this head: full `cargo test -p buzz-acp` (952 unit +
9 integration, 0 failed), `cargo clippy -p buzz-acp --all-targets -- -D
warnings`, and `cargo fmt --check`, all clean; the DCO check passed
remotely. Fresh remote CI is running on this head.

---------

Signed-off-by: Logan Johnson <loganj@squareup.com>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
2026-10-01 11:45:44 -04:00
Logan Johnson
5fdb2e5365 fix(acp): preserve routing for edited messages (#4741)
🤖
## Summary
When someone edits a message to add an @mention, the app does not change
the original message. It publishes a separate hidden "edit" event that
points back at it. An agent that handles the edit must work on the
**original message**: it reacts, replies, and posts any error notice on
that message or in its thread. Before this PR, an agent treated the
hidden edit as a new message. It replied in the wrong place, and with
per-thread sessions it started a new, empty conversation.

This is the first of three PRs. It sets where agents reply to edits. It
does not yet make edits wake agents by default (#6131 does that).

- The agent looks up the original message once, when it accepts the
edit. Every later step uses that result: the session, reactions, typing
indicator, reply target, and failure notices.
- **Per-thread sessions** (the `thread` session policy, where each
thread has its own agent conversation): the edit joins the original
message's thread session. It does not start a new session.
  - The original is a reply in a thread: the edit joins that thread.
- The original is a top-level message: the edit joins the thread that
starts at the original.
- **Channel sessions** (the default policy) and DMs: nothing changes.
The whole channel shares one conversation.
- Setup mode (the reply that asks the owner to finish configuring the
agent) now accepts edits. It replies at the original message or its
thread. If the original could not be fetched, the reply posts at the top
of the channel.
- Error notices for an edit (sign-in failure, hard timeout, retries used
up) now appear in the original's thread, or as a reply to a top-level
original. Before, they appeared at the top of the channel. If the
original could not be fetched when the edit was accepted, the notice
looks it up once more. If it is still unknown, the notice posts at the
top of the channel.
- If a newer request interrupts a turn that is working on an edit, the
next turn treats the edit as earlier work that the new request replaces.
The old edit does not run again.

## Details
- The lookup has a 2-second limit. It runs only for edits that already
matched a subscription rule (by default, edits that newly mention this
agent). It must finish before the edit is queued, because the session is
chosen at that point.
- If the lookup fails, the agent still targets the edited message's ID,
never the hidden edit event. Notices and setup replies never claim that
message as a thread root, because the relay rejects a root that does not
match the message's real thread. **Known gap:** if that message is a
reply in a thread, the agent cannot learn the thread root. So under
per-thread sessions it uses a separate session rooted at that reply, and
it loses the thread's earlier context. Its reply still goes to the right
message. Closing this gap needs a second lookup, which is left out on
purpose.
- The original is fetched with an explicit kind (stream message or its
edit) and a channel filter, because the relay rejects lookups without a
kind.
- Each queued event carries its own route, so an edit shares a batch
with other events like any message. Batch order is unchanged from
`main`.

### Stack
**1 of 3**, based on `main`. Next: #6131 (edits wake agents by default),
then #6132 (edits join a turn that is already running).

### Related issue
Part of #2540. The issue is fixed when #6131 merges.

### Testing
- `cargo test -p buzz-acp --all-targets` passes. One existing test,
`session_new_forwards_complete_git_block_without_duplicate_names`, fails
in a shell where the git setting `nostr.keyfile` is set. It passes when
the setting is removed. This PR does not touch that test.
- Session tests: an edit uses the original's thread for a threaded
original, a top-level original, and an original that could not be
fetched. It never uses the edit event.
- Tests check the signed events that the agent publishes: setup-mode
replies and failure notices, each for a threaded original, a top-level
original, and an original that could not be fetched (including a notice
whose second lookup finds the original). If either routing fix is
reverted, these tests fail.
- A queue-to-prompt test interrupts a turn on an edit with a newer
request. The next prompt has the edit as earlier work and the new
request with the "replaces earlier work" framing, and nothing is left to
run again.

---------

Signed-off-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
2026-09-30 21:36:59 -04:00
Will Pfleger
965e1997b5 fix(agents): stop built-in prompts from teaching sleep polling (#7992)
An @mention wakes an agent only once its running tool call returns, so
agents that loop `sleep` + `buzz messages get` (or poll CI) delay every
reply that arrives in the meantime. Buzz's own prompts taught that loop.

- `crates/buzz-acp/src/base_prompt.md`: replace "No push notifications —
poll with …" with a rule to never use a tool call (`sleep`, `--watch`)
to wait on CI, teammates, or other external events; hand off and end the
turn. `buzz messages get --since` stays as a one-shot catch-up. "keep
working until you publish the verified result" now reads "the result,
handoff, …" so agents stop reading it as wait-for-green-CI.
- `desktop/src-tauri/src/managed_agents/nest_skill.md`: the "Polling
Pattern" sleep-and-repeat loop becomes a "Catching Up" one-shot
`--since` read.

🤖 Opened by Duncan.

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-09-30 18:28:54 +00:00
Alex Rosenzweig
95b018c871 feat(acp): wrap workers at the subprocess launch boundary (#7985)
## Summary

Replacing `BUZZ_ACP_AGENT_COMMAND` with a sandbox launcher hides the
real adapter from Buzz: for example, Goose loses its default `acp`
argument. Add optional `BUZZ_ACP_LAUNCH_PREFIX`, a JSON argument array
applied at the shared subprocess boundary, after normal worker
configuration.

On Unix, every spawn runs `prefix... worker args...` without shell
interpolation. Worker identity, environment setup, stdio and existing
process cleanup are preserved. Invalid or unavailable prefixes fail
launch without falling back to the worker; unset keeps direct launch on
every platform. Configured prefixes fail closed on non-Unix because
per-worker process-tree cleanup is unavailable. Desktop reserves the key
against saved user-environment overrides, and spawn failures name the
executable.

### Related issue

Prerequisite for [Buzz-App
#415](https://github.com/block/buzz-app/pull/415). One commit directly
on `main`, independent of #7942–#7944. No duplicate launch-prefix PR
found. Policy enforcement, verified launcher staging, protected paths
and the supporting runtime pin remain Buzz-App/plugin work.

### Testing

- Original head `9ae9d82f8`: complete ACP suite (990 passed, three
existing ignored), shipped local-task test, real-Goose probe, and full
repository-wide `just ci` passed.
- Review fixes: five launch tests and 44 Desktop environment-filter
tests pass locally, covering the real production platform gate, save
rejection, both merge layers, case variants, and executable error
context.
- Subprocess tests cover direct/wrapped argument defaults, Pi skills,
Hermes/Codex environment, adapter identity, repeated spawns and
invalid/missing prefix refusal.
- Shipped `buzz-acp run` entrypoint completes a task through the wrapper
and deterministic ACP peer.
- Real Goose 1.52.0: direct and wrapped `auth-methods` return identical
results; the wrapper receives `goose acp` with initially empty worker
args.
- Independent agent review: no blocking findings.
- Required push checks passed on the updated head. The existing native
Windows CI job now explicitly runs the platform-contract tests. Its
result is pending; the non-Unix refusal branch was not exercised on this
macOS host.

Repeated-spawn coverage is not a full lazy-pool wake/crash test.
Protected Goose in the Buzz-App UI and human acceptance remain pending.

To try the hook after building `buzz-acp`, use an installed Goose path:

```sh
BUZZ_ACP_LAUNCH_PREFIX='["/usr/bin/env"]' target/debug/buzz-acp auth-methods \
  --agent-command /absolute/path/to/goose --agent-args '' --json
```

Expect the same authentication methods as without the prefix. Changing
the prefix to `["/missing-launcher"]` must fail. Draft pending human
testing.

Signed-off-by: Alex Rosenzweig <arosenzweig@squareup.com>
2026-09-30 10:52:29 -05:00
Logan Johnson
ea1e97e65f feat(acp): run one prepared task from a file or stdin (#7851)
🤖
## Summary
Scripts, scheduled jobs, and other automation need to give an agent one
prepared task and wait for a result without starting a long-running chat
listener. This adds `buzz-acp run`: it reads a task from a local file or
stdin, runs one fresh agent session with the configured tools and
instructions, cleans up, and exits with a machine-readable status.

```sh
buzz-acp run --task ./task.json
cat task.json | buzz-acp run --task -
```

- The versioned JSON task specifies a task ID, intended agent public
key, prepared prompt, and execution time limit. Invalid input or an
identity mismatch is rejected before agent launch. Credentials,
executable, tools, and permissions remain controlled by trusted launch
settings, not the task document.
- Each invocation uses the normal agent equipment: base/persona/team
instructions, enabled memory, tools, model and effort settings,
permissions, and Git identity, credentials, and signing. Both task and
conversational modes obtain these capabilities from one shared runtime
owner; new shared setup belongs there rather than in separate entry
points.
- Launch configuration reads have a separate ten-second bound and can be
cancelled even when a prompt file blocks. A deadline covers adapter
startup through the task turn. Interrupts and termination signals
trigger bounded cleanup. Stdout contains one terminal JSON record;
diagnostics stay on stderr. A normal turn ending is not proof that the
task achieved its goal.
- Normal invocation remains the conversational service. Task mode does
not start chat subscriptions, presence, heartbeat turns, or a persistent
agent pool. Tasks are trusted executable instructions, not sandboxed
input. Reusing a task ID does not prevent duplicate execution, and
uncertain outcomes must not be blindly retried.

See [the task contract](crates/buzz-acp/TASKS.md) for the document
format, exit codes, limits, and examples. URL retrieval, a task server,
and a persistent execution ledger are outside this change.

### Related issue
None found in the existing issue and PR search.

### Testing
- At `4e86bbc97`: 964 unit tests and 22 integration tests passed, with
three existing ignored tests. The package suite ran serially with
inherited harness/Git options cleared. A final test-only shutdown-result
assertion was then checked with the focused memory test and all-target
Clippy.
- All eight real-binary task tests independently passed on the exact
candidate, including blocked configuration reads, file/stdin validation,
session equipment, deadlines, cancellation, and cleanup. The memory test
passed 20 repeated runs. Removing the oversized-input guard fails the
new error assertion; restoring it passes.
- Package all-target Clippy, formatting, file-size checks, and pre-push
Rust/Tauri checks passed. All executed hosted checks passed on the
updated head (54 successful, 28 skipped). The separate Codex review
comment still requests authorization for this exact range.
- Earlier revisions passed the release build and a real Goose 1.51.0
tool-use smoke test. No new release artifact or installed Janet runtime
is claimed for this feedback revision.
- Repository-wide `just ci` was previously attempted; workspace Clippy
passed, then the desktop dependency check stopped on a local pnpm fetch
failure. Hosted checks cover the applicable PR lanes.

---------

Signed-off-by: Logan Johnson <loganj@squareup.com>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
2026-09-24 10:58:15 -04:00
Salman Mohammed
5621006bcf refactor: move agent Git bootstrap into ACP harness (#7819)
discovered in
https://github.com/block/buzz/issues/7742#issuecomment-5783512985

## Summary

Goose uses its native shell, so it bypassed the Git identity and signing
setup inside `buzz-dev-mcp`. Agent commits could use the host Git
identity, while MCP shells had a separate config. This moves Git setup
into `buzz-acp`, where both paths receive the same agent identity, Nostr
signer, and relay-scoped credential helper.

Before:

```text
Buzz Desktop
  └─ buzz-acp                 ← Desktop supplied relay Git authentication
      ├─ Goose → native shell ← no agent Git author or signing setup
      └─ Buzz Agent
          └─ buzz-dev-mcp
              └─ shell         ← MCP created the keyfile and Git signing config
```

After:

```text
Buzz Desktop or Sprig
  └─ buzz-acp                 ← owns the keyfile, identity, signing, and scoped credentials
      ├─ Goose → native shell ← inherits the complete Git environment
      └─ Buzz Agent
          └─ buzz-dev-mcp
              └─ shell         ← receives the same Git environment via mcpServers[].env
```

The harness keeps the signing key in a private temporary file and
removes it on normal exit, startup failure, and graceful termination. It
composes inherited `GIT_CONFIG_*` entries, forwards the complete block
to MCP servers, and bundles the helpers through the ACP multicall
entrypoint. For standard `buzz-acp` launches, Desktop and Sprig no
longer write Git config. A custom Desktop ACP command bypasses the
harness, so Desktop preserves its prior relay-scoped credential helper
for that supported override.

This changes Git settings for every ACP runtime. The startup signal path
also changes so a termination during adapter initialization removes the
keyfile. The temporary file can remain after SIGKILL or a machine crash.

### Related issue

Related: #6177 covers stricter commit identity enforcement and touches
the same Git paths. That PR and this one need to be reconciled before
either lands. No issue was found for this runtime bootstrap gap.

### Testing

- Ran the ignored `git_runtime_tests` with real Goose 1.50.0 and Buzz
Agent ACP sessions. `real_goose_native_git_shell` passed again on this
PR branch with `BUZZ_TEST_BIN_DIR` pointing to built binaries. Goose
made a signed commit and tag through its native shell; Buzz Agent did
the same through its MCP shell. Both verified signatures, identity,
credential scoping, and keyfile cleanup.
- Built the Linux Sprig image and ran `scripts/test-sprig-image.sh
buzz-sprig:acp-git-bootstrap`. The container made and verified a signed
commit and tag, then confirmed keyfile removal after SIGTERM.
- `just ci` passed on the original branch. After rebasing onto `main`
and addressing review feedback, the ACP Git bootstrap integration tests,
focused desktop smoke test, custom-command regression test, formatting,
and ACP/Desktop Clippy passed. The pre-push hook hit two
timing-sensitive ACP deadline tests under parallel load; both passed
alone and the complete ACP package passed serially (964 library, 2 Git
bootstrap, 9 pool lifecycle tests). The push used `--no-verify` after
those checks; GitHub CI is rerunning on the new head.
- Under injected `BUZZ_ACP_ALLOWED_RESPOND_TO`, `BUZZ_ACP_LAZY_POOL`,
and `BUZZ_ACP_IDLE_POOL_SLEEP`, the clean-env serial package command in
`crates/buzz-acp/TESTING.md` passed: 964 active library tests, 2 Git
bootstrap tests, and 9 pool lifecycle tests. Both named Databricks OAuth
tests passed locally; another reviewer reproduced their
environment-specific failures on `main`.
- No authenticated relay clone or push was run. The local probes used
disposable repositories and did not contact a relay.

Generated with Codex

---------

Signed-off-by: Salman Mohammed <smohammed@squareup.com>
2026-09-23 14:41:00 -04:00
Salman Mohammed
4d08194ea9 Deduplicate thread context by ACP session delivery (#7620)
## Summary

Buzz can repeat a thread reply in later prompts after the provider
already received that event as context. The old fix treated every
message signed by the agent key as retained by the current provider
session. That could hide an unseen heartbeat reply because independent
sessions share the same signing key.

This PR now removes only exact event IDs already delivered to the live
ACP session. It uses bounded overfetch so removed events do not shrink
the new-context window, and it preserves unseen same-key replies from
other sessions.

The context header reports `truncated=true` whenever session history was
omitted. The harness calls `/count` only when the relay query finds a
sentinel beyond its fetch window.

This deliberately does not parse event IDs from shell output. A reply
that the agent just published may appear once in the next context delta;
after that event reaches the provider as context, later prompts omit it.

---------

Signed-off-by: Salman Mohammed <smohammed@squareup.com>
2026-09-15 10:24:27 -04:00
Salman Mohammed
deda09c18c Document incoming Buzz turn structure (#7624)
## Summary

Document how agents should read Buzz’s semantic turn envelope. The base
prompt now identifies current event content as the request, conversation
context as support for follow-ups, and context/event fields as routing
and identity metadata.

The existing compiled-prompt regression test now protects that contract.
The lockfile also updates rustls to 0.23.45 to clear RUSTSEC-2026-0285
from the security gate.
2026-09-15 04:04:25 -06:00
Salman Mohammed
6c35e82bd5 fix(acp): improve hints in pi agent setup guide (#7594)
## Summary

Handle missing Pi adapter setup and update its install guidance.

### Related issue

None found.

### Testing

No manual testing.

Generated with Amp

---------

Signed-off-by: Salman Mohammed <smohammed@squareup.com>
Co-authored-by: Amp <amp@ampcode.com>
2026-09-12 17:40:15 +00:00
Salman Mohammed
e17cdd9d5c refactor(buzz-acp): point agents at buzz --help instead of a command table (#7586)
## Summary

The ACP base prompt carried a `Group | Key commands` table for the
`buzz` CLI — a second copy of a surface that already documents itself,
with nothing tying it to the parser. It had already drifted: 16 of the
CLI's 23 groups (missing `emoji`, `gifs`, `notes`, `patches`, `media`,
`moderation`, `pack`) and 50 of the 91 subcommands under the groups it
did list. The adjacent exit-code line stopped at 4; the CLI has a 5th (5
= write conflict, NIP-33 LWW).

#7584 was the prerequisite: `buzz --help` now renders the full command
tree — every group, its subcommands, and their descriptions — in one
invocation (199 lines, 13 KB), generated from clap's own command
definition, so it cannot drift. `buzz -h` still prints the group-level
summary. Pointing the prompt at `--help` was not viable before that,
because discovering `buzz messages send` cost a second call and
discovering `buzz canvas set` exists cost a third.

So this replaces the table with a pointer to `buzz --help`, fixes the
exit codes, and adds the one CLI ergonomic that `--help` does not make
obvious (`--format compact` is a global flag that goes before the
subcommand). Everything after the table is kept verbatim — it is the
non-discoverable semantics `--help` cannot express: multiline content
through stdin, the `BUZZ_AUTH_TAG` requirement for agent drafts,
`--channel` on `pr open`, the `link` field, mention and assignment
rules.

Signed-off-by: Salman Mohammed <smohammed@squareup.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 18:07:12 +00:00
Salman Mohammed
813bbd1412 fix(acp): integrate the Buzz Pi adapter fork (#7552)
## Summary

PR #7335 worked around missing Pi adapter support by generating a
private Pi launcher and injecting Buzz's standing prompt and skills at
process launch. The Buzz Pi fork now carries the required adapter
extensions, so this removes that launcher and returns prompt
construction to the normal ACP session path while retaining the
base-prompt composition introduced by #7335.

The Pi preset now installs `salman1993/pi-acp` and launches its renamed
`buzz-pi-acp` binary. Buzz adds `-- --skill
<harness-cwd>/.agents/skills` only when launching that binary, sends the
complete composed prompt as the `_meta.systemPrompt` replacement string
on `session/new` only when `initialize.agentInfo.name` is `buzz-pi-acp`,
and sends the scoped title alongside it as `_meta.sessionTitle`. The
fork identity is treated as system-prompt capable regardless of its
reported ACP protocol version, which prevents duplicate legacy
user-message framing. Upstream `pi-acp` does not receive either
fork-specific behavior. Observer transcript projection accepts the
string, `{ replace }`, and `{ append }` metadata forms.

The fork now stores restore metadata in one atomic file per session
under `~/.pi/buzz-pi-acp/sessions/`. This prevents concurrent Buzz
workers from overwriting another session's prompt or title. The fix
landed in
[salman1993/pi-acp#9](https://github.com/salman1993/pi-acp/pull/9).

This supersedes the closed #7508. No agent-configuration rules changed;
this changes the Buzz Pi adapter contract and launch arguments.

### Related issue

#7329

### Testing

Installed fork commit `09cf07e436b8f18e52401558f988f31a15702313` through
the documented Git URL. The installed bundle matched the committed
`dist/index.js` byte for byte and contained the `~/.pi/buzz-pi-acp`
metadata path. The fork's 106 non-skipped tests, typecheck, and lint
pass.

Ran the ignored real-Pi integration test through Buzz's production
session composer. The test exercised the renamed package,
`agentInfo.name`, and the new per-session metadata store. Base, persona,
team, core-memory, huddle, canvas, and skill markers each appeared once
after switching sessions and again after restarting the adapter, while
the other session and Pi's native default prompt were absent.

Added regression coverage proving `buzz-pi-acp` receives fork-specific
system-prompt metadata and managed skills while upstream `pi-acp` does
not. `just ci` passes.

Generated with Codex

---------

Signed-off-by: Salman Mohammed <smohammed@squareup.com>
2026-09-10 21:38:42 -04:00
Diem Nguyen
051c3a270b fix(buzz-acp): report missing models without retrying (#7538)
## Summary

When an agent reports model-not-found, Buzz retries the unavailable
model and delays the failure reply until retries are exhausted. Stop
retrying this error and immediately post a threaded recovery notice. The
notice tells users to select a different model in agent settings, save,
restart the agent to apply the configuration, and re-send their request.

This adds one error-handling branch and regression coverage in
`buzz-acp`. It matches `-32002` errors containing `model not found`.
Other resource-not-found errors, such as stale sessions, retain the
existing retry behavior. Detailed error events remain available for
diagnosis. The existing restart policy is unchanged.

### Related issue

None found in existing issue/PR searches for model-not-found recovery.

### Testing

Playwright captured and visually checked the thread UI with seeded
conversation data and the exact recovery text. The check opens the
request's thread, confirms no reply before the failure, injects the
notice, and verifies the full text is visible. [Before/after
screenshots](https://github.com/block/buzz/pull/7538#issuecomment-5608196506)
show the corrected save-and-restart instructions. These are local test
captures, not a deployed provider recovery flow.

Generated with Codex

---------

Signed-off-by: Diem Nguyen <diem@squareup.com>
2026-09-09 16:52:35 -07:00
Logan Johnson
c045321a7f fix(acp): pace targeted overflow recovery on consumer capacity (#7325)
🤖
## Summary

When a Buzz agent falls behind on incoming messages, its connection can
make the backlog worse while trying to recover. The connection buffers
messages from the relay server until the agent is ready to process them;
if that buffer overflows, recovery previously requested history for
**every subscribed channel** and paused socket reads while sending those
requests. That adds traffic to an already overloaded connection. This
change requests history only for affected subscriptions, once the code
consuming those messages has room, with at least five seconds between
attempts.

The recovery path now:

- Combines repeated losses into one pending recovery per affected
subscription, keeping the oldest dropped timestamp so replay starts
early enough.
- Waits until at least half the consumer queue is free and the relay's
existing rate-limit delay has expired. The queue wakes recovery when
space becomes available; recovery does not periodically sample capacity
or hold queue space away from live messages.
- Attempts one subscription at a time, choosing the least recently
attempted so a busy channel cannot crowd out other channels or
membership notifications. The five-second delay starts when an attempt
finishes, including a failed write; failed writes leave recovery
pending.

Recovery is paced by available capacity, not by how often messages are
lost. This is not a larger buffer or a cutoff that abandons recovery.
Subscription identifiers, message filters, replay timestamp overlap and
duplicate filtering are unchanged; no downstream agent changes are
required.

This targets a reproducible overload **amplifier**, not every cause of
overload or every catch-up limitation. The initial live overload's cause
has not been established. Recovery remains best effort: a successful
request write is not proof of delivery, and existing history/retention
limits, bounded duplicate tracking and replay limitations still apply.
There is no exactly-once or complete catch-up guarantee. A stalled write
can still pause socket reads for the existing ten-second timeout; the
pacing bound does not cover initial subscriptions, reconnects or other
retry paths.

### Related issue

Closest related: #5014 (channel re-subscription); also #6661 (membership
reconciliation) and #6090 (relay backpressure gap signaling). This
addresses local overflow recovery scheduling, not those separate
mechanisms.

### Testing

Recorded offline comparisons against the previous behavior, with the
final implementation at `8000636f3073167c5a5107bb179c7d91160f1729`:

| Same fixture: 18 subscriptions, three overload rounds | Before | After
|
| --- | --- | --- |
| Recovery history requests | 108 | 3 |
| Ping-response delay | About 4.6 seconds | Below the measurement's 1 ms
resolution |

A separate bounded-history fixture delivered all 320 events plus
subsequent live traffic in **both** versions. Regression coverage
exercises the real socket-handling task, including intermittent consumer
capacity, fairness, failed writes and cancellation of capacity waits
before live delivery. These are synthetic results, not production
throughput measurements or evidence of a deployed cure.

The full local `RUST_TEST_THREADS=4 just ci` run passed on September 4,
2026. Earlier unsuccessful local runs remain part of the validation
history. The [recorded validation evidence and separate desktop
follow-up](https://github.com/block/buzz/pull/7325#issuecomment-5540592398)
preserve the original desktop mock-history scroll failure, its passing
rerun and the remaining investigation. That desktop path does not run
the agent connection code; neither this repair nor the passing rerun
fixes the observed scroll problem.

---------

Signed-off-by: Logan Johnson <loganj@squareup.com>
2026-09-08 21:51:47 +00:00
Salman Mohammed
86c189e857 fix(buzz-acp): wake held ACP threads and fence forked sessions (#7340)
## Summary

Adds an independent deadline wakeup so held thread work dispatches after
its 10-second bound even when the relay loop is otherwise quiet. Fences
session ownership by generation so a worker returning after a fork
cannot make an older provider session claimable again.

This follows up on the two post-merge findings from
[#7337](https://github.com/block/buzz/pull/7337#pullrequestreview-5116329341).

### Related issue

Follow-up to #7337.

### Testing

- `cargo test -p buzz-acp`
- `cargo clippy -p buzz-acp --all-targets -- -D warnings`
- Pre-push file-size, differential Rust test, and desktop Tauri gates

No UI changes.

---
**Update Sep 4, 15:35:** Addressed both Codex review findings.
- Queue-cap eviction now prunes orphaned hold deadlines.
- An expired hold stays expired until a worker is successfully claimed.
- Hold timers remain disabled while every worker is busy; worker return
wakes dispatch directly.
- Added regressions for queue eviction and pool exhaustion.

Generated with Codex

---------

Signed-off-by: Salman Mohammed <smohammed@squareup.com>
2026-09-08 10:10:38 -04:00
Salman Mohammed
4beffef697 feat(buzz-acp): update base prompt; add buzz context and skills to Pi agents (#7335)
## Summary

- launch Pi through a private Buzz wrapper so managed Pi agents receive
Buzz's base prompt plus the configured thread/channel session model via
`--system-prompt`
- load the Buzz workspace skill directory with Pi's repeatable `--skill`
flag, making `~/.buzz/.agents/skills/buzz-cli` available alongside Pi's
normal global skill discovery
- report Pi and `pi-acp` installation states separately so setup
guidance points to the missing component

`pi-acp` does not currently consume the ACP `session/new` system prompt,
but its normal new-session and restore paths do honor
`PI_ACP_PI_COMMAND` as the executable used to launch Pi. Buzz reserves
that variable and sets it to a private generated launcher. The launcher
invokes `pi` from Buzz's effective `PATH`, adds `--system-prompt <file>`
and `--skill <workspace>/.agents/skills`, and forwards `pi-acp`'s RPC
and session arguments unchanged. This keeps the integration entirely in
`block/buzz`, without changes to Pi or `pi-acp`.

### How PI_ACP_PI_COMMAND works in `pi-acp`

`PI_ACP_PI_COMMAND` selects one executable; it does not accept
arguments. Therefore:

```bash
# Does not work
PI_ACP_PI_COMMAND="pi --skill ~/.buzz/.agents/skills"
```

Buzz does not expose this variable as user configuration. It creates a
private launcher and sets `PI_ACP_PI_COMMAND` only on the `pi-acp`
child. Conceptually, that launcher executes:

```sh
#!/bin/sh
exec pi \
  --system-prompt "$PRIVATE_SYSTEM_PROMPT" \
  --skill "$HOME/.buzz/.agents/skills" \
  "$@"
```

### Related issue

Follow-up to #7208. No duplicate issue or open PR found.

### Testing

- `just ci`
- `cargo test -p buzz-acp pi_launcher`
- `cargo test --manifest-path desktop/src-tauri/Cargo.toml
managed_agents::env_vars::tests`
- `cargo test --manifest-path desktop/src-tauri/Cargo.toml
managed_agents::discovery::presets::tests`
- pre-push branch checks

No screenshots: the UI change is state-dependent setup guidance only.

---
**Update Sep 4, 13:35:** Made `PI_ACP_PI_COMMAND` entirely Buzz-owned.
- Managed agent configuration now rejects the variable as a user
override.
- Buzz always launches `pi` from its effective `PATH` and rejects
inherited values before creating its private launcher.
- Removed the custom Pi executable discovery path and its internal
environment alias.

---
**Update Sep 4, 14:03:** Reject inherited `PI_ACP_PI_COMMAND` values
instead of replacing them.
- Pi startup now fails with an actionable message telling the user to
unset the variable.
- Removed the Pi-specific exception from the generic ACP environment
injection path.

Generated with Codex

---------

Signed-off-by: Salman Mohammed <smohammed@squareup.com>
2026-09-04 21:17:00 +00:00
Jordan Mecom
e09f715c9d Verify ACP relay events before prompt routing (#7010)
## What changed

Verify every Nostr EVENT received by buzz-acp immediately after
deserialization. Events with an invalid NIP-01 ID or Schnorr signature
are dropped before subscription routing, deduplication, replay-watermark
updates, membership handling, or either harness queue.

## Safety

Signature verification runs on the blocking pool so cryptographic work
does not block the relay task. A verification failure drops only that
event and keeps the connection available for subsequent valid traffic.
The existing observer-control verification remains as defense in depth.

Regression tests cover valid delivery; changed content, ID, signature,
pubkey, tags, and timestamp; a forged owner shutdown command with a
recomputed ID; forged membership notifications; and forged
observer-control events.

## Testing

cargo test -p buzz-acp

cargo clippy -p buzz-acp --all-targets -- -D warnings

just ci

---------

Signed-off-by: Jordan Mecom <jm@squareup.com>
2026-09-04 11:19:03 -07:00
Will Pfleger
b17c0776b7 fix(buzz-acp): bound busy-owner hold to prevent cross-channel starvation (#7337)
## Problem

#6732 added a busy-owner hold to the ACP harness: when a scope's
recorded session owner (`session_owners`) is checked out on **any**
turn, `dispatch_pending` holds the scope's batch instead of dispatching
it. The hold was added to keep one provider session per thread — but it
is unconditional: it applies to `Conversation` scopes too, and it has no
time bound.

Under the default `session_policy=channel`, every channel collapses to a
single `Conversation` scope, so once two channels' sessions land on the
same worker (pass 2 of `try_claim` picks the first idle worker by index,
so this happens quickly after any restart), channel A's mention starves
behind channel B's in-flight turn — for up to the full
`max_turn_duration` (7200s by default) — while other workers sit idle.
The only signal is a DEBUG-level log, and the 👀 seen-reaction is added
at queue admission *before* the hold decision, so the user sees the
agent acknowledge the mention and then nothing.

Observed in production on the first day of the v0.5.22 rollout: three
separate incidents where a mention got 👀 but no turn started until an
unrelated channel's turn ended on the shared worker (in the worst case
the blocking turn sat in a single tool call for 6+ minutes).

## Fix

One new seam, `AgentPool::hold_decision`, replaces the raw
`should_hold_for_busy_owner` check in `dispatch_pending` (the predicate
itself is unchanged and remains the inner check):

- **`Conversation` scopes never hold.** Channel-policy channels and all
DMs dispatch immediately; a busy owner means forking onto an idle
worker, exactly the pre-#6732 behavior. This removes the cross-channel
head-of-line blocking entirely for the default policy.
- **`Thread` scopes hold for a bounded window.**
`HOLD_BUSY_OWNER_TIMEOUT` (10s) is measured from the first time the
batch is held (`held_since` stamp); once elapsed, the batch stops
holding and forks a fresh session on an idle worker, rebuilding thread
context from the relay. This preserves #6732's session-continuity intent
for the momentary-busy case while capping the worst-case wait. No new
timer is needed: held batches are requeued with preserved timestamps and
re-evaluated on every dispatch trigger (turn end, relay event, 30s
maintenance tick), so the effective worst-case re-check gap on a fully
silent system is one maintenance tick.
- **Holds are observable.** Holding logs at INFO and a hold expiry logs
at WARN (previously DEBUG-only), and both emit observer-feed events
(`busy_owner_hold`, `busy_owner_hold_forked`) with the scope, owner
index, and held duration.

`held_since` is derived state and is cleared on every removal path:
dispatch/fork (inside `hold_decision`), `invalidate_channel_sessions`,
`invalidate_scope_session`, and `switch_idle_agent_model`.

## Accepted trade-offs

- A fork after an expired hold leaves the old owner's now-orphaned
thread session in its session map until natural rotation/invalidation —
benign, and identical to pre-#6732 fork semantics (`loadSession: false`;
sessions are worker-pinned, so migration is not an option).
- Under sustained pool exhaustion the hold stamp is cleared on the fork
attempt and re-stamped next cycle, so the bound is effectively "timeout
after a worker frees up," not absolute wall clock.

## Tests

- New table test `hold_decision_covers_variant_session_busy_and_timeout`
over the full input space (scope variant × idle-session presence × owner
busyness × elapsed vs. window). The `Conversation` + busy-owner row is
the cross-channel regression guard; the past-window row guards the
bound. Both were mutation-checked: removing the variant gate or the
timeout branch fails the suite.
- `busy_session_owner_holds_batch_instead_of_forking_session` extended
with the Hold → ForkAfterHold transition, the `Conversation` dispatch
guard, and `held_since` pruning on channel invalidation.
- Scope-invalidation and idle-model-switch tests extended to cover
`held_since` cleanup alongside the existing `session_owners` assertions.

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-09-04 14:07:45 -04:00
Salman Mohammed
ce9decb235 fix(acp): rename system tag to agent-instructions (#7332)
## Summary

Rename the paired persona prompt boundary from `<system>` to
`<agent-instructions>` in both modern and legacy ACP delivery paths.
Update Desktop diagnostics to label the new boundary as Agent
Instructions while keeping archived `<system>` captures readable.

`system` is confusing (esp with system role) and agent instructions is
what shows up in the UI today

<img width="767" height="298" alt="Screenshot 2026-09-04 at 11 37 54 AM"
src="https://github.com/user-attachments/assets/4e34ac06-db50-461e-a555-ce17885b0031"
/>

### Related issue

Follow-up to #6701. No duplicate open issue or PR found.

### Testing

- `cargo test -p buzz-acp`
- `cargo clippy -p buzz-acp --all-targets -- -D warnings`
- `node --import ./desktop/test-loader.mjs --experimental-strip-types
--test
desktop/src/features/agents/ui/agentSessionTranscriptHelpers.test.mjs
desktop/src/features/agents/ui/agentSessionTranscript.test.mjs`
- `pnpm --dir desktop typecheck`
- Pre-push hook: 6,237 Desktop tests, 3,164 Tauri tests, and all 13 Rust
unit-test lanes passed

Generated with Codex

Signed-off-by: Salman Mohammed <smohammed@squareup.com>
2026-09-04 16:35:18 +00:00
Matt Toohey
40220d5618 perf(desktop): publish mention sends before waking agents (#7154)
## Summary

Cuts perceived agent-mention send latency by publishing the message
first and waking the agent afterwards, instead of blocking the send on a
synchronous agent start/deploy round-trip. A send that mentions a
stopped or undeployed managed agent now shows the message immediately;
the wake runs fire-and-forget after the relay accepts the publish. The
already-running-agent send also gets faster via revalidation dedupe and
NIP-11 caching.

## Changes

### Publish-first agent wake

- Wakes for mentioned managed agents are collected during send
preparation and flushed fire-and-forget only after `await send(...)`
resolves. No start can fire — and no "your message was sent" toast can
appear — for a message the relay never accepted; every abort path
(cancel, readiness error, publish rejection, dismissed non-member
prompt) simply drops the queue. Persona-create wakes ride the pending
draft behind the non-member prompt for the same reason.
- Each wake is bound to the tenant scope captured at send time: the new
`useDetachedAgentStart` hook passes `expectedRelayUrl` +
`expectedSignerPubkey` with every start, so a wake that outlives a
community switch fails closed at the backend instead of spawning against
the new tenant. A wake whose scope has not resolved yet (identity query
still loading, blank stored relay URL) is refused with a recoverable
toast rather than fired unscoped.
- In-flight wakes are deduped through a module-level map keyed by
`(relay URL, pubkey)` — the same tenant pair the backend keys on — so
two quick sends or two composers cannot double-spawn a cold agent during
the seconds-long start window. Entries are deliberately retained across
community switches (the key *is* the tenant scope, so a retained entry
can never affect another community, and clearing it let an A→B→A round
trip deploy a provider agent twice) and self-clean when the start
settles.
- Wake-failure toasts are fenced to the community they fired in via a
module-level scope mirror: a start that settles after a community switch
logs instead of rendering community A's failure over community B's UI,
and an A→B→A return re-delivers the warning where it is actionable.
- Membership attach and access-policy writes stay synchronous, so the
harness's first kind-39002 read still sees the channel.

### Replay floor

- The send timestamp travels with the wake as `BUZZ_ACP_REPLAY_FLOOR`,
threaded through both local spawns (`spawn_agent_child`) and provider
deploys (`deploy_to_provider` injects it into `launch.policy_env`), so
the harness's startup watermark replays back past the just-published
triggering message no matter how long the spawn takes. `buzz-acp` clamps
the floor to `[now − 15min, now]`.
- The floor is captured at enqueue time, not flush time — the flush runs
post-publish, so a flush-time stamp could exceed the message's
`created_at` and skip the very message the floor exists to cover.
- On local spawns the caller's floor is asserted *after* the user env
layering (and the ambient parent-process value is stripped
unconditionally), so a saved persona/global/agent env entry cannot
shadow this send's floor — mirroring the shadow-strip the provider path
applies to `launch.env`. Both halves share one `REPLAY_FLOOR_ENV_VAR`
const.

### Send-path latency reductions (already-running agents)

- Mention revalidation is deduped: the publish-boundary pass reuses the
pre-side-effect authorization pass unless an awaited round-trip actually
separated the two (background upload, link-preview settlement, DM
expansion, a real access-policy/membership write, or active-huddle
enrollment). This preserves the #5681 authorization boundary while
making the common send single-pass.
- NIP-11 `self` lookups are cached per relay URL for 5 minutes. Only
verified values are cached — non-2xx and malformed responses stay
retryable — and URL keying keeps community switches from serving another
relay's identity.
- `applyReusableAgentAccessPolicy` now reports its relay write
explicitly (`{ agent, wrote }`) instead of signalling through object
identity, so the revalidation trigger above is load-bearing by
construction.

### File splits

Four files crossed the repository file-size ratchet during this work;
one cohesive unit was extracted from each rather than raising a ceiling
— `runtime/setup_payload.rs`, `commands/agents_create_fields.rs`,
`app_state_accessors.rs`, and `useEnsureAgentMentionsReady.ts`. The
ratchet is green at the tip.

### Review follow-ups

The three concrete findings from the first review round are fixed at the
tip: the pre-publish wake and its false "your message was sent" toast
(fixed by queueing wakes behind the publish), the stale cross-community
failure toast (fixed by the scope-mirror fence), and the A→B→A duplicate
provider deploy (fixed by retaining the tenant-keyed in-flight entries
across switches). The fast-path admission-staleness point is answered in
the review thread: deferred paths already re-validate at the publish
boundary, and the remaining fast-path window is milliseconds against an
irreducible network-transit race.

Mid-branch send-perf instrumentation was added to attribute the residual
spinner latency and reverted once that analysis concluded — it is
net-zero in this diff.

### Deferred follow-ups

Durable mention catch-up via `event_mentions` (option 2 step 3) and
backend deploy-epoch coalescing for the wake paths that do not funnel
through `useDetachedAgentStart` (Agents-panel Start, restore,
inbound-persona deploys) are intentionally left for separate changes.

## Testing

- `cargo test --lib` on desktop/src-tauri: 3054 passed; clippy `-D
warnings` + fmt clean
- Desktop unit tests: 5856 passed (the 5 failures are the pre-existing
`inboxReopenNavigation` / `useRetainedProjectGitViews` baseline, present
on origin/main); `tsc --noEmit` and biome clean
- Full mentions (87), channels (89), and community-rail (25) Playwright
smoke suites against `pnpm build:e2e` bundles, with 3× stress reruns of
each new spec
- The load-bearing regression specs were confirmed red on the pre-fix
code: publish-failure → zero starts and no false toast, the dedupe hold
(1 call vs 2), the fail-closed scope refusal, the rail-switch toast
fence, and the A→B→A retention spec (1 deploy vs 2)
- New unit coverage pins the queue contract (enqueue-time floors,
attach-seam queueing), the scope capture and verbatim relay-URL handoff,
the dedupe map's keying and settle-then-repermit behavior, the unscoped
refusal, the toast-scope mirror, the `{ agent, wrote }` contract, and
the replay-floor env layering on both spawn paths

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-09-03 14:46:11 +10:00
Will Pfleger
47d068e210 feat(cli): add buzz gifs command group and NIP-30 emoji tags on messages (#7259)
## What

Two new agent-facing capabilities in `buzz-cli`:

### 1. `buzz gifs` command group (agent KLIPY picker path)

Agents can now search and share GIFs via the relay's authenticated KLIPY
proxy without holding a provider credential.

```bash
buzz gifs search                         # trending GIFs
buzz gifs search --query "celebration"   # search GIFs
buzz gifs share --slug <slug>            # report selection to provider Recents
```

Output is a JSON array of GIF objects. Paste the `cdn_url` field
directly into `buzz messages send --content` — sending a GIF is a plain
message containing the CDN URL, no special send-path handling.

**Implementation details:**
- Gates on NIP-11 `supported_extensions` containing `buzz-gif` and
`gif.provider == "klipy"`
- Uses relay-relative paths from the NIP-11 `gif` descriptor — no
hardcoded paths; safe-path validation mirrors
`desktop/src/features/gifs/api.ts`
- New `post_json_authed` helper in `BuzzClient` handles NIP-98-signed
JSON POSTs and 204 No Content responses
- `customer_id` derived as `SHA-256(secret_key_bytes || '\0' ||
relay_url_bytes)[..16]` → 32 hex chars: stable, relay-scoped, not
computable from public data, no storage needed
- `locale` defaults to `$LANG` (stripped of encoding suffix) or `en_US`

### 2. NIP-30 custom emoji tags on outgoing messages

`buzz messages send` now automatically attaches `["emoji", shortcode,
url]` tags for any `:shortcode:` patterns in the content that resolve in
the workspace palette — identical to the desktop composer behavior.

```bash
buzz messages send --channel <uuid> --content "hello 👋 everyone 🎉"
# → event carries ["emoji", "wave", "..."] and ["emoji", "tada", "..."] tags
```

**Implementation details:**
- Hand-rolled single-pass scanner (no new dependency) implementing
`:([a-z0-9_-]+):` case-insensitively with canonical lowercase output —
mirrors `desktop/src/shared/lib/customEmojiTags.ts` exactly
- Zero extra relay round-trips when content contains no `:` character;
one `query` when candidates exist but none match
- Palette fetch reuses the existing `union_custom_emoji` logic from
`commands/emoji.rs`
- `build_message` in `buzz-sdk` gains a new `emoji_tags: &[Vec<String>]`
parameter (additive — all existing callers pass `&[]`); NIP-30 tag
attachment lives in the SDK alongside `imeta` tags
- MCP send path (`buzz-acp`) continues to pass `&[]` and is not
affected; the MCP gap is noted in a comment

## Files changed

| Crate | File | Change |
|-------|------|--------|
| `buzz-cli` | `src/commands/gifs.rs` | New — search + share handlers,
NIP-11 gating, tests |
| `buzz-cli` | `src/commands/mod.rs` | `pub mod gifs` |
| `buzz-cli` | `src/lib.rs` | `Gifs(GifsCmd)` variant, dispatch arm,
inventory test update |
| `buzz-cli` | `src/client.rs` | `post_json_authed` helper |
| `buzz-cli` | `src/commands/emoji.rs` | `scan_shortcodes` +
`resolve_emoji_tags_for_content` + tests |
| `buzz-cli` | `src/commands/messages.rs` | Emoji scan + tag injection
in `cmd_send_message` + seam tests |
| `buzz-cli` | `README.md` | `buzz gifs` section + emoji-in-messages
note |
| `buzz-sdk` | `src/builders.rs` | `build_message` gains `emoji_tags`
param + tests |
| `buzz-acp` | `src/pool.rs` | Update `build_message` call site (`&[]`)
|
| `buzz-acp` | `src/setup_mode.rs` | Update `build_message` call site
(`&[]`) |
| `countdown-bot` | `src/main.rs` | Update `build_message` call site
(`&[]`) |

Relates to: https://buzz.block.builderlab.xyz — buzz-team channel thread
on agent GIF/emoji support

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-09-02 20:03:38 -04:00
Will Pfleger
2af9773d62 fix(acp): replace real user name in base prompt mention example (#7250)
Replace the real user name in the shared ACP mention guidance with the
fictional `Alice Smith` example. Preserve the exact-display-name and
no-inference instructions while avoiding prompt priming from a real user
identity.

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Alia <d32955ad69077062930cc46cfe2df30ca9aaf6f8e76422681265e9e9af704d78@buzz.block.builderlab.xyz>
Co-authored-by: Alia <d32955ad69077062930cc46cfe2df30ca9aaf6f8e76422681265e9e9af704d78@buzz.block.builderlab.xyz>
2026-09-02 13:12:25 -04:00
Salman Mohammed
42aeb1571c feat(desktop): add Pi agent preset (#7208)
## Summary

Add Pi as a selectable desktop ACP runtime via `pi-acp`, including
discovery guidance and its official logo.

### Related issue

Follow-up to closed #6546.

### Testing

- Focused Tauri preset and desktop logo tests
- TypeScript typecheck

Generated with Codex

---------

Signed-off-by: Salman Mohammed <smohammed@squareup.com>
2026-09-01 17:41:09 -04:00
Will Pfleger
cae158ce7f fix(dev-mcp): extend shell timeout cap to 20 minutes and align outer budgets (#7185)
Raise the `buzz-dev-mcp` shell timeout ceiling to 1,200,000 ms (20
minutes) while keeping the omitted-value default at 120,000 ms (2
minutes), and align the two outer timeout layers that bound every MCP
tool call.

## What changed

**`crates/buzz-dev-mcp`** — shell timeout cap raised to 1,200,000 ms;
tool and parameter descriptions updated to match.

**`crates/buzz-agent`** — `BUZZ_AGENT_TOOL_TIMEOUT_SECS` default raised
from 660 s to 1,260 s (20-min cap + 60 s cleanup headroom). The old 660
s default would have killed the MCP server at 11 min, before a 20-min
shell call could finish. README updated.

**`crates/buzz-acp`** — `DEFAULT_IDLE_TIMEOUT_SECS` raised from 900 s to
1,500 s and its doc comment updated (1,200 s max shell + 300 s breathing
room). The old value was documented as "600 s max shell + 300 s" — stale
arithmetic.

## Tests

- Existing boundary tests in `buzz-dev-mcp` (default, exact-cap,
over-cap, `u64::MAX`) all pass.
- `default_idle_timeout_is_1500_seconds` — pinned constant replaces old
900 s assertion.
- `default_tool_timeout_is_1260_seconds` — new pinned constant in
`buzz-agent`.
-
`budget_ordering_invariant_shell_cap_plus_headroom_fits_within_idle_timeout`
— new const assertion in `buzz-acp` that encodes the full three-layer
ordering: shell cap (1,200 s) ≤ agent tool timeout (1,260 s) < ACP idle
timeout (1,500 s) < max turn duration. A future edit that inverts any
tier fails to compile.

---------

Signed-off-by: Alia <d32955ad69077062930cc46cfe2df30ca9aaf6f8e76422681265e9e9af704d78@buzz.block.builderlab.xyz>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Alia <d32955ad69077062930cc46cfe2df30ca9aaf6f8e76422681265e9e9af704d78@buzz.block.builderlab.xyz>
Co-authored-by: Hayt <9e1c23a3fd83f61da34420e4e88ff1b16e45cafcc0cd9019eb07d4ecfa8ca9b0@buzz.block.builderlab.xyz>
2026-09-01 18:20:25 +00:00
Salman Mohammed
674c173eb7 feat(buzz-acp): give each channel thread its own agent session (#6732)
## What this does

In a channel, people often run several unrelated conversations at once
(separate threads). Today the agent treats the whole channel as one
conversation, so unrelated threads share the same running session —
their context bleeds together and independent tasks can step on each
other.

This change gives the agent a **separate session per thread** inside a
channel. Direct messages stay as one conversation (unchanged). The
channel is still the boundary for who is allowed in and what is visible
— only the agent's working context is now split by thread.

## How it is turned on

Off by default. Operators opt in with one setting:

- `BUZZ_ACP_SESSION_POLICY=channel` — default, current behavior
- `BUZZ_ACP_SESSION_POLICY=thread` — new per-thread behavior

Being behind a flag means we can enable it for a few agents, watch how
it behaves, and roll back instantly without a code change.

## Key design decisions

- **Decide the thread once, up front.** When a message arrives we work
out which thread it belongs to a single time and tag it. Everything
after that (which line it waits in, which session runs it, what history
it sees) uses that tag instead of re-guessing later, which avoids
mismatches.
- **Default stays identical to today.** Under the default setting a
"thread" is just "the whole channel," so existing behavior and every
existing test are unchanged. The new, riskier behavior is strictly
opt-in.
- **Give the agent only its thread's history.** On a reply the agent
sees that thread's messages (including ones that did not mention it),
not the whole channel transcript — less noise and smaller prompts.
- **Don't let one channel use more memory than before.** More threads
means more live sessions, so the existing per-channel limit now caps all
of a channel's threads together — splitting into threads can't multiply
how much work is held.

## Bugs found and fixed while iterating (from review)

- **Same thread, two sessions.** If the worker already holding a
thread's session was busy, a new message for that thread could start a
*second* session on another worker and split its history. Now it waits
for the right worker instead of forking.
- **Interrupting the wrong thread.** A follow-up meant for thread A
could interrupt thread B in the same channel. Interrupts now target the
exact thread.
- **Stuck thread after a crash.** If a thread's turn crashed, its slot
wasn't cleared and stayed blocked for up to ~2 hours. It now clears
right away and retries.
- **Lost the original request.** When a thread was interrupted and then
had to wait for a busy worker, only the follow-up was kept and the
original request was dropped. The full request is now preserved on
retry.
- **Same thread seen as two.** Two spellings of the same thread id
(upper/lower case) could be treated as different threads. Normalized so
they count as one.

## Not in this PR

- The desktop Settings toggle and rollout wiring for managed agents —
https://github.com/block/buzz/pull/6909
- One pre-existing retry edge case (present today without this flag,
unrelated to this change) — tracked separately so this PR stays focused.

## Testing

The full `buzz-acp` test suite passes (830+ unit and integration tests),
plus new focused tests for thread routing, session reuse, interrupt
targeting, crash recovery, and request preservation. Behavior with the
flag off is unchanged.

---------

Signed-off-by: Salman Mohammed <smohammed@squareup.com>
Signed-off-by: Leo <5faf251baee50ee6bcde338aef6acdd70bb3e60115664c2cd490d94a55dfc488@buzz.block.builderlab.xyz>
Co-authored-by: Leo <5faf251baee50ee6bcde338aef6acdd70bb3e60115664c2cd490d94a55dfc488@buzz.block.builderlab.xyz>
2026-08-31 18:17:28 -04:00
Wes
2f3dd850db fix(relay): reject a frame on its own acknowledgement channel (#6961)
Pinky, an AI agent, updated this description on Wes's behalf after
taking over the startup investigation.

**Category:** fix

**User Impact:** An EVENT refused by WebSocket admission or handler
saturation receives a correlated `OK(event_id, false, reason)` instead
of an uncorrelated NOTICE, so the client can settle that refusal without
waiting for its publish timeout. Rate-limited refusals also arm client
backoff. This fixes a protocol failure mechanism; it does not establish
that every startup send will succeed or that the reported Desktop
startup incident is fully resolved.

**Problem:** Startup opens several live subscriptions and publishes at
once, and the relay's WebSocket admission gate is a fixed 5-second
window (`ws_admission_budget` = `human_ws_events_per_sec * 5`). If that
shared per-principal quota is exhausted, `enforce_ws_admission`
previously rejected an EVENT with a bare `["NOTICE", reason]`. Quota
pressure is a possible trigger, not proof of the original incident's
complete cause.

A NOTICE carries no event id. Both clients settle a pending publish
*only* from an `OK` keyed by event id (desktop `pendingEvents`, mobile
`_pendingEvents`), so nothing settled — and `handle_text_message`
returns early, so no `OK` ever followed either. The send **could not
fail**; it could only time out at `PUBLISH_TIMEOUT_MS` = 25s. That
explains how this rejection mechanism can produce a roughly 25-second
timeout; attributing the original report to it still requires the actual
startup/send workflow.

The handler-semaphore saturation path had the identical defect, and that
one needs no quota burst to fire.

**Solution:** NIP-01 gives each request type its own acknowledgement
channel, and a rejection is only actionable on the same one. Reject a
REQ with `CLOSED`, an EVENT with `OK(id, false, reason)`, and fall back
to `NOTICE` only where no per-request correlation exists. COUNT refusals
now also use `CLOSED(query_id, reason)` per NIP-45, covering both quota
admission and handler saturation (added in
`cd12c93804b87a24b61075dfd171dc471a0a527f`).

Reason strings are unchanged, so the `rate-limited:` prefix and `retry
in {N}s` hint that existing client gates parse keep working (desktop
`parseRateLimitHint`, mobile `RelayRateLimitGate`, buzz-acp
`set_rate_limit_gate`). Only the frame *type* changes, so
`docs/multi-tenant-relay.md` L7 stays satisfied.

Two notes on how this landed, both worth a reviewer's attention:

1. **A survived mutation became a design change.**
`send_admission_result` originally took a `RejectionTarget` parameter,
and reverting the *second* call site (the per-minute message quota)
survived the whole suite — with Redis unreachable the first quota check
short-circuits, so that line is unreachable in test. Rather than test
around it, the parameter is gone: the target is derived from the frame,
so no call site can name the wrong channel.

2. **The relay fix would have caused a client regression on its own.**
Gate arming lived only in the NOTICE branch. Once rejections arrive as
`OK:false`, `handleOk` failed the send without ever backing off — the
client would retry straight into the same quota. Desktop and Mobile now
arm on a `rate-limited:` OK rejection. ACP was subsequently fixed in
`3b06dd32493596ec650f20abf8805791c50fdc24`: it arms the gate and
re-parks only the refused observer frame, preserving other in-flight
frames. Desktop gets `activateRateLimitIfSignalled` as the single owner
of that prefix test, called from both `handleOk` and the NOTICE branch.

<details>
<summary>File changes</summary>

**crates/buzz-relay/src/rejection.rs** (new)
Owns the admission-rejection concern: `RejectionTarget`,
`rejection_target_for`, `request_rejection_message`,
`send_admission_result`, and `enforce_ws_admission`, moved out of
`connection.rs`. Six tests, two of which drive the real
`enforce_ws_admission` against a real `AppState`.

**crates/buzz-relay/src/connection.rs**
Fix the EVENT handler-semaphore rejection to correlate to the event id;
delegate admission to the new module. Add two tests that drive the real
`handle_text_message` with every handler permit held. Down from 1319 to
1116 lines.

**crates/buzz-relay/src/state.rs**
Widen the existing `test_state` helper to `pub(crate)` so the rejection
tests reuse it rather than adding a ninth copy of `AppState`
construction.

**desktop/src/shared/api/relayRateLimitGate.ts**
Add `activateRateLimitIfSignalled` — one owner for the `rate-limited:`
prefix test, since three inbound frame types now carry it.

**desktop/src/shared/api/relayClientSession.ts**
Arm the gate on a rate-limited OK rejection; route the NOTICE branch
through the same helper. Net zero lines, which keeps this
already-oversized file within the differential ratchet.

**desktop/src/shared/api/relayClientPublishRejection.test.mjs** (new)
Four tests against the real `RelayClient`: a rate-limited OK settles the
pending publish and arms the gate; an ordinary rejection does not arm
it; an accepted OK still resolves.

**mobile/lib/shared/relay/relay_session.dart**
Arm the gate in `_handleOk` for a rate-limited rejection.

**mobile/test/shared/relay/relay_session_test.dart**
Two tests driving the real `publish` + `debugHandleMessage` path.

</details>

<details>
<summary>Validation</summary>

**Mutation-tested — 5 mutations, all now killed.** Each production call
site was reverted to the defective behaviour to confirm a test fails.
This caught two false-negative tests:

| # | Mutation | Result |
|---|----------|--------|
| 1 | `rejection_target_for`: EVENT → `Connection` | 4 tests fail |
| 2 | EVENT handler-semaphore call site → bare NOTICE | **survived at
first** |
| 3 | per-minute quota call site → `Connection` | **survived**; fixed by
removing the parameter |
| 4 | desktop `handleOk` gate arming removed | 1 test fails |
| 5 | mobile `_handleOk` gate arming removed | 1 test fails |

Mutation 2 is the lesson: my first saturation test called
`request_rejection_message` directly, so reverting the real call site
inside the `match` arm left it green. It now drives
`handle_text_message` itself and dies on that mutation.

- `cargo test -p buzz-relay` — 928 passed, 1 failed:
`api::mesh_demo::tests::demo_join_forwarded_arm_round_trips_echo`,
**pre-existing**, reproduced with all changes stashed at `4dd4d73de`.
- `cd desktop && npm test` — 5721 passed, 0 failed (full suite).
- `cd mobile && flutter test` — 1876 passed, 0 failed (full suite).
- `just fmt-check`, `just clippy`, `just desktop-check`, `just
mobile-check`, `just file-size-check` — clean. Desktop's 5 biome
warnings are pre-existing (reproduced with changes stashed).
- All 9 pre-push lanes green, including `rust-tests` and
`desktop-tauri-checks`.

**Not verified:** not reproduced end-to-end against a live relay under a
forced quota burst. The causal chain is source-proven and
mutation-proven at the frame level; the ~25s attribution follows from
`PUBLISH_TIMEOUT_MS` but is not directly measured. A packaged-build
click-through would close that gap.

</details>

Related work: #6957 bounds Desktop HTTP event submission, but safe
retained-operation recovery after exhausted/ambiguous outcomes remains
unfinished. #6998 is the separately reviewable Desktop
readiness/duplicate-subscription slice. Neither is claimed to complete
native before/after startup-send validation.

Diagnosis note: `RESEARCH/DESKTOP_STARTUP_SEND_STALL_2026_08_27.md`
(Brain's workspace).

## Current review disposition (2026-08-28)

The [review on
`cd12c938`](https://github.com/block/buzz/pull/6961#pullrequestreview-5052902510)
identified ACP's missing rate-limited-OK handling. Commit
`3b06dd32493596ec650f20abf8805791c50fdc24` fixes gate arming, re-parking
the specifically refused observer frame, and the stale NOTICE comment.
Two regressions drive the real frame dispatcher. See [the implementation
and validation
response](https://github.com/block/buzz/pull/6961#issuecomment-5455032054).

The Mobile generation-check inline thread is resolved: its `async
publish` returns a failed Future when superseded; it does not throw
synchronously at invocation. No further production change was indicated
by that comment.

The validation counts above describe the original slice, not a new
rerun. At `3b06dd324`, the current GitHub check rollup has successful
completed test/build checks (non-applicable jobs skipped). The
security-review comment still requires review for the current base/head
range; do not read a green authorization job as a completed security
review. Approval and merge remain human decisions.

---------

Signed-off-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz>
Co-authored-by: Carl <9d00794d3df50972eb8b615511783cab12a77a8fd5dd5edd58073ec73b54bd8b@buzz.block.builderlab.xyz>
2026-08-31 14:43:31 -06:00
Wes
93237b4a74 fix(acp): wake agents from workflow messages (#6953)
> Pinky, an AI agent, is opening this PR on Wes's behalf.

## Summary

Workflow-generated messages can contain a valid agent mention but still
fail the ACP inbound author gate because the relay signs the event. This
keeps the existing wake policy and gives ACP a narrowly verified
effective author:

- preserve the workflow owner's existing `p` tag and all
rendered-mention `p` tags
- add explicit `["buzz:workflow-owner", <owner hex>]` provenance to
relay-generated workflow messages
- add `["buzz:workflow-mention", <agent hex>]` authority only for
mentions resolved from the stored, unrendered workflow step template
- accept that owner only for a verified kind-9 event signed by the
relay's current NIP-11 `self` key, with unique canonical workflow
metadata and an explicit workflow mention for the receiving agent
- route the verified owner through the existing author and in-flight
mode policies in both normal and setup listeners
- refresh relay identity after reconnects, retaining the last verified
key on transient fetch errors while treating a successful response
without `self` as definitive removal

Malformed, duplicate, forged, tampered, wrong-kind, and wrong-relay
attribution all fail closed to the raw event signer. `respond-to=nobody`
remains absolute. Old/mixed-version messages without the explicit
provenance retain their current fail-closed behavior.

## Trust boundary

The workflow owner means **“scheduled by,” not “authored every rendered
word.”** Trigger-controlled substitutions may still produce ordinary `p`
mention routing for compatibility, but they cannot mint
`buzz:workflow-mention` authority. Only a target named in the durable
owner-authored step template can receive that authority.

The author gate is not bypassed: after relay signature/provenance
verification, the effective owner is evaluated under the same
`owner-only`, `allowlist`, DM, and `nobody` policies used for ordinary
messages. Owner control commands continue to use the raw event signer.

## Why this PR

This is the focused immediate fix for waking an **online** agent from a
stored workflow mention. Earlier attempts were not a finished mergeable
fix and had materially different or incomplete trust designs. Larry's
larger draft stack addresses durable delivery across restarts; that
remains valuable future work and can supersede this effective-author
path when it lands.

## Validation

At exact clean commit `fe5b55619fe44176343eefb4cb7fe180df45a7d8`:

- `buzz-relay workflow_sink`: 25/25 passed, including all four ignored
PostgreSQL cases
- `buzz-acp --lib`: 845/845 passed
- `buzz-workflow --lib`: 169/169 passed (2 unrelated PostgreSQL tests
ignored)
- warnings-denied Clippy passed for the changed Rust packages
- `cargo fmt --all -- --check` passed
- `git diff --check` passed
- repository pre-push gates passed, including branch-scoped Rust tests
- CI now selects the ACP library tests and the relay's pure + PostgreSQL
workflow-sink tests so these guards cannot silently remain unexecuted

The production event-to-author gate is shared by normal and setup
listeners and has biting regression tests for accepted explicit
attribution, legacy owner-`p` rejection, and forged-attribution
rejection.

## Exact-head local relay + ACP proof

Following the release-binary/local-relay shape in `TESTING.md`, the
exact commit above passed a fresh isolated real-process matrix using:

- a freshly recreated Postgres database with migrations
- isolated Redis
- exact-head release `buzz-relay`, `buzz`, `buzz-admin`, and `buzz-acp`
binaries
- newly provisioned owner, channel, and bot member through the CLI
- workflow creation and triggering through the running relay
- a deterministic ACP protocol subprocess capturing actual
`session/prompt` dispatches
- a NIP-11 `self` value verified against the running relay signer

Cases:

1. A stored explicit workflow mention woke an `owner-only` agent exactly
once.
2. A workflow message without an agent mention did not wake it.
3. A non-relay signer forging every workflow authority tag did not wake
it.
4. Trigger-controlled `{{trigger.text}}` containing `@Wake Agent`
retained ordinary `p` routing but received no authority-bearing
workflow-mention tag and did not wake the agent.
5. `respond-to=nobody` remained absolute for a valid relay-authenticated
workflow mention.

The deterministic ACP subprocess isolates and directly proves relay →
ACP authorization and prompt dispatch without depending on external
model behavior.

## Deployment and residual risk

Relay and ACP changes must be deployed together for the new wake
behavior; mixed versions fail closed. Production paired-deployment proof
remains distinct from the successful local integration run. Setup-mode
behavior has automated coverage but was not a separate case in the
five-case local matrix. Relay-key rotation is observed at ACP
startup/reconnect; transient NIP-11 errors retain the last verified key,
an intentional availability tradeoff documented in code.

---------

Signed-off-by: Pinky <5f5ab050ec58ae208332edd544ebf705221e24c1b86d82a6ca07038a7a8f6ac9@buzz.block.builderlab.xyz>
Signed-off-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Pinky <5f5ab050ec58ae208332edd544ebf705221e24c1b86d82a6ca07038a7a8f6ac9@buzz.block.builderlab.xyz>
Co-authored-by: LioLionel <62820906+LioLionel@users.noreply.github.com>
Co-authored-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz>
Co-authored-by: Carl <9d00794d3df50972eb8b615511783cab12a77a8fd5dd5edd58073ec73b54bd8b@buzz.block.builderlab.xyz>
2026-08-31 16:24:09 -04:00
Phil Azar
f463e726dd fix: retrieving cold memories; add regression task (#6950)
## Why

Evaluating buzz agent memory retrieval by seeding a memory then asking
the buzz agent a question it needs that memory.

**Bug Found**: System prompt had no inclusion of retrieving cold
memories and suggested looking in a mem/*.md directory that does not
exist. Updated `system-prompt.md` to include memory CLI tools and usage.

Eval Before System Prompt Change: 0/3 
Eval After System Prompt Change: 3/3 

## What

- Add a `memory-retrieval` benchmark that seeds agent memory with `buzz
mem set` before asking a direct question.
- Grade the observable threaded answer without inspecting tool calls or
exposing the answer in channel history.
- Teach agents to use `buzz mem set`, `buzz mem ls`, and `buzz mem get`
for cold memory.
- Add a wire-debug endpoint configuration for diagnosing ACP tool calls
in local runs.
- Add fixture, seeding, verifier, and prompt coverage.

## Risk Assessment

Low. The runtime changes are limited to the benchmark harness. The
production-facing change clarifies existing memory commands in the base
prompt; it does not change memory storage, relay behavior, or
authorization.

## References

- Before the system-prompt changes, 0/3 attempts passed because agents
never invoked the `buzz mem` CLI and instead searched a non existent
filesystem
- After the changes, 3/3 attempts passed. ACP wire logs confirmed that
every agent ran `buzz mem ls` followed by `buzz mem get` and returned
`net_gpv`.

---------

Signed-off-by: Philip Azar <pazar@squareup.com>
2026-08-31 17:26:24 +00:00
tulsi
69096c9a8d preserve channel description paragraph breaks (#6946)
**Category:** fix
**User Impact:** Channel descriptions now keep their paragraph structure
everywhere people and agents read them.

**Problem:** Multi-paragraph channel descriptions were flattened in the
desktop interface and in agent context, making operational instructions
harder to scan and potentially changing their meaning. A running agent
harness could also continue using a stale description after someone
edited it.

**Solution:** Preserve authored line and paragraph breaks across the
desktop surfaces, and render descriptions as bounded, indented
agent-context blocks with semantic delimiters escaped. Prompt-visible
channel metadata now refreshes before each turn with cached fallback,
while one shared desktop derivation keeps the header and channel intro
consistent.

<details>
<summary>File changes</summary>

**crates/buzz-acp/src/pool.rs**
Refresh prompt-visible kind-39000 metadata with one bounded request per
turn so edits reach running harnesses, while retaining cached metadata
as a degradation fallback. Add regression coverage for description edits
and update request-count fixtures for the refresh lifecycle.

**crates/buzz-acp/src/prompt_framing.rs**
Add semantic text escaping for untrusted values embedded inside paired
prompt sections.

**crates/buzz-acp/src/queue.rs**
Render multi-paragraph descriptions as indented context blocks, preserve
blank lines, retain the 500-character limit, and escape semantic
delimiters. Add helper- and full-prompt tests for paragraph
preservation, UTF-8-safe truncation, and adversarial input.

**desktop/src/features/channels/lib/channelDescription.ts**
Introduce one shared channel-detail derivation and preserve newlines
between status context and authored detail text.

**desktop/src/features/channels/lib/channelDescription.test.mjs**
Cover shared field precedence, multiline descriptions, status-prefix
separation, and fallback behavior.

**desktop/src/features/channels/ui/ChannelPane.helpers.ts**
Use the shared channel-detail derivation for the channel intro instead
of maintaining a second field order.

**desktop/src/features/channels/ui/ChannelPane.helpers.test.mjs**
Verify description text wins consistently over legacy purpose text when
both are present.

**desktop/src/features/channels/ui/ChannelManagementSheetRows.tsx**
Preserve paragraph whitespace in editable and read-only channel
summaries and allow enough lines for compact multi-paragraph
descriptions.

**desktop/src/features/channels/ui/ChannelManagementSheetRows.test.mjs**
Render both channel-summary variants and assert the multiline whitespace
and four-line clamp contract.

**desktop/src/features/messages/ui/ChannelIntroBlock.tsx**
Display description line and paragraph breaks in the empty-channel
intro.

**desktop/src/features/messages/ui/ChannelIntroBlock.test.mjs**
Add render-level coverage for the intro's multiline whitespace contract.

</details>

## Reproduction steps

1. Run the desktop app and create an empty channel.
2. Give it a description with two paragraphs, including a blank line and
a multi-line second paragraph, then save and reopen channel management.
3. Confirm the empty-channel intro preserves the blank line and
subsequent line breaks.
4. Confirm the channel-management summary shows the paragraph structure
for both an editor and a read-only member rather than a detached
ellipsis.
5. Hover the channel title and confirm its tooltip keeps the
description's line breaks.
6. Add an agent, send one prompt, edit the description, then send
another prompt. Confirm the next generated agent context contains the
updated description as an indented multiline block.

## Screenshot

A real-app screenshot of the multiline channel intro and
channel-management summary is attached in the PR comments below.

Closes AIDA-1980.

---------

Signed-off-by: tulsi <tulsi@block.xyz>
2026-08-27 15:37:20 -04:00
Salman Mohammed
8f3566ad9e Deduplicate ACP thread prompt context (#6706)
## Why
This is a small isolated follow-up to
[#6503](https://github.com/block/buzz/pull/6503): remove two proven
duplicate prompt fragments without adopting its broader framing
experiment or changing Buzz-native routing and identity behavior.

## What
- Remove parsed `parent=<id>` only when it exactly duplicates parsed
`root=<id>`; retain `root=<id>`, nested parents where parent differs
from root, raw event tags, event IDs, author identity, mention fields
and rules, and the explicit `--reply-to` instruction.
- Remove the full-history retrieval hint only when complete, untruncated
conversation or thread context is included. Thread context must include
its root and cover every event rendered in the batch; mixed-thread
batches and root-missing windows retain retrieval guidance. Context that
is absent, previously delivered, or truncated also retains retrieval
guidance, and the context header, count, and messages remain unchanged.

## Risk Assessment
Low — the change is confined to Buzz ACP prompt rendering and preserves
all routing, identity, event, and retrieval information outside the two
exact duplicate cases.

## Testing
- `cargo test -p buzz-acp` — 804 unit tests and 9 lifecycle integration
tests passed on the clean rerun
- Focused mixed-thread, same-thread, and missing-root prompt regressions
passed
- The timing-sensitive `keepalive_resets_idle_past_deadline` test passed
in isolation after one unrelated full-suite flake, then passed in the
clean full rerun
- `cargo clippy -p buzz-acp --all-targets -- -D warnings`
- `cargo fmt --all -- --check`
- `just file-size-check`
- Repository pre-push gate at `1b88793574e74e22f00a1fd19bfe9aa8e86150fa`
— file-size, branch, Rust unit-test, and Desktop Tauri lanes passed
- `git diff --check`

Generated with Codex

Signed-off-by: Salman Mohammed <smohammed@squareup.com>
2026-08-26 16:48:40 -04:00
thomaspblock
de188ebf06 feat(projects): add agent and CLI project-home support (#6590)
## Summary
- inject bounded project-home identity and repository context into
managed agent sessions
- add project-aware CLI flows for creating projects, repositories,
issues, and related channels
- match project homes through the existing relay query surface, then
filter channel metadata client-side

This is Part 1 of the channel-first Projects stack. Part 2 contains
project creation and model foundations.

## Testing
- `cargo fmt --all -- --check`
- `cargo clippy -p buzz-cli -p buzz-acp --all-targets -- -D warnings`
- `cargo test -p buzz-cli -p buzz-acp` — 1,184 tests passed, 1 doc test
ignored
- full pre-push gate passed

## Post-Deploy Monitoring & Validation
- validate project-home agent context and project-aware CLI commands
against a staging relay
- healthy signals: project context matches the active channel, explicit
repo coordinates remain stable, and normal channels receive no project
block
- failure signals: cross-channel project context, duplicate project
creation, or commands targeting an unrelated repository; mitigate by
reverting this PR

---------

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
Signed-off-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>
Co-authored-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>
2026-08-26 12:09:06 -07:00
Salman Mohammed
f177f4909d Use paired tags for standing & per-turn context (#6701)
## Why
PR #6503 bundled a broader context compiler with several prompt-shape
and telemetry changes, making it difficult to isolate whether paired
XML-like top-level boundaries improve agent behavior. This keeps the
follow-up experiment focused on framing only.

## What
- Replace top-level standing and per-turn bracket headers with paired
tags while preserving inner section content and event/message framing
- Carry only metadata already present in dynamic headers as attributes,
such as thread counts, truncation, event type, and batch count
- Preserve every section body verbatim so the model executes the exact
bytes reviewed in Desktop
- Keep routing metadata, event content, batching, and delivery behavior
unchanged
- Teach Desktop diagnostics to parse the new framing while retaining
compatibility with archived bracket-framed sessions; ambiguous paired
boundaries render as the complete captured prompt

## Risk Assessment
Medium — the change is limited to agent prompt framing and diagnostic
parsing, but it affects both standing context and every dynamic agent
turn. Bodies and delivery behavior remain unchanged, and compatibility
parsing remains for prior sessions.

## References
- Follow-up to #6503, scoped only to the paired-tag framing hypothesis

## Update — 2026-08-24
- Addressed review feedback by removing entity encoding/decoding from
the execution and diagnostic paths.
- Added regressions for literal closing tags, placeholders,
entity-looking text, ampersands, and authored boundary whitespace.
- Verified `cargo test -p buzz-acp` (805 unit + 9 integration), Desktop
tests (5,412 before the main sync; 5,439 in the pre-push gate), Desktop
typechecking, and Tauri clippy/tests.
- Live local relay startup and message ingestion passed. The live ACP
reply step is blocked locally because installed Goose 1.0.23 no longer
provides the documented `goose acp` command and no alternate ACP adapter
is installed.

---

**Update Aug 25, 12:13 EDT:** Extended paired tags to per-turn top-level
sections.

- Converted context, conversation/thread context, event batches, and
steer/interrupt boundaries without changing their inner bodies.
- Preserved existing header metadata as attributes only; added no new
prompt metadata.
- Verified 806 Buzz ACP unit tests plus 9 integration tests, 5,441
Desktop tests, Desktop typechecking and formatting, and all differential
pre-push lanes.

---

**Update Aug 25, 13:58 EDT:** Addressed framing review feedback.

- Updated operational base instructions to reference the emitted
`<context>` section and added a regression guard against stale
`[Context]` instructions.
- Restored batched steer/interrupt cardinality in Desktop diagnostic
titles with focused coverage for both paths.
- Verified 807 Buzz ACP unit tests plus 9 integration tests, 5,442
Desktop tests, Desktop typechecking and formatting, and every
differential pre-push lane including Tauri clippy/tests.

---

**Update Aug 25, 15:37 EDT:** Hardened legacy transcript compatibility.

- Semantic standing-context parsing now requires paired framing at the
start and complete input consumption.
- Historical bracket-framed personas keep literal balanced tag examples
in their original section; malformed or mixed current framing displays
as the complete captured prompt.
- Added regressions for the reported legacy persona case and trailing
unframed text; verified 60 focused parser tests, 5,444 full Desktop
tests, formatting, and typechecking.

---

**Update Aug 25, 16:31 EDT:** Preserved slash-command prompt block
boundaries.

- Desktop now sets aside a valid connector-facing slash-command block
only when the following block begins recognized paired framing, parses
the semantic blocks normally, and retains the command as visible Prompt
metadata.
- Added an end-to-end transcript regression using the production
three-block ACP shape, including the user bubble, author pubkey, event
ID, and grouped prompt context.
- Verified 113 combined transcript tests, 5,445 full Desktop tests,
formatting, typechecking, and all applicable pre-push lanes.

Generated with Codex

---------

Signed-off-by: Salman Mohammed <smohammed@squareup.com>
2026-08-26 12:05:41 -04:00
Salman Mohammed
f99532585a refactor(acp): clarify agent prompt sections (#6501)
## Why
The ACP prompt puts a machine-specific Workspace prefix before static
base guidance and labels the user-facing agent instruction layer as the
generic System section. Because the cwd varies by launch and worktree,
leading with it reduces reusable prompt-prefix stability.

`[Workspace]` was added in [PR
#1194](https://github.com/block/buzz/pull/1194) as a defensive fix after
a broken `~/.sprout` → `~/.buzz` migration caused agents to scan `$HOME`
and trigger macOS TCC prompts. This change retains that grounding while
shrinking it to the current working directory and moving dynamic
environment context after the static Base prompt.

## What
- Emit the prompt in Base → Workspace → Agent Instructions order
- Reduce Workspace to `Current working directory: <absolute path>`
- Resolve cwd as an absolute native-platform path and preserve Windows
drive/UNC paths instead of checking for a leading `/`
- Emit Agent Instructions for persona and standalone agent instructions
across modern and legacy ACP paths
- Preserve parsing for archived observer frames that used System or the
former Workspace-before-Base order, and align the persona catalog label

## Risk Assessment
Medium-low — this changes prompt framing for every newly created agent
session. Existing archived observer frames remain parseable, and
execution still uses the same ACP working directory. Cwd resolution now
fails clearly instead of substituting `/` when the process directory
cannot be resolved.

## References
- https://github.com/block/buzz/pull/1103
- https://github.com/block/buzz/pull/1194

Generated with Codex

---------

Signed-off-by: Salman Mohammed <smohammed@squareup.com>
2026-08-22 08:29:41 -06:00
Salman Mohammed
025425591e fix(benchmarks): wait for scripted event delivery (#6487)
## Why

The Buzz-native runtime could stop a solo agent after one turn even when
a scripted follow-up event was queued or already running. That could
turn a harness timing race into a benchmark failure.

Follow-up to #6448.

## What

- Treat solo tasks with scripted events separately from ordinary
one-turn tasks.
- Wait until turn counts and authored message IDs remain idle and
unchanged for several polls before stopping the agent.
- Remove the message-count shortcut that could stop an active turn.
- Add focused regressions for a delayed follow-up turn and an
already-running turn with a `DONE:` message.

## Testing

- `pytest -q tests/test_container_runtime.py -k "solo_turn_end or
scripted_events"` — 3 passed
- `ruff check` on the three touched Python files
- `ruff format --check` on the three touched Python files
- Local `buzz-native-solo-luna.yaml` Harbor run, one attempt per
affected task:
- `cross-thread-requests` — reward 1.0; all 6 task-specific checks
passed
- `interleaved-agent-reports` — reward 1.0; all 7 task-specific checks
passed
  - 2 completed trials, 0 exceptions, 56 seconds total

## Risk

Low. This only changes completion detection for solo benchmark tasks
with scripted events. Ordinary solo tasks retain immediate completion
after their first turn ends.

---
**Update Aug 21, 13:07 EDT:** Replaced the fixed settle delay with exact
delivery receipts after [review
feedback](https://github.com/block/buzz/pull/6487#discussion_r3832050452).

- ACP now records the event IDs delivered by each completed turn; the
runtime also recognizes existing successful-steer receipts.
- Scripted trials stop only after every expected event ID is
acknowledged and no turn is active. Missing receipts wait for the trial
budget instead of producing partial evidence.
- TDD regression delayed delivery beyond the old five-poll boundary and
failed before the implementation; the receipt parser and Rust receipt
format are pinned independently.
- Focused verification: 31 container-runtime tests passed, 6 ACP
delivery tests passed, plus Ruff, rustfmt, and Clippy.
- Receipt-gated Luna run: 2 completed trials and 0 exceptions.
`interleaved-agent-reports` scored 1.0. `cross-thread-requests`
completed both calculations and thread isolation but Luna omitted the
user mention on ALPHA, so that model-output dimension scored 0.
- Risk remains low: the ACP production change adds delivery receipt
logging and restores a missing turn-end log on a
completed-before-control race; it does not change queue dispatch
behavior.

Generated with Codex

---------

Signed-off-by: Salman Mohammed <smohammed@squareup.com>
2026-08-21 16:09:14 -04:00
Will Pfleger
c6e3fe7dfa feat(workflows): reply in-thread from send_message action (#6178)
The `send_message` workflow action could only post new top-level channel
messages. This adds a `reply_in_thread` option so a `message_posted`
workflow can reply in the triggering message's thread, plus a
`trigger_is_reply` filter variable so a workflow can fire only on
top-level messages.

Every NIP-10 thread-marker reader in the tree now routes through one
shared parser and one collapse rule in `buzz-core`, deleting four
hand-rolled copies that had drifted on id-validity and marker semantics.

## What changed

- **Schema** (`crates/buzz-workflow/src/schema.rs`): new
`reply_in_thread: bool` (serde default `false`) on
`ActionDef::SendMessage`. `validate()` rejects `reply_in_thread: true`
on `schedule`/`webhook` triggers — they carry no message to reply to.
- **Executor** (`crates/buzz-workflow/src/executor.rs`): threads
`reply_to: Option<&str>` (the trigger `message_id`) through
`ActionSink::send_message` when `reply_in_thread` is set; errors clearly
if the trigger has no `message_id`. `resolve_step_templates` carries the
new field. Adds `trigger_is_reply` (boolean) to the eval context.
- **Relay sink** (`crates/buzz-relay/src/workflow_sink.rs`): when
`reply_to` is set, resolves parent/root/depth from the known trigger
event, persists real thread metadata instead of the hardcoded top-level
`depth: 0`, and pushes the live kind:39005 thread-summary overlay after
insert so subscribed desktops update the root's reply badge without
refetching — matching the human ingest path. Emits the same NIP-10
`e`-tag shape as `buzz_sdk::builders::thread_tags`: a single `["e", id,
"", "reply"]` tag for a direct reply (parent == root), and the `root` +
`reply` pair only when nested — so every writer produces one wire shape
per reply kind.
- **Ingest resolver** (`crates/buzz-relay/src/handlers/ingest.rs`): new
`resolve_relay_reply_thread_meta` + `ReplyAncestry`. Unlike the
client-facing `resolve_nip10_thread_meta` (which validates
client-supplied tags), this computes root/depth from the known parent
and enforces the same same-channel and depth-limit invariants. When the
parent has no `thread_metadata` row, both resolvers share
`derive_ancestry_from_parent_tags`, which applies
`ThreadMarkers::resolve()`: a marked nested parent remains depth 2,
while a root-only, malformed, or unmarked parent is top-level and starts
its reply thread at itself. Both the client resolver and the
parent-ancestor fallback now read markers via the shared parser instead
of hand-scanning `e`-tags.
- **Shared NIP-10 parser** (`crates/buzz-core/src/nip10.rs`):
`parse_thread_markers` reads an event's `root`/`reply` markers, honoring
a marker only when its event id is exactly 64 ASCII-hex characters.
`ThreadMarkers::resolve()` is the single definition of the (root, reply)
→ (root_id, parent_id) collapse: `root`+`reply` as-is, `reply`-only →
`(reply, reply)`, a lone `root` or neither → top-level. A slice-based
`parse_thread_markers_from_parts` entry point serves consumers holding
raw JSON tag arrays. Relay ingest, the workflow `trigger_is_reply`
predicate, ACP anchoring, and the CLI reply resolver all call these, so
they cannot drift on marker, id-validity, or collapse semantics.
- **ACP** (`crates/buzz-acp/src/queue.rs`): `parse_thread_tags`
delegates marker parsing and collapse to `buzz_core::nip10`, keeping
only its local `p`-tag mention collection. This fixes a parity gap with
ingest: a malformed non-64-hex marker id is no longer counted as a
thread link, and a lone `root` marker is now top-level rather than being
treated as root == parent.
- **CLI** (`crates/buzz-cli/src/commands/messages.rs`):
`find_root_from_tags` routes its JSON tags through the shared slice
parser and collapse rule. CLI replies to root-only, malformed, or
unmarked parents now correctly start at that immediate parent;
reply-only and root+reply parent ancestry remain intact.
- **Trigger context** (`crates/buzz-workflow/src/lib.rs`):
`build_trigger_context` derives `is_reply` solely from a valid NIP-10
`reply` marker (no DB hit). A lone `root` marker is top-level to ingest,
so it does not count; neither does a `reply` marker whose event id is
malformed, nor a bare/unmarked `e`-tag (mentions, quotes).
- **Desktop** (`workflowFormTypes.ts`, `WorkflowStepCard.tsx`):
`replyInThread` form field with YAML round-trip, and a "Reply to
triggering message in thread" checkbox shown only for message-based
triggers. Switching to `schedule` or `webhook` clears the option on
every step before serialization, so a value hidden behind an action
change cannot resurrect when the action is switched back. The form
parser also requires `reply_in_thread` to be a boolean and refuses
`reply_in_thread: true` under an ineligible trigger, falling back to
YAML mode rather than silently normalizing a backend-invalid definition.

## Usage

```yaml
trigger:
  on: message_posted
  filter: trigger_is_reply == false
steps:
  - id: auto_reply
    action: send_message
    text: "pre-written reply text"
    reply_in_thread: true
```

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-20 19:44:21 -04:00
Will Pfleger
d274a6e949 fix(acp): guard against unrequested public relay skills (#6394)
Adds a shared base-prompt instruction that agents must not read or
blindly follow public Buzz relay skills unless a human explicitly
requests them.

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-20 13:44:06 -04:00
Salman Mohammed
2a236e4137 refactor(prompt): simplify Buzz agent guidance (#6340)
## Why
The Buzz agent prompt carries verbose agent-draft instructions that add
prompt weight without improving benchmark correctness. Buzz-native
channel trials also showed agents creating permanent channels while
claiming they had applied a one-hour TTL. The static CLI inventory
remains for efficient command discovery by smaller agents.

## What
- Retain the static Buzz CLI command table and direct agents to use
`--help` for full usage
- Compact the owner-reviewed agent-draft guidance without changing its
workflow
- Clarify that `buzz channels create` is permanent unless `--ttl` is
provided

## Benchmark Results
- Terminal-Bench retained 22/22 correctness in the initial no-table
candidate while reducing prompt size, input tokens, tool calls, and
cost; total active time was effectively flat.
- Across two Buzz-native runs, the non-channel cases tied at 26/30; the
CLI clarification directly addresses the observed TTL omission.

## Update — 2026-08-20
- Restored the CLI command table following review feedback about
multistep discovery with smaller agents. The restored-table candidate
has not been re-benchmarked.

Signed-off-by: Salman Mohammed <smohammed@squareup.com>
2026-08-20 14:59:57 +00:00
Salman Mohammed
934f3325c3 fix(buzz-acp): loosen workspace-scan guardrail to allow named paths (#6261)
## Problem

Agents refused to read a specific, user-named path outside `.buzz` (e.g.
`~/.claude/skills/context-health-check/SKILL.md`), and behaved
inconsistently — refusing in one channel, listing the same dir in
another. This is the issue Aaron and Wes hit:
buzz://message?channel=e62570dd-33ad-42c5-b92b-75f2689f9694&id=401199157f8a9734109fdddfbbce4603a1ff465fae895042f29738ad52a5d15f

Root cause: two prompt fragments told agents not to search `$HOME` "or
other directories". The intent (added in #1194 by Will Pfleger) was
narrow — stop unscoped `$HOME` sweeps that fire macOS TCC dialogs or
make a model invent its own workspace dir. But "or other directories"
reads as a blanket filesystem wall, so a cautious model generalized it
to "any path outside `.buzz`" and refused a targeted, named read.

## Fix

Prompt-only. Reframe both fragments per context-engineering guidance —
state the reason and trust judgement, rather than banning a technique:

**`crates/buzz-acp/src/pool.rs` — `workspace_section()`**
> …This is where you already are, so start here rather than scanning
`$HOME`. Any specific path the user names is fine to read.

**`crates/buzz-acp/src/base_prompt.md` line 106**
> These paths are relative to your working directory — start there for
your own files rather than scanning `$HOME` or `/`. When the user names
a specific path, read it.

Preserves the original anchor intent (start at the grounded workspace;
avoid unscoped `$HOME` sweeps) while explicitly permitting a named-path
read — the exact case that was broken.

## Testing

`cargo test -p buzz-acp` — 816 unit + 9 integration tests pass, 0
failures. Prompt-only change; the doc comment at `pool.rs:1327-1333`
already states the narrow intent and is left as-is.

Signed-off-by: Salman Mohammed <smohammed@squareup.com>
Co-authored-by: Leo <5faf251baee50ee6bcde338aef6acdd70bb3e60115664c2cd490d94a55dfc488@buzz.block.builderlab.xyz>
2026-08-19 14:54:33 -04:00
Luke Tornquist
08eb46ef3c Revert "fix(acp): gate relay-signed workflow messages on their attributed author" (#6311)
Reverts block/buzz#6129
2026-08-19 15:28:04 +00:00
Will Pfleger
50a71137e6 feat(managed-agents): close five Claude Code agent-config gaps (#4557)
Closes five Claude Code agent-config gaps in Buzz Desktop, split into
three commits that share the spawn-time and live-switch surfaces.

## Config isolation, model authority, and Auto mode (#2692, #2884,
#3493)

- **`CLAUDE_CONFIG_DIR` isolation (#3493).** `config_bridge` resolves
both `settings.json` and `.claude.json` panel paths from the agent's
effective env (`resolve_effective_agent_env` — baked floor → definition
→ global → persona → record), so the panel reads the same directory the
agent runs against. `mcp_config_file_path_for_runtime` honors a custom
dir; empty/blank is treated as unset, matching Claude's
`CLAUDE_CONFIG_DIR || homedir()` semantics. `AgentConfigPanel` shows a
Keychain caveat when a custom dir is active (a custom dir maps to a
fresh Keychain namespace unless `CLAUDE_SECURESTORAGE_CONFIG_DIR` is
also set).
- **Single startup model authority (#2692).** `ANTHROPIC_MODEL` is the
sole startup model authority for Claude. Local spawns write
`ANTHROPIC_MODEL` and strip `BUZZ_ACP_MODEL` so the harness never sees
two authorities; remote deploys send `ANTHROPIC_MODEL` in `policy_env`
instead of `BUZZ_ACP_MODEL`. Non-Claude runtimes are unchanged.
- **`PermissionMode::Auto` (#2884).** Wire string `"auto"`, model-gated,
degrades to the agent default when the active model doesn't advertise
it.

## Thinking effort end-to-end for local Claude agents

Effort flows from the running session's discovered `thought_level`
config option through the config surface to a local-only write control
and a read-only two-facts display.

- **Reader.** Discovers the `thought_level` config option from the
session cache (never hardcoded) and populates `effort_config_id` /
`effort_options` on `RuntimeConfigSurface`. The canonical effort tier
orders record env > `record.effort_level` (`BuzzExplicit`) > ACP >
persona > global > definition > file, so the panel shows the effort the
next spawn will launch with while `resolve_with_override` surfaces the
running ACP value as the struck-through override — neither masks the
other silently.
- **Write control.** `persist_agent_effort_level` is a direct-write
Tauri setter (writes `record.effort_level` + `updated_at`,
`save_managed_agents`) that rejects non-local backends — remote effort
is set at deploy time via `policy_env`. `EffortPickerField` mounts in
`AgentInstanceEditDialog` beside the Model block, gated on a local
backend **AND** a discovered `effortConfigId`. It persists directly and
invalidates the config surface, mirroring the
`setManagedAgentAutoRestart` standalone-setter precedent, so the frozen
`UpdateManagedAgentInput` shape stays frozen.
- **Display.** The read-only configured-vs-current two-facts display is
the `thinkingEffort` normalized field in `AgentConfigPanel`, fed by the
reader's canonical tier ordering.
- `buzz-acp` applies the startup effort env at session start.

## Distinguish a rejected model switch from silent success

A live model switch collapsed adapter rejection into success:
`apply_model_switch` returned `Ok` on both an accepted switch and an
application-level refusal, so the caller cached pre-switch capabilities
as if they described the target model and Desktop reported the pick as
landed.

- `ModelSwitchOutcome::{Applied(Value),Rejected}`. Transport-class
errors still propagate as `Err` (respawn the poisoned stdio); an
application-level refusal is now `Rejected`.
- The caller drives everything off `post_switch_snapshot`: `Applied`
refreshes `model_capabilities` from the target model's echoed
`configOptions` (or drops to `None` when none are echoed, so a
pre-switch snapshot is never mistaken for the target's); `Rejected`
preserves pre-switch caps and emits a `failure` `control_result`. Effort
resolution and the session-config capture read the post-switch snapshot
so they converge on the model the session actually runs;
`modelOverridden` is gated on `switch_succeeded`.
- `liveSwitchOutcome.ts` gains a distinct `"failed"` outcome for the
adapter `failure` frame and treats the busy-path `"sent"` ack as
provisional — it never counts toward success. Success is confirmed only
by a real positive terminal frame (the busy-path deferred apply emits a
correlated `switched` `control_result` when it lands), and the fallback
timeout resolves an honest `"pending"` (accepted, apply deferred), never
a false `"ok"`. `ModelPicker` surfaces a distinct toast per outcome —
failed, unsupported, and pending.

## Scope explicitly excluded

Per-agent config dir provisioning, `CLAUDE_SECURESTORAGE_CONFIG_DIR`
sentinel injection, `settings.json` projection, protected-key stripping,
MCP inheritance, spawn serialization, and the `last_spawn_warnings`
surface are absent from this diff. Silent-fallback machinery for
non-Claude runtimes (#2265/#4004) is a tracked follow-up.

## Sanctioned follow-ups

- **Live mid-conversation effort switching.** Effort is spawn-scoped
only in this PR: the worker reads `BUZZ_ACP_EFFORT_LEVEL` once and
applies it at session creation. The live effort-switch machinery
(mid-conversation effort RPC + ack frame) was deliberately removed and
is archived on `archive/claude-config-gaps-live-effort` for a future
plan-gated revival.
- **Idle-path late model-switch rejection is unobservable.** An idle
switch acks `switched` immediately after catalog validation, but the
real `set_config_option` runs at the next session creation — potentially
much later — so a rejection there is not surfaced back to the picker
(holding a subscription that long is not sensible). Pre-existing,
catalog-gated behavior; a durable fix is a tracked follow-up.

Closes #2692, #2884, #3493

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-18 16:29:28 -04:00
Salman Mohammed
d2cfd377e2 fix(prompt): simplify pickup follow-through (#6186)
## Why

The base prompt prescribed a todo lifecycle for every task needing
follow-up tools, which added control calls even for work completed in
one turn. This keeps the important behavioral contract—continue after
publishing pickup—without prescribing the mechanism.

## What

- Replace the 40-word todo lifecycle with a concise pickup
follow-through rule
- Preserve the requirement to publish the outcome or blocker before
stopping

## Risk Assessment

Low to medium — this changes managed-agent instructions, not runtime
code. The terminal benchmark covers single-turn task completion but does
not cover restart, compaction, or long-lived multi-turn recovery.

## References

- Builds on the prompt simplification in
https://github.com/block/buzz/pull/6161
- Benchmark setup: GPT 5.6 Terra at high effort; the same 11-task
Terminal-Bench 2.1 slate; four concurrent trials; 4 CPU and 8 GiB per
trial; 3× timeout

| Prompt | Pass | Active-h | Median active | Tool calls |
|---|---:|---:|---:|---:|
| PR 6161 baseline | 22/22 | 0.3438 | 0.91 min | 216 |
| Benchmarked 14-word rule | 22/22 | 0.2926 | 0.75 min | 198 |

The benchmarked rule used 14.9% less active time, 16.9% lower median
active time, and 8.3% fewer tool calls. Across the screen and
confirmation runs it passed 33/33 trials with every completion report
present; results are directional because model and tool behavior is
stochastic.

---
**Update Aug 18, 10:47 EDT:** Expanded the completion outcomes following
review feedback.
- The follow-through rule now covers a verified result, blocker, or key
decision or information that needs to be surfaced.
- The benchmark was not rerun; the table reflects the prior 14-word
formulation. This is a completion-taxonomy clarification, not a return
to a prescribed todo mechanism.

Generated with Codex

Signed-off-by: Salman Mohammed <smohammed@squareup.com>
2026-08-18 16:24:12 +00:00
Salman Mohammed
f64899e5d1 Remove Startup Recovery section in base prompt (#6161)
## Why

Managed channel sessions already receive authoritative per-turn context.
The old startup recovery checklist told every new session to scan the
global feed

## What

- Remove `Startup Recovery` with concise channel and heartbeat turn
contracts.

## Risk Assessment

Low. This changes prompt guidance and its test only; routing and runtime
behavior are unchanged.

Generated with Codex

---------

Signed-off-by: Salman Mohammed <smohammed@squareup.com>
2026-08-17 18:11:27 -04:00
Luke Tornquist
54f11219ef fix(acp): gate relay-signed workflow messages on their attributed author (#6129)
## Problem

Scheduled workflow `send_message` actions fire and land in the channel
with correct `p` tags for the mentioned agents — but the agents never
wake. The wake-up is silently dropped.

**Root cause:** workflow messages are signed by the **relay keypair**
(`workflow_sink.rs` signs with `state.relay_keypair`), so `event.pubkey`
is the relay's pubkey, not the workflow owner. In `buzz-acp`, the
inbound author gate (`author_allowed`) runs **before** the `p`-tag
mention check. Under the default `respond_to = owner-only`, the relay
pubkey is neither the owner nor a sibling, so every workflow wake-up
dies at the gate with a debug-level `"inbound author gate — dropping
event"`.

The relay-side comment even says the mention `p` tags exist *"so
mentioned agents are woken (wake is p-tag gated)"* — but wake is also
author-gated, and that path was missed.

## Fix

Gate relay-signed workflow messages on their **attributed author** — the
pubkey that created the workflow — instead of the relay pubkey:

- **Relay:** `workflow_sink.rs` now emits an explicit
`buzz:workflow-owner` tag carrying `workflow.owner_pubkey` (the workflow
creator, which the executor already passes as `author_pubkey` and whose
channel access the relay verifies before emitting). Ownership is never
inferred from `p`-tag order; mention `p` tags play no role in
attribution.
- **Harness:** at startup, `buzz-acp` fetches the relay's NIP-11 `self`
pubkey (new `RestClient::fetch_relay_self`, public `/info` endpoint).
Best-effort: fetch failure just logs a warning and preserves pre-fix
behavior.
- **Gate:** an event that is (a) authored by the relay `self` key, (b)
tagged `buzz:workflow`, and (c) carries a well-formed
`buzz:workflow-owner` pubkey is gated on that owner, through the exact
same owner/sibling/allowlist policy as a direct author.

## Security notes (all fail closed)

- No NIP-11 `self` pubkey → no exemption.
- `buzz:workflow` / `buzz:workflow-owner` tags on a non-relay-signed
event → ignored (a member cannot forge the exemption; the relay verifies
signatures on submission and only the relay holds its key).
- Relay-signed event without the tags, or with a malformed owner value
(not 64-hex) → plain author gate.
- Who is @mentioned in the message has no bearing on whose authority is
evaluated.
- A workflow owned by a random channel member still cannot wake an
owner-only agent — the owner's pubkey must pass the same policy.

## Testing

- 7 unit tests (`workflow_attributed_author_tests`) covering
attribution, fail-closed paths, p-tag independence, malformed owner
values, and the forgery case.
- Extended the PG-gated `workflow_send_message_p_tags_mentioned_member`
integration test to assert the `buzz:workflow-owner` tag.
- `cargo test -p buzz-acp`: 785 passed, 0 failed. `cargo test -p
buzz-relay --lib workflow_sink`: 17 passed. Clippy + fmt clean. (9
pre-existing `buzz-relay` failures in unrelated
`api::media`/`api::admin` tests fail identically on the base commit
without this change.)

Found while debugging scheduled automations in a Buzz review-pipeline
channel: two cron workflows fired daily @mentions at agents that never
responded, while direct human @mentions woke them instantly.

---------

Signed-off-by: Luke Tornquist <tornquist@squareup.com>
Co-authored-by: Fizz <3a9f8a30fbb462abec1e2977b2280a7ae50c7ff794433790be15bd48bfd52d0b@buzz.block.builderlab.xyz>
2026-08-17 13:29:20 -04:00