Repository navigation
feat: call an agent from its 1:1 DM (desktop and iOS) - #252
Merged
Merged
Conversation
A Call button in a 1:1 agent DM header places a proper voice call with the agent through its host's voice core (WebRTC to the host's realtime voice provider, the host answering the SDP offer), not a huddle. The host answers only after proving the signer is the verified requester of that private 1:1 DM with the agent. - agent_voice_request: one native command that signs NIP-98 as the signed-in user (exact URL, method, body hash) with X-Buzz-Community and reaches only the host's call routes (allowlisted, https, size and time caps). - AgentCallController: a port of the host's web call controller (mic, offer, data channel, time limit, disconnect grace, mute, stop, hang-up that never cancels work) following the host's call screen feed by long-poll. - AgentCallDock (mounted once in AppShell): timer, mute, hang up, stop, the call's asks, the open question's option taps, cancel/archive and the recap. - A community switch hangs up and clears the call. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Ross Hartmann <rosshartmann@RosssMacStudio.attlocal.net>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Ross Hartmann <rosshartmann@RosssMacStudio.attlocal.net>
A Call action in a 1:1 agent DM header places a proper voice call with the agent through its host's voice core, the same as desktop: the host answers the WebRTC offer only after proving the signer is the verified requester of that private 1:1 DM with the agent. - AgentCallPlugin (buzz/agent_call): stasel/WebRTC 154.0.0 pinned exactly, an outgoing CallKit call from a provider created at engine start (kept out of Recents, system mute and end), WebRTC in manual-audio mode activated by CallKit (playAndRecord, voiceChat), an in-app fallback when CallKit refuses, and interruption, route-change and media-reset handling. - Info.plist: UIBackgroundModes audio and voip; the microphone string mentions calls. - Dart: a NIP-98 client for <guest route origin>/api/buzz-voice/ios, the call screen feed model, a controller that follows the feed long-poll (asks, option taps, cancel and archive, recap), refuses while a huddle holds the audio and hangs up on a community switch, and the call screen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Ross Hartmann <rosshartmann@RosssMacStudio.attlocal.net>
Signed-off-by: Ross Hartmann <rosshartmann@RosssMacStudio.attlocal.net>
…caps Review fixes for 1:1 agent calls on desktop and iOS. - One audio owner per platform. Desktop: a call refuses while a huddle is active or starting (probe from the audio-owning HuddleProvider, then the native huddle state, before and after the mic opens); startHuddle/joinHuddle refuse while a call is busy (the huddle is refused, the call is never ended for it). iOS: the call holds agentCallAudioBusyProvider; HuddleSessionNotifier.join and MobileHuddleController._start refuse on it; the call re-checks the huddle after the native call is up; HuddleMediaPlugin never prepares, reactivates or deactivates the AVAudioSession while a call holds it. - Desktop native request: parse the URL once, check its normalised path, sign and send that same URL; ask ids reject escapes of dot, slash and backslash; answers capped by declared length and while read; the voice URL override and the HTTP-loopback allowance are debug-only. - Connect timeout (20 s) ends a still-connecting call as media_failed and closes the mic, on both platforms. - Desktop: a superseded mic request stops only its own stream; a call hung up while the offer is made never reaches the host. - iOS: peer and data-channel events act only for the current peer connection; CallKit name trimmed and capped; response cap shared with the hosted API client; in-call banner without CallKit; the Call button shows on iOS only. - Feed links show their host; no desktop Call button for agents managed on this device. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Ross Hartmann <rosshartmann@RosssMacStudio.attlocal.net>
base_urls_must_be_https_or_loopback_http now expects loopback HTTP to be accepted exactly when debug assertions are on, so it holds in release test runs too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Ross Hartmann <rosshartmann@RosssMacStudio.attlocal.net>
… starts Review follow-ups for 1:1 agent calls. - Desktop: when the native huddle state can't be read, the call refuses with a clear reason instead of assuming no huddle. - iOS: a call also refuses while a huddle start is under way (huddleStartingProvider, mirrored from MobileHuddleController), so a huddle is never announced and then rolled back for a call. - Desktop: the Call button waits for the local agent inventory, so it never flashes for an agent managed on this device. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Ross Hartmann <rosshartmann@RosssMacStudio.attlocal.net>
MobileHuddleController.join supersedes an in-flight start by bumping the generation, so the start's finally skipped its reset and the controller (and huddleStartingProvider, which agent calls read) stayed "starting" until the next leave, end, background or community switch. join now clears it like the other superseding paths. Widget test covers a join during an in-flight start. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Ross Hartmann <rosshartmann@RosssMacStudio.attlocal.net>
Signed-off-by: Ross Hartmann <rosshartmann@RosssMacStudio.attlocal.net>
RossHartmann
marked this pull request as ready for review
October 23, 2026 20:26
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Item 5e (desktop and iOS): call an agent from its 1:1 DM. This is a proper voice call with the agent through its host's voice core, the same as the host's phone and web calls. It is not a huddle.
agent_voice_request(Rust). One native command that:X-Buzz-Community;<guest route origin>/api/buzz-voice/desktop: allowlisted paths, https, 35 s timeout, no redirects;Url. Ask ids reject percent escapes of.,/and\(%2e,%2f,%5c) and malformed escapes;Content-Lengthfirst and then while reading chunks;BUZZ_DESKTOP_AGENT_VOICE_URL, and the shared HTTP-loopback allowance inacceptable_base_url, in debug builds only;{status, body}.The key never enters the webview.
AgentCallController. A port of the host's web call controller:oai-eventsdata channel, the time limit and a 5 s disconnect grace;media_failedand closes the mic;HuddleProviderregisters a probe, and the native huddle state is checked as well, both before and after the mic opens. If the huddle state can't be read, the call fails closed: it refuses and says why.startHuddleandjoinHuddlerefuse while a call is busy, with "End your call with X before you start/join a huddle." The huddle is refused and the call is never ended for it, which is the safer choice: a click elsewhere never drops a call in progress.AgentCallDock. Mounted once in AppShell. It shows the timer, mute, hang up and stop, the call's asks (working, done, now line, results, https links with their host shown beside the label), the open question's option taps, per-ask cancel and archive (feed v2) and the recap.resetCommunityState).tauri.conf.jsonis untouched (minimum macOS 13.0 from fix(desktop): raise the macOS minimum to 13.0 #251).HuddleContext.tsx,huddle_session.dart,huddle.dart,mobile_huddle_controller.dartandHuddleMediaPlugin.swift.Server side (host) is live on the Kiingo side.
iOS
dmAgentis set, and on iOS only (the native media is iOS; Android has no button).AgentCallPlugin.swift(buzz/agent_call). Mirrors the host's phone-app media layer:0c0ad84d, binary checksuma2bcdda9…), as an SPM package in Runner;playAndRecord,voiceChat), with an in-app fallback when CallKit refuses. The call screen then shows a banner saying the call isn't on the iPhone's call screen;VoiceAudioSessionOwnermarks the call's hold on theAVAudioSession. While it is set,HuddleMediaPluginnever prepares, reactivates (on interruption end) or deactivates the session.UIBackgroundModesnow hasaudioandvoip, and the microphone string mentions calls. Huddles still leave on background as before.features/agent_call.<guest route origin>/api/buzz-voice/ios. It reads answers with the hosted API's 2 MiB cap (guestAccessMaxResponseBytes), on the declared length and while streaming.media_failed, and hangs up on a community switch.agentCallAudioBusyProvider(shared/huddle/agent_call_audio.dart).HuddleSessionNotifier.joinrefuses on it, and so doesMobileHuddleController._start, before announcing a huddle. The call also refuses while a huddle start is under way (huddleStartingProvider, mirrored fromMobileHuddleController), so a huddle is never announced and then rolled back.voip background mode without PushKit (App Review)
Buzz never receives VoIP pushes and does not register with PushKit.
voipis declared because an agent call is a user-initiated outgoing CallKit call carried over WebRTC. The background mode keeps that live call's network connection and audio running while the phone is locked or another app is in front, like any outgoing VoIP call. Without it, iOS would suspend the call when the screen locks. The app runs no work in the background beyond the call the user placed, and the call ends when they hang up or the host ends it.Testing
What has run:
tsc --noEmit, biome,dart format,flutter analyze(changed paths, clean),cargo fmt,check-kiingo-fork-boundary.flutter build ios --config-only --no-codesign, thenxcodebuild … -sdk iphoneos -configuration Release CODE_SIGNING_ALLOWED=NO). BUILD SUCCEEDED..appbundle at the shipped 13.0 minimum (MACOSX_DEPLOYMENT_TARGET=13.0 CARGO_PROFILE_RELEASE_STRIP=none pnpm tauri build --bundles app --no-sign). Built Buzz.app; its Info.plist hasLSMinimumSystemVersion13.0 and the new microphone string.strip=noneworks around Rust 1.95's strip on macOS 27, where dyld rejects stripped proc-macro dylibs (Stripping debuginfo on macOS produces misaligned dylibs rust-lang/rust#157750). That isn't this PR; the fix is on Kiingo main.What has not run:
_ci-desktop.yml:60-62: desktop unit tests areif: github.repository != 'Kiingo/buzz'._ci-desktop.yml:87-88: the Tauri matrix runs clippy and check only, with nodesktop-tauri-test._ci-clients.yml:74-75: the mobile matrix has no Flutter test shards._ci-clients.yml:163: the Mobile Swift job carries the same repository guard.ci.yml.node:test:agentCallApi,agentCallControllerandagentCallFeed.test.mjs;extensions::agent_voicetests, pluscommands::guest_accessbase_urls_must_be_https_or_loopback_http;test/features/agent_call/agent_call_test.dart.🤖 Generated with Claude Code