Skip to content

Commit 93ccd19

Browse files
author
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7
committed
Merge remote-tracking branch 'origin/main' into duncan/install-ceiling-pipeline
* origin/main: (25 commits) Refine agent sharing dialog (#3699) desktop: enable getUserMedia in the Linux WebKitGTK webview (#3607) fix: align responsive agent views (#3688) Add macOS agent menu-bar menu (#3565) Fix pending message feedback (#3543) fix(desktop): remove remaining Projects panel fills (#3742) feat(mobile): desktop-parity emoji and thread experience (#3485) desktop: restore direct community member adds (#3634) fix(desktop): explain open agent access (#2561) fix(cli): resolve agents from owner records (#3178) fix(desktop): remove Projects overview card fills (#3416) feat(replica): portable heartbeat-token fence with snapshot-local reader routing (#3268) fix(git): channel binding tooling + author remediation for unbound repos (#3626) feat: configure S3 URL addressing style (#3400) feat: add first-class OpenRouter provider support (#1975) feat(agent,acp): wire provider total_tokens through NIP-AM publish chain (#3593) chore(release): release Buzz Desktop version 0.5.2 (#3624) docs: add Linux rendering troubleshooting guide (#3573) fix(desktop): discover bun-installed agent CLIs in ~/.bun/bin (#3343) feat(tracing): correlate trace IDs in relay logs (#3608) ... Signed-off-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2 parents bc6710b + 9a386a0 commit 93ccd19

216 files changed

Lines changed: 19404 additions & 2218 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,19 @@ RELAY_URL=ws://localhost:3000
8282
# BUZZ_GIT_PACK_CACHE_MAX_BYTES=5368709120
8383
# BUZZ_GIT_PACK_CACHE_MAX_CONCURRENT_POPULATIONS=2
8484

85+
# -----------------------------------------------------------------------------
86+
# S3-Compatible Object Storage (media + Git/CAS)
87+
# -----------------------------------------------------------------------------
88+
# The local MinIO container is reachable from host processes at localhost:9000.
89+
# Path style keeps the bucket in the URL path and is required by this local DNS
90+
# setup. Use `virtual` only when the provider requires bucket-as-subdomain URLs.
91+
BUZZ_S3_ENDPOINT=http://localhost:9000
92+
BUZZ_S3_ACCESS_KEY=buzz_dev
93+
BUZZ_S3_SECRET_KEY=buzz_dev_secret
94+
BUZZ_S3_BUCKET=buzz-media
95+
BUZZ_S3_REGION=us-east-1
96+
BUZZ_S3_ADDRESSING_STYLE=path
97+
8598
# -----------------------------------------------------------------------------
8699
# Media Upload Admission
87100
# -----------------------------------------------------------------------------

.github/workflows/linux-canary.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ jobs:
2121
name: Build Linux canary
2222
if: github.repository == 'block/buzz'
2323
runs-on: ubuntu-latest
24-
container: ubuntu:22.04@sha256:0e0a0fc6d18feda9db1590da249ac93e8d5abfea8f4c3c0c849ce512b5ef8982
24+
container: ubuntu:24.04@sha256:4fbb8e6a8395de5a7550b33509421a2bafbc0aab6c06ba2cef9ebffbc7092d90
2525
timeout-minutes: 60
2626
permissions:
2727
contents: read

.github/workflows/release.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -476,7 +476,7 @@ jobs:
476476
if: github.repository == 'block/buzz'
477477
runs-on: ubuntu-latest
478478
# Digest-pinned like the SHA-pinned actions below; Renovate keeps it fresh.
479-
container: ubuntu:22.04@sha256:0e0a0fc6d18feda9db1590da249ac93e8d5abfea8f4c3c0c849ce512b5ef8982
479+
container: ubuntu:24.04@sha256:4fbb8e6a8395de5a7550b33509421a2bafbc0aab6c06ba2cef9ebffbc7092d90
480480
needs: setup
481481
timeout-minutes: 60
482482
permissions:
@@ -498,7 +498,7 @@ jobs:
498498
env:
499499
DEBIAN_FRONTEND: noninteractive
500500
run: |
501-
# Must run first: bare ubuntu:22.04 ships without curl, wget, git, or
501+
# Must run first: bare ubuntu:24.04 ships without curl, wget, git, or
502502
# ca-certificates. activate-hermit bootstraps via curl+HTTPS (needs
503503
# both), and actions/checkout falls back to a REST tarball without git.
504504
# Running as root — no sudo needed.

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
# Changelog
22

3+
## v0.5.2
4+
5+
- feat(cli): mirror Desktop mention delivery ([#3330](https://github.com/block/buzz/pull/3330)) ([`7adc46268`](https://github.com/block/buzz/commit/7adc46268d5e93f0b1d4dc8e700af22815dcac1b))
6+
- fix(desktop): deduplicate relay outage notification ([#3579](https://github.com/block/buzz/pull/3579)) ([`66e705492`](https://github.com/block/buzz/commit/66e7054928cc29395f828467c3e8c81b7408dd29))
7+
- fix(desktop): reconcile thread arrivals at bottom ([#3585](https://github.com/block/buzz/pull/3585)) ([`b42a8d447`](https://github.com/block/buzz/commit/b42a8d447e3a2b85b2313dc4fdd123731fd8bba3))
8+
- Improve emoji autocomplete matching ([#3571](https://github.com/block/buzz/pull/3571)) ([`259de6afb`](https://github.com/block/buzz/commit/259de6afbe0cc0d106e57ebdb2323064990e4122))
9+
- Fix shared agent avatar import profiles ([#3578](https://github.com/block/buzz/pull/3578)) ([`324bd6b46`](https://github.com/block/buzz/commit/324bd6b464de5751e12abbd155376046ce3d2afc))
10+
- Fix inline raster avatars in agent catalog ([#3581](https://github.com/block/buzz/pull/3581)) ([`7e9b77f72`](https://github.com/block/buzz/commit/7e9b77f72d82e019a99f074f1c9829be30c57ae1))
11+
- feat(agent): make Gemini and MLflow-route models usable through databricks_v2 ([#3569](https://github.com/block/buzz/pull/3569)) ([`4a1ebf25c`](https://github.com/block/buzz/commit/4a1ebf25c782fc6a68f0a69e6f866f793a259a1f))
12+
13+
314
## v0.5.1
415

516
- perf(desktop): move observer-feed archive and decrypt commands off main thread ([#3415](https://github.com/block/buzz/pull/3415)) ([`294c8c821`](https://github.com/block/buzz/commit/294c8c821de51442a8c384c0bdb66b1a10224ca0))

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Justfile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,7 @@ test-unit:
276276
#!/usr/bin/env bash
277277
if command -v cargo-nextest &>/dev/null; then
278278
cargo nextest run -p buzz-core -p buzz-auth --lib
279+
cargo nextest run -p buzz-cli
279280
# buzz-db migrator/lint tests: pure SQL-parsing unit tests (no infra).
280281
# They guard the embedded-migrator invariant (exactly the consolidated
281282
# 0001; cutover/backfill stays an operator script, not startup state)
@@ -620,6 +621,11 @@ mobile-check:
620621
mobile-test:
621622
unset GIT_DIR GIT_WORK_TREE; cd {{mobile_dir}} && flutter test
622623

624+
# Regenerate the emoji dataset asset from desktop's emoji-mart install.
625+
# Output is committed — rerun after bumping @emoji-mart/data.
626+
mobile-emoji-data:
627+
node {{mobile_dir}}/scripts/generate-emoji-data.mjs
628+
623629
# Compile an unsigned Android debug APK (worktree-aware debug identity)
624630
mobile-build-android:
625631
./scripts/mobile-worktree-overrides.sh

crates/buzz-acp/src/base_prompt.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ For explicit changes to an existing personal agent, use `buzz agents draft-updat
4040

4141
- Use the person's **exact full display name** after `@` (e.g., `@Will Pfleger`, not `@Will`). Partial names fail silently.
4242
- Do NOT format mentions with bold, italic, or backticks — it breaks notification delivery.
43+
- When you know intended recipient pubkeys, send readable `@Name` text and pass the identities separately in the same command: `buzz messages send ... --content "@Name ..." --mention <hex-or-npub>`. Repeat `--mention` for multiple recipients. Any explicit identity (`--mention` or `nostr:npub...`) permits unresolved or ambiguous `@Name` text as presentation-only; uniquely resolved member names still add their own recipients. Include a pubkey for every presentation-only name that should notify. The success JSON's `mention_pubkeys` comes from the signed event and is the delivery evidence; no follow-up verification command is needed.
44+
- Without `--mention`, the CLI resolves `@Name` against current channel members. It stops before sending on an unresolved/ambiguous name or a mentioned pubkey that is not a member. For a non-member, add them explicitly with `buzz channels add-member` only when authorized, then retry. Sending never changes membership automatically.
4345
- Only `@mention` when you need their attention. Don't mention in narrative (e.g., "coordinating with Duncan" — no `@`). Naming someone while talking *about* them is narrative — "waiting on @morgan", "until @morgan brings work", "I'll loop in @morgan later". Drop the `@`. Every mention sends a notification; a mention nobody needs to act on is a false alarm.
4446

4547
### Callback Mentions

crates/buzz-acp/src/lib.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3625,6 +3625,22 @@ mod agent_draft_prompt_tests {
36253625
assert!(prompt.contains("single-quoted shell strings preserve `\\n` literally"));
36263626
assert!(prompt.contains("buzz messages send ... --content -"));
36273627
}
3628+
3629+
#[test]
3630+
fn shared_base_prompt_teaches_single_command_mentions_and_preflight() {
3631+
let prompt = include_str!("base_prompt.md");
3632+
assert!(prompt.contains("--mention <hex-or-npub>"));
3633+
assert!(prompt.contains("every presentation-only name that should notify"));
3634+
assert!(
3635+
prompt.contains("permits unresolved or ambiguous `@Name` text as presentation-only")
3636+
);
3637+
assert!(prompt.contains("success JSON's `mention_pubkeys`"));
3638+
assert!(prompt.contains("no follow-up verification command is needed"));
3639+
assert!(prompt.contains("stops before sending"));
3640+
assert!(prompt
3641+
.contains("add them explicitly with `buzz channels add-member` only when authorized"));
3642+
assert!(prompt.contains("never changes membership automatically"));
3643+
}
36283644
}
36293645

36303646
fn default_heartbeat_prompt() -> String {

crates/buzz-acp/src/pool.rs

Lines changed: 164 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3409,33 +3409,35 @@ fn acp_stop_to_core(r: &StopReason) -> buzz_core::agent_turn_metric::StopReason
34093409
}
34103410
}
34113411

3412-
/// Best-effort: build and publish a `kind:44200` NIP-AM agent turn metric event.
3412+
/// Build the `(turn, cumulative)` `TokenCounts` pair for a NIP-AM kind-44200
3413+
/// payload from a completed `TurnUsage`.
34133414
///
3414-
/// Does nothing when `usage` is `None` (goose emitted no usage notification
3415-
/// for this turn) or when `owner_pubkey` is unconfigured (no NIP-AO identity).
3416-
/// Errors are logged at WARN and never surface to the caller — metric
3417-
/// publishing must never fail a turn.
3418-
async fn publish_agent_turn_metric(
3419-
ctx: &PromptContext,
3420-
usage: Option<crate::usage::TurnUsage>,
3421-
channel_id: Option<uuid::Uuid>,
3422-
session_id: &str,
3423-
turn_id: &str,
3424-
stop_reason: Option<buzz_core::agent_turn_metric::StopReason>,
3415+
/// Extracted as a pure function so the mapping logic can be tested independently
3416+
/// of relay/crypto infrastructure. `publish_agent_turn_metric` is the only
3417+
/// production caller.
3418+
///
3419+
/// - `turn` is `None` when `delta_reliable` is false; otherwise it carries the
3420+
/// per-turn i/o/total/cost deltas for this turn.
3421+
/// - `cumulative` always carries the session-aggregate i/o/cost totals.
3422+
/// `total_tokens` is `Some` only when the session accumulated a genuine
3423+
/// provider-reported total on every turn — never derived from i/o sums
3424+
/// (NIP-AM MUST NOT).
3425+
pub(crate) fn build_turn_metric_counts(
3426+
usage: &crate::usage::TurnUsage,
3427+
) -> (
3428+
Option<buzz_core::agent_turn_metric::TokenCounts>,
3429+
Option<buzz_core::agent_turn_metric::TokenCounts>,
34253430
) {
3426-
use buzz_core::agent_turn_metric::{AgentTurnMetricPayload, TokenCounts};
3427-
use nostr::{EventBuilder, Kind, Tag};
3428-
3429-
let (usage, owner_pk) = match (usage, ctx.agent_owner_pubkey.as_ref()) {
3430-
(Some(u), Some(pk)) => (u, pk),
3431-
_ => return,
3432-
};
3431+
use buzz_core::agent_turn_metric::TokenCounts;
34333432

34343433
let turn_counts = if usage.delta_reliable {
34353434
Some(TokenCounts {
34363435
input_tokens: usage.turn_input_tokens,
34373436
output_tokens: usage.turn_output_tokens,
3438-
total_tokens: None,
3437+
// Field-local: present only when both the previous and current
3438+
// cumulative totals were available and monotonic. Never derived
3439+
// from input+output.
3440+
total_tokens: usage.turn_total_tokens,
34393441
cost_usd: usage.turn_cost_usd,
34403442
cache_read_tokens: None,
34413443
cache_write_tokens: None,
@@ -3450,11 +3452,40 @@ async fn publish_agent_turn_metric(
34503452
let cumulative_counts = Some(TokenCounts {
34513453
input_tokens: Some(usage.cumulative_input_tokens),
34523454
output_tokens: Some(usage.cumulative_output_tokens),
3453-
total_tokens: None,
3455+
// Present when every turn in the session reported a genuine provider
3456+
// total. None when the session has never emitted one or any turn lacked
3457+
// one. Never derived from input+output (NIP-AM MUST NOT).
3458+
total_tokens: usage.cumulative_total_tokens,
34543459
cost_usd: usage.cumulative_cost_usd,
34553460
cache_read_tokens: None,
34563461
cache_write_tokens: None,
34573462
});
3463+
(turn_counts, cumulative_counts)
3464+
}
3465+
3466+
/// Best-effort: build and publish a `kind:44200` NIP-AM agent turn metric event.
3467+
///
3468+
/// Does nothing when `usage` is `None` (goose emitted no usage notification
3469+
/// for this turn) or when `owner_pubkey` is unconfigured (no NIP-AO identity).
3470+
/// Errors are logged at WARN and never surface to the caller — metric
3471+
/// publishing must never fail a turn.
3472+
async fn publish_agent_turn_metric(
3473+
ctx: &PromptContext,
3474+
usage: Option<crate::usage::TurnUsage>,
3475+
channel_id: Option<uuid::Uuid>,
3476+
session_id: &str,
3477+
turn_id: &str,
3478+
stop_reason: Option<buzz_core::agent_turn_metric::StopReason>,
3479+
) {
3480+
use buzz_core::agent_turn_metric::AgentTurnMetricPayload;
3481+
use nostr::{EventBuilder, Kind, Tag};
3482+
3483+
let (usage, owner_pk) = match (usage, ctx.agent_owner_pubkey.as_ref()) {
3484+
(Some(u), Some(pk)) => (u, pk),
3485+
_ => return,
3486+
};
3487+
3488+
let (turn_counts, cumulative_counts) = build_turn_metric_counts(&usage);
34583489
let timestamp = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
34593490
let payload = AgentTurnMetricPayload {
34603491
harness: ctx.harness_name.clone(),
@@ -5238,9 +5269,11 @@ mod tests {
52385269
delta_reliable: true,
52395270
turn_input_tokens: Some(100),
52405271
turn_output_tokens: Some(50),
5272+
turn_total_tokens: None,
52415273
turn_cost_usd: None,
52425274
cumulative_input_tokens: 100,
52435275
cumulative_output_tokens: 50,
5276+
cumulative_total_tokens: None,
52445277
cumulative_cost_usd: None,
52455278
model: None,
52465279
};
@@ -5270,9 +5303,11 @@ mod tests {
52705303
delta_reliable: true,
52715304
turn_input_tokens: Some(200),
52725305
turn_output_tokens: Some(80),
5306+
turn_total_tokens: None,
52735307
turn_cost_usd: Some(0.001),
52745308
cumulative_input_tokens: 200,
52755309
cumulative_output_tokens: 80,
5310+
cumulative_total_tokens: None,
52765311
cumulative_cost_usd: Some(0.001),
52775312
model: None,
52785313
};
@@ -5303,9 +5338,11 @@ mod tests {
53035338
delta_reliable: true,
53045339
turn_input_tokens: Some(50),
53055340
turn_output_tokens: Some(20),
5341+
turn_total_tokens: None,
53065342
turn_cost_usd: None,
53075343
cumulative_input_tokens: 150,
53085344
cumulative_output_tokens: 70,
5345+
cumulative_total_tokens: None,
53095346
cumulative_cost_usd: None,
53105347
model: None,
53115348
};
@@ -5336,9 +5373,11 @@ mod tests {
53365373
delta_reliable: false, // first turn from buzz-agent
53375374
turn_input_tokens: None,
53385375
turn_output_tokens: None,
5376+
turn_total_tokens: None,
53395377
turn_cost_usd: None,
53405378
cumulative_input_tokens: 400,
53415379
cumulative_output_tokens: 100,
5380+
cumulative_total_tokens: None,
53425381
cumulative_cost_usd: None,
53435382
model: None,
53445383
};
@@ -5354,6 +5393,110 @@ mod tests {
53545393
.await;
53555394
}
53565395

5396+
/// `build_turn_metric_counts` maps exact turn and cumulative totals from
5397+
/// `TurnUsage` to the corresponding `TokenCounts.total_tokens` fields.
5398+
/// Reverting the production fields at the call site to `None` would break
5399+
/// this test; the test constrains the real code path.
5400+
#[test]
5401+
fn test_build_turn_metric_counts_exact_totals_map_through() {
5402+
let usage = crate::usage::TurnUsage {
5403+
session_id: "sess-total".to_string(),
5404+
turn_seq: 2,
5405+
delta_reliable: true,
5406+
turn_input_tokens: Some(100),
5407+
turn_output_tokens: Some(30),
5408+
turn_total_tokens: Some(130), // genuine per-turn total
5409+
turn_cost_usd: None,
5410+
cumulative_input_tokens: 500,
5411+
cumulative_output_tokens: 120,
5412+
cumulative_total_tokens: Some(620), // genuine cumulative total
5413+
cumulative_cost_usd: None,
5414+
model: None,
5415+
};
5416+
5417+
let (turn, cumulative) = crate::pool::build_turn_metric_counts(&usage);
5418+
5419+
// Serialise to JSON — this is what ultimately goes on the wire.
5420+
let turn_json = serde_json::to_value(turn.as_ref().expect("turn counts present")).unwrap();
5421+
let cum_json =
5422+
serde_json::to_value(cumulative.as_ref().expect("cumulative counts present")).unwrap();
5423+
5424+
// Per-turn total must be the genuine provider-reported value.
5425+
assert_eq!(
5426+
turn_json["totalTokens"],
5427+
serde_json::json!(130),
5428+
"per-turn total must map to TokenCounts.totalTokens in wire JSON"
5429+
);
5430+
assert_eq!(turn_json["inputTokens"], serde_json::json!(100));
5431+
assert_eq!(turn_json["outputTokens"], serde_json::json!(30));
5432+
5433+
// Cumulative total must be the genuine session total.
5434+
assert_eq!(
5435+
cum_json["totalTokens"],
5436+
serde_json::json!(620),
5437+
"cumulative total must map to TokenCounts.totalTokens in wire JSON"
5438+
);
5439+
assert_eq!(cum_json["inputTokens"], serde_json::json!(500));
5440+
assert_eq!(cum_json["outputTokens"], serde_json::json!(120));
5441+
}
5442+
5443+
/// When totals are absent, `build_turn_metric_counts` must produce null
5444+
/// `total_tokens` — never a derived input+output sum (NIP-AM MUST NOT).
5445+
/// Reverting the production fields to hardcoded `None` would leave this test
5446+
/// passing but input/output would disagree, making the null-path detectable.
5447+
#[test]
5448+
fn test_build_turn_metric_counts_null_totals_never_derived() {
5449+
let usage = crate::usage::TurnUsage {
5450+
session_id: "sess-nototal".to_string(),
5451+
turn_seq: 1,
5452+
delta_reliable: true,
5453+
turn_input_tokens: Some(200),
5454+
turn_output_tokens: Some(60),
5455+
turn_total_tokens: None, // provider did not supply a total
5456+
turn_cost_usd: None,
5457+
cumulative_input_tokens: 200,
5458+
cumulative_output_tokens: 60,
5459+
cumulative_total_tokens: None, // session has no total
5460+
cumulative_cost_usd: None,
5461+
model: None,
5462+
};
5463+
5464+
let (turn, cumulative) = crate::pool::build_turn_metric_counts(&usage);
5465+
5466+
let turn_json = serde_json::to_value(turn.as_ref().expect("turn counts present")).unwrap();
5467+
let cum_json =
5468+
serde_json::to_value(cumulative.as_ref().expect("cumulative counts present")).unwrap();
5469+
5470+
// total_tokens must be null in the wire JSON.
5471+
assert!(
5472+
turn_json["totalTokens"].is_null(),
5473+
"absent turn total must serialize as null — not derived from in+out"
5474+
);
5475+
assert!(
5476+
cum_json["totalTokens"].is_null(),
5477+
"absent cumulative total must serialize as null — not derived from in+out"
5478+
);
5479+
5480+
// Input/output must still carry their real values.
5481+
assert_eq!(
5482+
turn_json["inputTokens"],
5483+
serde_json::json!(200),
5484+
"inputTokens must be present even when total is absent"
5485+
);
5486+
assert_eq!(
5487+
turn_json["outputTokens"],
5488+
serde_json::json!(60),
5489+
"outputTokens must be present even when total is absent"
5490+
);
5491+
5492+
// The null total must not equal the input+output sum — it must be genuinely null.
5493+
let derived_sum = serde_json::json!(200u64 + 60u64);
5494+
assert_ne!(
5495+
turn_json["totalTokens"], derived_sum,
5496+
"total_tokens must never equal input+output when provider omitted it"
5497+
);
5498+
}
5499+
53575500
fn make_prompt_context_no_owner() -> PromptContext {
53585501
let agent_keys = nostr::Keys::generate();
53595502
make_prompt_context_impl(&agent_keys, None)

0 commit comments

Comments
 (0)