Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
ad92335
feat(relay): accept kind:30179 private managed-agent events at ingest…
tlongwell-block Aug 7, 2026
f53bbd1
fix(bench): mention the orchestrator by pubkey when posting the task …
tlongwell-block Aug 7, 2026
ee9690a
fix(cli): emit structured JSON warning when archive/unarchive owner-a…
wpfleger96 Aug 7, 2026
c71f658
Polish advanced agent setup and Welcome composer (#4926)
klopez4212 Aug 7, 2026
cd2125c
Improve video review readiness and controls (#5161)
klopez4212 Aug 7, 2026
c293b3c
fix(agent): resolve oauth cache home cross-platform (#5151)
kalvinnchau Aug 7, 2026
346ae8c
fix(buzz-agent): escalate LLM timeouts per retry and log per-call lat…
wpfleger96 Aug 7, 2026
c8743b2
Remove agent creation success modal (#5063)
klopez4212 Aug 7, 2026
626e2c3
feat(mobile): add bee pull-to-refresh (#5059)
klopez4212 Aug 7, 2026
8476ea0
Mobile: add anchored reaction popover (#5025)
klopez4212 Aug 7, 2026
daa8877
Make public starter channels best effort (#5192)
wesbillman Aug 7, 2026
60ae74b
fix(desktop): use WEBKIT_DMABUF_RENDERER_FORCE_SHM for NVIDIA/AppImag…
Chessing234 Aug 7, 2026
cc9a2f7
fix(desktop): make terminal output selectable (#4980)
wesbillman Aug 7, 2026
8630e58
fix(desktop): fence localStorage SecurityError from killing the React…
iroiro147 Aug 7, 2026
e47894a
fix(desktop): drop unhandled rejection from throwing window.Notificat…
iroiro147 Aug 7, 2026
b2ac66c
refactor(cli): replace probe/decider/detail split with single typed e…
wpfleger96 Aug 7, 2026
fb73561
feat(desktop): Projects follow-ups — access restrictions, fast loadin…
thomaspblock Aug 7, 2026
ef2ecaf
fix(desktop): defer channel visibility change to Save (#5203)
kchung Aug 7, 2026
e9925db
fix(desktop): retain distinct agent instances in autocomplete (#5202)
atishpatel Aug 7, 2026
742e8d1
fix(buzz-agent): Responses reasoning summary, Anthropic display:summa…
wpfleger96 Aug 7, 2026
1922d49
feat(desktop): adding rich link previews to messages (#3818)
tellaho Aug 7, 2026
c3c39cc
bump @tauri-apps/cli to ~2.11.4 to fix linux app icon issue (#4858)
johan456789 Aug 7, 2026
78c87ae
fix(sdk): preserve self-mention p tags in message and forum event bui…
BradGroux Aug 7, 2026
0799942
fix(mobile): keep latest messages above composer (#4981)
tellaho Aug 7, 2026
3855687
chore(release): release Buzz Desktop version 0.5.6 (#5214)
wesbillman Aug 7, 2026
2b873cf
Recover from max-token response truncation (#5223)
tlongwell-block Aug 7, 2026
a5a9240
fix(desktop): let imported and recovered identities finish onboarding…
tellaho Aug 7, 2026
dcc1231
fix(desktop): externalize boot <style> to prevent Tauri CSP nonce ove…
wpfleger96 Aug 7, 2026
74b913c
fix(desktop): isolate relay admission tests (#5221)
wesbillman Aug 7, 2026
13c9e90
chore(release): release Buzz Desktop version 0.5.7 (#5252)
wesbillman Aug 7, 2026
65834d6
infra: bind development services to loopback (#4871)
Karniej Aug 7, 2026
c7b6636
fix(buzz-agent): budget summarizer reasoning separately so it cannot …
tlongwell-block Aug 7, 2026
02f640b
feat(desktop): unify add agent flows (#5015)
tellaho Aug 7, 2026
77fef32
Merge remote-tracking branch 'upstream/main' into upstream-sync-20260808
adrienlacombe Aug 8, 2026
f08d8e4
docs(agents): track upstream rename of the link-preview cache reset
adrienlacombe Aug 8, 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
14 changes: 8 additions & 6 deletions .release/desktop-candidate.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
{
"schema": 1,
"version": "0.5.5",
"base_sha": "25a9cf1be6d245fbd7373cb1160dbc790baf5bd5",
"previous_tag": "desktop-v0.5.4",
"tag": "desktop-v0.5.5",
"commit_count": 44
"schema": 2,
"version": "0.5.7",
"base_sha": "74b913cff8512c015dc6f1a7473b253fa803f954",
"previous_tag": "desktop-v0.5.6",
"previous_base_sha": "78c87ae20e182fffdd99744d6c9ff99df82b159c",
"previous_merge_sha": "3855687e7699f1b169f179122b8bc92f433abd6d",
"tag": "desktop-v0.5.7",
"commit_count": 5
}
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -1331,7 +1331,8 @@ reconnects preserve pending avatar verification work):
- `resetRenderScopedReactionHydration()` — reaction hydration cache
- `clearSearchHitEventCache()` — search result event cache
- `clearMarkdownNodeCache()` — markdown parse-node cache
- `resetLinkPreviewTitleCache()` — link preview title cache (Buzz entity titles come from relay events)
- `resetLinkPreviewMetadataCache()` — link preview metadata cache (Buzz entity titles come from relay events)
- `resetBackgroundMediaUploads()` — in-flight background media upload store

**If you add a new module-level cache, Map, or class instance that holds
community-scoped data, you must add its reset to `resetCommunityState()`.**
Expand Down
99 changes: 99 additions & 0 deletions CHANGELOG.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -157,9 +157,17 @@ async def run(
)
# The task arrives exactly as it would in production Buzz: a
# user prompt @mentioning the orchestrator. The harness never
# speaks as any agent.
# speaks as any agent. The orchestrator is mentioned by pubkey,
# not by name resolution: task text is untrusted payload, and any
# @-token inside it (e.g. Vim's `:%normal! @a`) would otherwise
# fail member resolution and kill the trial before the agent
# ever saw the task. An explicit --mention demotes unresolved
# @-tokens in the text to presentation-only.
await self._send(
trial.user, trial, f"@{orchestrator.agent_id} {instruction}"
trial.user,
trial,
f"@{orchestrator.agent_id} {instruction}",
mention=orchestrator.nostr_pubkey,
)
final_message = await asyncio.wait_for(
self._wait_for_done(environment, orchestrator, trial, agents + infra),
Expand Down Expand Up @@ -519,18 +527,24 @@ async def _verify_m1_output(
)

async def _send(
self, credential: AgentCredential, trial: TrialHandle, content: str
self,
credential: AgentCredential,
trial: TrialHandle,
content: str,
*,
mention: str | None = None,
) -> None:
await self._buzz_json(
credential,
trial,
args = [
"messages",
"send",
"--channel",
trial.channel_id,
"--content",
content,
)
]
if mention is not None:
args += ["--mention", mention]
await self._buzz_json(credential, trial, *args)

async def _buzz_json(
self, credential: AgentCredential, trial: TrialHandle, *args: str
Expand Down
31 changes: 31 additions & 0 deletions benchmarks/harbor-buzz-orchestra/tests/test_container_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,37 @@ async def test_m1_output_probe_matches_grader_and_is_condition_scoped(
assert bool(probed) == (condition == "M1-hello-world")


async def test_send_mentions_by_pubkey_so_task_text_stays_inert(
tmp_path, monkeypatch
):
"""Task text is untrusted payload: `:%normal! @a` in a task statement must
not be fed to member-name resolution (it would fail and kill the trial).
An explicit --mention pins delivery to the orchestrator's pubkey."""
rt = runtime(tmp_path)
orch = credential("orch-1", "orchestrator", "orch-model")
trial = trial_handle((orch,))
calls = []

async def buzz_json(credential, trial, *args):
calls.append(args)
return {}

monkeypatch.setattr(rt, "_buzz_json", buzz_json)

await rt._send(
trial.user,
trial,
"@orch-1 run `:%normal! @a` on the file",
mention=orch.nostr_pubkey,
)
assert calls[-1][-2:] == ("--mention", "pubkey-orch-1")

# Without an explicit mention the send is unchanged (name resolution).
await rt._send(trial.user, trial, "plain content")
assert "--mention" not in calls[-1]
assert calls[-1][-2:] == ("--content", "plain content")


async def test_wait_for_done_requires_orchestrator_authorship(tmp_path, monkeypatch):
rt = runtime(tmp_path, poll_seconds=0)
orch = credential("orch-1", "orchestrator", "orch-model")
Expand Down
1 change: 1 addition & 0 deletions crates/buzz-agent/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ hex = { workspace = true }
sha2 = { workspace = true }
urlencoding = "2"
webbrowser = "1"
dirs = "6"

[target.'cfg(unix)'.dependencies]
nix = { version = "0.31", default-features = false, features = ["signal", "process"] }
Expand Down
75 changes: 74 additions & 1 deletion crates/buzz-agent/src/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::sync::Arc;
use serde_json::json;
use tokio::sync::{mpsc, watch, Semaphore};
use tokio::task::JoinSet;
use tracing::Instrument as _;

use crate::builtin;
use crate::config::{Config, MAX_PROMPT_BYTES, MAX_TOOL_CALLS_PER_TURN, MAX_TOOL_RESULT_BYTES};
Expand All @@ -23,6 +24,17 @@ const ERROR_REFLECTION_SUFFIX: &str =

const UNSUPPORTED_IMAGE_TOOL_MESSAGE: &str = "The current model does not support image input. The image was removed from conversation history so this turn can continue. Use a text-based inspection tool or ask the user for a textual description instead.";

/// Model-visible feedback after the provider truncates an assistant response at
/// its output-token limit. This is a user message rather than a synthetic tool
/// result because truncation can happen without a tool call (and an unpaired
/// tool result is invalid on every provider wire format).
const MAX_TOKENS_RECOVERY_MESSAGE: &str = "Your previous response exceeded the model's output token limit and was truncated. Any incomplete tool call was not run. Continue the task, breaking the work or tool call into smaller steps and keeping the response concise.";

/// A provider can repeatedly spend its entire output allowance without making
/// progress, while `max_rounds` is unbounded by default. Keep the in-turn rescue
/// finite so a persistently truncating model eventually surfaces `max_tokens`.
const MAX_TOKENS_RECOVERIES_PER_RUN: u32 = 2;

/// Remove image blocks that the provider has explicitly rejected while keeping
/// their surrounding tool result (and therefore the tool-call/result pairing)
/// intact. Returns the number of images removed; zero means the provider error
Expand Down Expand Up @@ -144,6 +156,12 @@ pub struct RunCtx<'a> {
pub history: &'a mut Vec<HistoryItem>,
pub original_task: &'a mut Option<String>,
pub handoff_count: &'a mut usize,
/// ACP v2 session identifier for this prompt turn. Used to derive
/// per-message `messageId` values that are unique within the ACP session.
/// Distinct from `session_id` (which is the ACP session); this is a
/// per-`session/prompt` random token so that IDs from one prompt invocation
/// never collide with those from another even within the same session.
pub run_id: String,
/// Cache-summed input tokens reported by the provider on this session's
/// most recent request (persists across `session/prompt` calls), or `None`
/// before the first response and immediately after a handoff resets the
Expand Down Expand Up @@ -255,6 +273,10 @@ impl RunCtx<'_> {
// per-session: a fresh prompt deserves a fresh chance to recover, and
// `max_rounds` defaults to 0 (unbounded) so it cannot bound this.
let mut context_recoveries = 0u32;
// Per-run output-truncation recovery budget. Unlike context recovery,
// these successful provider requests consume a real round and are not
// refunded; this counter only bounds the default-unlimited case.
let mut max_tokens_recoveries = 0u32;
loop {
if self.cfg.max_rounds > 0 && round >= self.cfg.max_rounds {
return Ok(StopReason::MaxTurnRequests);
Expand Down Expand Up @@ -292,7 +314,8 @@ impl RunCtx<'_> {
let response_result = tokio::select! {
biased;
_ = self.cancel.changed() => return Ok(StopReason::Cancelled),
r = self.llm.complete(self.cfg, self.system_prompt, self.history, &tools, self.effective_model) => r,
r = self.llm.complete(self.cfg, self.system_prompt, self.history, &tools, self.effective_model)
.instrument(tracing::info_span!("llm", session_id = %self.session_id)) => r,
_ = async {
// Keepalive ticker: emit a lightweight session update every 30s
// while waiting on the LLM provider. This resets the ACP harness
Expand Down Expand Up @@ -460,13 +483,31 @@ impl RunCtx<'_> {
self.emit_usage_update().await;
}

// Stable per-kind message IDs for ACP v2 ContentChunk compliance.
// ACP v2 requires every ContentChunk to carry `messageId`; all chunks
// that belong to the same logical message must share the same ID, and
// IDs must be unique per message within the ACP session.
//
// A provider round produces at most one thought and one assistant
// message (the parsers collapse all provider output into one
// LlmResponse.reasoning string and one LlmResponse.text string).
// These are two *distinct* logical messages, so they get distinct IDs.
//
// `run_id` is a fresh random token per `session/prompt` invocation,
// so `<run_id>-thought-<round>` and `<run_id>-message-<round>` are
// unique within the ACP session even across multiple prompts.
//
// ACP v1 allows the field, so this is a backwards-safe addition.
let thought_msg_id = format!("{}-thought-{round}", self.run_id);
let message_msg_id = format!("{}-message-{round}", self.run_id);
if !response.reasoning.is_empty() {
wire::send(
self.wire,
wire::session_update(
self.session_id,
json!({
"sessionUpdate": "agent_thought_chunk",
"messageId": &thought_msg_id,
"content": { "type": "text", "text": &response.reasoning }
}),
),
Expand All @@ -481,13 +522,45 @@ impl RunCtx<'_> {
self.session_id,
json!({
"sessionUpdate": "agent_message_chunk",
"messageId": &message_msg_id,
"content": { "type": "text", "text": &response.text }
}),
),
)
.await;
}

// `max_tokens` describes a truncated assistant response, not turn
// completion. Never execute tool calls from it: although one may
// parse as valid, a later call (or surrounding instructions) may
// have been cut off. Replay only the text, with no tool calls, so
// the history remains valid without fabricated tool results; then
// add actionable user-role feedback and ask the model to continue.
if response.stop == ProviderStop::MaxTokens {
self.history.push(HistoryItem::Assistant {
text: response.text,
tool_calls: Vec::new(),
reasoning_details: response.reasoning_details,
});
if max_tokens_recoveries >= MAX_TOKENS_RECOVERIES_PER_RUN {
tracing::warn!(
recoveries = max_tokens_recoveries,
"provider repeatedly hit output token limit; recovery budget exhausted"
);
return Ok(StopReason::MaxTokens);
}
max_tokens_recoveries = max_tokens_recoveries.saturating_add(1);
tracing::warn!(
recovery = max_tokens_recoveries,
max_recoveries = MAX_TOKENS_RECOVERIES_PER_RUN,
discarded_tool_calls = response.tool_calls.len(),
"provider hit output token limit; asking model to continue in smaller steps"
);
self.history
.push(HistoryItem::User(MAX_TOKENS_RECOVERY_MESSAGE.to_string()));
continue;
}

if response.tool_calls.is_empty() {
if response.stop == ProviderStop::ToolUse {
return Err(AgentError::Llm(
Expand Down
28 changes: 15 additions & 13 deletions crates/buzz-agent/src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -453,15 +453,12 @@ fn cache_path_for(cfg: &PkceOAuthConfig) -> Result<PathBuf, AgentError> {

let dir = match &cfg.cache_dir_override {
Some(p) => p.join(&cfg.cache_namespace),
None => {
let home = std::env::var("HOME")
.map_err(|_| AgentError::Llm("oauth cache: $HOME not set".into()))?;
PathBuf::from(home)
.join(".config")
.join("buzz-agent")
.join("oauth")
.join(&cfg.cache_namespace)
}
None => dirs::home_dir()
.ok_or_else(|| AgentError::Llm("oauth cache: home directory not found".into()))?
.join(".config")
.join("buzz-agent")
.join("oauth")
.join(&cfg.cache_namespace),
};
Ok(dir.join(format!("{hash}.json")))
}
Expand Down Expand Up @@ -683,8 +680,7 @@ mod tests {
}

#[test]
fn cache_path_includes_namespace_and_hash() {
// HOME is required; cargo test runs set it.
fn cache_path_uses_platform_home_directory() {
let cfg = PkceOAuthConfig {
discovery_url: "https://example.com/.well-known".into(),
client_id: "abc".into(),
Expand All @@ -693,8 +689,14 @@ mod tests {
cache_dir_override: None,
};
let p = cache_path_for(&cfg).unwrap();
assert!(p.to_string_lossy().contains("/buzz-agent/oauth/demo/"));
assert!(p.extension().and_then(|s| s.to_str()) == Some("json"));
let expected_dir = dirs::home_dir()
.unwrap()
.join(".config")
.join("buzz-agent")
.join("oauth")
.join("demo");
assert_eq!(p.parent(), Some(expected_dir.as_path()));
assert_eq!(p.extension().and_then(|s| s.to_str()), Some("json"));
}

#[test]
Expand Down
Loading
Loading