Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
218633b
fix(link-preview): keep composer fetches user-paced (#7211)
tellaho Sep 8, 2026
c045321
fix(acp): pace targeted overflow recovery on consumer capacity (#7325)
loganj Sep 8, 2026
93761e4
fix(mobile): render push notification sender identity as npub (#7494)
loganj Sep 9, 2026
bfc3848
fix(desktop): shared npub identity foundation (canonicalNpub, PubKey …
loganj Sep 9, 2026
ad2a841
fix(desktop): npub identity displays for mention, member, and workflo…
loganj Sep 9, 2026
2226b6f
fix(desktop): npub identity controls across profile, agents, and work…
loganj Sep 9, 2026
82656ff
fix(mobile): standardize public-key identity display on npub (#7493)
loganj Sep 9, 2026
ad9591c
fix(desktop): order unnamed roster members by full canonical npub (#7…
loganj Sep 9, 2026
12023a3
fix(desktop): require a Codex adapter with Astra support (#7427)
sbddesign Sep 9, 2026
051c3a2
fix(buzz-acp): report missing models without retrying (#7538)
ngthuydiem Sep 9, 2026
cec5c8f
fix(desktop): let inbox title and message author names truncate under…
cynfria Sep 10, 2026
0020907
fix(relay): reject presence updates when Redis storage fails (#7532)
loganj Sep 10, 2026
092c6a7
fix(markdown): align mention chip wrapping (#7501)
tellaho Sep 10, 2026
813bbd1
fix(acp): integrate the Buzz Pi adapter fork (#7552)
salman1993 Sep 11, 2026
6146c4f
feat(git): add default-branch management to relay and CLI (#7562)
wesbillman Sep 11, 2026
9847b09
test(desktop): isolate login-shell probe measurements (#7570)
wesbillman Sep 11, 2026
f3408fc
fix(desktop): isolate quota backoff and reuse channel discovery roste…
wesbillman Sep 11, 2026
d074576
fix(mobile): bind same-name mentions to exact selected identities (#7…
loganj Sep 11, 2026
8670a30
Merge remote-tracking branch 'upstream/main' into fork-sync/2026-09-11
yjc801 Sep 11, 2026
b181700
fix(desktop): name Waggle in the Pi adapter restart hint
yjc801 Sep 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/_ci-relay.yml
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,19 @@ jobs:
VALUES ('00000000-0000-4000-8000-00000000c0de', 'localhost:3000')
ON CONFLICT (lower(host)) DO NOTHING
;"
- name: Git default-branch route and clone regressions
env:
# The preceding step already applied and reconciled schema/schema.sql.
BUZZ_TEST_SCHEMA_MODE: desired
BUZZ_TEST_DATABASE_URL: postgres://buzz:${{ env.BUZZ_TEST_POSTGRES_PASSWORD }}@localhost:5432/buzz
BUZZ_TEST_REDIS_URL: redis://localhost:6379
BUZZ_TEST_S3_ENDPOINT: http://localhost:9000
BUZZ_TEST_S3_BUCKET: buzz-media
run: |
cargo nextest run \
--archive-file target/ci/backend-integration-tests.tar.zst \
-E 'package(buzz-relay) and test(/api::git::settings::tests::external_infra::/)' \
--run-ignored ignored-only
- name: Workflow message provenance unit tests
# The relay's workflow_sink suite is not selected by the infra-free
# unit job. Its ignored database cases run in the isolated PostgreSQL
Expand Down
4 changes: 4 additions & 0 deletions crates/buzz-acp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,10 @@ Forum event kinds:
4. **Prompting** — When events are pending and no prompt is in flight for that channel, drains all queued events for the oldest channel into a single batched prompt via ACP `session/prompt`.
5. **Agent response** — The agent processes the prompt and uses the Buzz CLI (`send_message`, `get_messages`, etc.) to interact with Buzz.
6. **Recovery** — If the agent crashes, the harness respawns it. If the relay disconnects, the harness reconnects with a `since` filter to avoid missing events.
If the inbound queue overflows, the harness attempts replay for affected
subscriptions when capacity and relay quota permit, with at least five seconds
between attempts. Recovery depends on available relay history and the consumer
making progress; complete delivery is not guaranteed.

Each channel has at most one prompt in flight. Multiple channels can be processed concurrently when agents > 1.

Expand Down
58 changes: 58 additions & 0 deletions crates/buzz-acp/TESTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Pi adapter integration

Buzz's Pi preset uses [salman1993/pi-acp](https://github.com/salman1993/pi-acp).
Requires Node.js 22 or newer. Install Pi and configure its model provider,
then install the adapter directly from the fork:

```sh
npm install -g @earendil-works/pi-coding-agent
pi
npm install -g --install-links=true git+https://github.com/salman1993/pi-acp.git#main
```

Restart Buzz, then select **Pi** as the agent harness. Buzz starts `buzz-pi-acp`
automatically. Run the same adapter install command again to update it.
The unscoped `npm install -g pi-acp` command installs the upstream package,
without these extensions. Use fresh sessions to replace old user-framed
standing instructions.

Buzz adds `-- --skill <harness-cwd>/.agents/skills` when launching `buzz-pi-acp`.
An existing separator and explicit Pi options are preserved. Managed agents
run from the Buzz nest (normally `~/.buzz`): Desktop sets the `buzz-acp` child
CWD through `default_agent_workdir()`, and adapters inherit it. The default
skill directory is that launch workspace's `.agents/skills`. Standalone CLI
launches use the caller's working directory.
The path is fixed at adapter launch and applies to every Pi subprocess.

The full composed session prompt is sent as a replacement string through the
provisional `session/new.params._meta.systemPrompt` field. Buzz recognizes
`buzz-pi-acp` by the agent name returned during initialization, regardless of
protocol version. Upstream `pi-acp` does not receive this fork-specific metadata.
No custom capability negotiation or legacy Pi prompt fallback is used.
Session titles continue to use `_meta.sessionTitle`.

## Validation

Activate Hermit from the Buzz repository root, then run the package tests:

```sh
. ./bin/activate-hermit
cargo test -p buzz-acp
```

To exercise the real adapter through Buzz's production session composer:

```sh
BUZZ_TEST_PI_ACP=/absolute/pi-acp/dist/index.js \
cargo test -p buzz-acp real_pi_preserves -- --ignored
```

This test requires Node and Pi on PATH. It isolates HOME and Pi settings,
disables extensions and context files, and uses a synthetic transcript without
model calls. It inspects Pi's effective prompt through RPC HTML export after
switching sessions and restarting the adapter. Base, persona, team, core memory,
huddle, canvas, and the extra skill must each appear once, without another
session's instructions or Pi's default coding preamble.

HTML export reports the exporting process's current system prompt. It cannot
recover a historical prompt from an old transcript alone.
153 changes: 28 additions & 125 deletions crates/buzz-acp/src/acp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ use crate::usage::{
/// Lines exceeding this limit are rejected to prevent OOM from rogue agents.
const MAX_LINE_SIZE: usize = 10_000_000; // 10 MB

/// Package and binary name used by Buzz's Pi ACP fork.
pub(crate) const BUZZ_PI_ACP_NAME: &str = "buzz-pi-acp";

/// An MCP server configuration passed to `session/new`.
///
/// Corresponds to the `McpServerStdio` variant in the ACP schema.
Expand Down Expand Up @@ -460,8 +463,17 @@ impl AcpClient {
use std::process::Stdio;

let mut cmd = tokio::process::Command::new(command);
cmd.args(args)
.stdin(Stdio::piped())
cmd.args(args);
if crate::config::normalize_agent_command_identity(command) == BUZZ_PI_ACP_NAME {
if !args.iter().any(|arg| arg == "--") {
cmd.arg("--");
}
// Desktop launches buzz-acp in the Buzz nest; adapters inherit that
// workspace. Keep managed skills tied to launch CWD across sessions.
cmd.arg("--skill")
.arg(std::env::current_dir()?.join(".agents/skills"));
}
cmd.stdin(Stdio::piped())
.stdout(Stdio::piped())
// Inherit stderr so agent logs are visible in the harness terminal.
.stderr(Stdio::inherit())
Expand Down Expand Up @@ -637,14 +649,16 @@ impl AcpClient {
///
/// - `None` — no system-prompt field in the request (legacy framing).
/// - `Some(SystemPromptTransport::Field(text))` — bare `systemPrompt` field
/// (ACP protocol v2, buzz-agent, goose unused).
/// (ACP protocol v2, buzz-agent; goose unused).
/// - `Some(SystemPromptTransport::PiMeta(text))` — `_meta.systemPrompt`
/// as a replacement string for the Buzz pi-acp fork.
/// - `Some(SystemPromptTransport::ClaudeMeta(text))` — `_meta.systemPrompt`
/// as `{"append": text}`, keeping claude-agent-acp's native preset intact.
///
/// `session_title` rides in `_meta.sessionTitle` when `Some`; `_meta` is
/// omitted entirely otherwise, since adapters may distinguish an absent
/// member from a null one. When both `ClaudeMeta` and `session_title` are
/// present the two `_meta` members are merged into a single object.
/// member from a null one. Metadata prompt transports and the title are
/// merged into a single object.
///
/// Callers use [`extract_model_config_options`] and [`extract_model_state`]
/// to pull model info from the raw result.
Expand All @@ -663,14 +677,17 @@ impl AcpClient {
Some(SystemPromptTransport::Field(sp)) => {
params["systemPrompt"] = serde_json::Value::String(sp.to_owned());
}
Some(SystemPromptTransport::PiMeta(sp)) => {
params["_meta"]["systemPrompt"] = serde_json::Value::String(sp.to_owned());
}
Some(SystemPromptTransport::ClaudeMeta(sp)) => {
// Merge into _meta so sessionTitle (set below) is not clobbered.
params["_meta"]["systemPrompt"] = serde_json::json!({ "append": sp });
}
None => {}
}
if let Some(title) = session_title {
// Merge — _meta may already carry systemPrompt from ClaudeMeta above.
// Merge — _meta may already carry a system prompt from an adapter extension.
params["_meta"]["sessionTitle"] = serde_json::Value::String(title.to_owned());
}
let result = self.send_request("session/new", params).await?;
Expand Down Expand Up @@ -2131,16 +2148,18 @@ pub struct SessionNewResponse {

/// How to deliver a system prompt on `session/new`.
///
/// The two variants match the two mechanisms supported by current adapters:
///
/// - **`Field`** — bare `systemPrompt` field (ACP protocol v2, buzz-agent).
/// - **`PiMeta`** — `_meta.systemPrompt: text`, used by the Buzz pi-acp fork
/// to replace Pi's native system prompt.
/// - **`ClaudeMeta`** — `_meta.systemPrompt: {"append": text}`, used by
/// `claude-agent-acp` to append to the adapter's own native system prompt
/// while keeping its tool-use preset intact.
#[derive(Debug, Clone, PartialEq)]
pub enum SystemPromptTransport<'a> {
/// Deliver as a bare top-level `systemPrompt` field.
Field(&'a str),
/// Deliver as `_meta.systemPrompt: text`.
PiMeta(&'a str),
/// Deliver as `_meta.systemPrompt: {"append": text}`.
ClaudeMeta(&'a str),
}
Expand Down Expand Up @@ -3631,123 +3650,7 @@ mod tests {

// ── claude-agent-acp _meta.systemPrompt transport ─────────────────────

#[tokio::test]
async fn session_new_full_sends_claude_meta_system_prompt_when_claude_meta_transport() {
// When ClaudeMeta transport is requested, the prompt must appear as
// _meta.systemPrompt: {"append": text} — never as a bare systemPrompt field.
let script = r#"
read -t 2 _init
echo '{"jsonrpc":"2.0","id":0,"result":{"protocolVersion":1,"agentCapabilities":{}}}'
read -t 2 REQ
echo '{"jsonrpc":"2.0","id":1,"result":{"sessionId":"ses_claude","_receivedRequest":'"$REQ"'}}'
sleep 1
"#;
let mut client = spawn_script(script).await;
client
.initialize()
.await
.expect("initialize should succeed");

let resp = client
.session_new_full(
"/tmp",
vec![],
Some(SystemPromptTransport::ClaudeMeta("Be concise")),
None,
)
.await
.expect("session_new_full should succeed");

let received = &resp.raw["_receivedRequest"];
assert!(
received["params"].get("systemPrompt").is_none(),
"bare systemPrompt must not be present for ClaudeMeta transport"
);
assert_eq!(
received["params"]["_meta"]["systemPrompt"]["append"].as_str(),
Some("Be concise"),
"_meta.systemPrompt.append must carry the prompt text"
);
}

#[tokio::test]
async fn session_new_full_merges_claude_meta_and_session_title_into_single_meta_object() {
// Both ClaudeMeta prompt and session_title must coexist under _meta —
// the prompt must not clobber sessionTitle or vice versa.
let script = r#"
read -t 2 _init
echo '{"jsonrpc":"2.0","id":0,"result":{"protocolVersion":1,"agentCapabilities":{}}}'
read -t 2 REQ
echo '{"jsonrpc":"2.0","id":1,"result":{"sessionId":"ses_merged","_receivedRequest":'"$REQ"'}}'
sleep 1
"#;
let mut client = spawn_script(script).await;
client
.initialize()
.await
.expect("initialize should succeed");

let resp = client
.session_new_full(
"/tmp",
vec![],
Some(SystemPromptTransport::ClaudeMeta("Be concise")),
Some("Fizz · #buzz-dev"),
)
.await
.expect("session_new_full should succeed");

let received = &resp.raw["_receivedRequest"];
assert_eq!(
received["params"]["_meta"]["systemPrompt"]["append"].as_str(),
Some("Be concise"),
"_meta.systemPrompt.append must be present"
);
assert_eq!(
received["params"]["_meta"]["sessionTitle"].as_str(),
Some("Fizz · #buzz-dev"),
"_meta.sessionTitle must be present alongside systemPrompt"
);
}

// ── Goose-native steer scaffold (PR follow-up to #1160) ──────────────

/// Helper: spawn an inert `cat` subprocess so we have a real AcpClient
/// to drive `handle_session_update` against. `cat` never writes back,
/// which is fine — these tests don't read from the agent, they just
/// feed JSON into the parser.
async fn spawn_inert_client() -> AcpClient {
AcpClient::spawn("cat", &[], &[], false)
.await
.expect("spawn cat as inert client")
}

/// Build a `session/update` JSON-RPC notification carrying a
/// `session_info_update` with the given `_meta.goose.activeRunId` value.
/// Pass `None` to omit the `activeRunId` field entirely.
///
/// `_meta` is nested inside the `update` object (per the ACP
/// `SessionInfoUpdate` schema), matching what goose and buzz-agent
/// emit on the wire.
fn session_info_update_msg(active_run_id: Option<serde_json::Value>) -> serde_json::Value {
let mut goose = serde_json::Map::new();
if let Some(v) = active_run_id {
goose.insert("activeRunId".to_string(), v);
}
let mut meta = serde_json::Map::new();
meta.insert("goose".to_string(), serde_json::Value::Object(goose));
serde_json::json!({
"jsonrpc": "2.0",
"method": "session/update",
"params": {
"sessionId": "test-session",
"update": {
"sessionUpdate": "session_info_update",
"_meta": serde_json::Value::Object(meta),
},
}
})
}
include!("acp/system_prompt_tests.rs");

#[tokio::test]
async fn active_run_id_sets_on_string() {
Expand Down
Loading
Loading