Skip to content

feat(desktop): deliver background-agent completions into live voice sessions#10257

Open
skanderkaroui wants to merge 12 commits into
mainfrom
claude/voice-completion-delivery
Open

feat(desktop): deliver background-agent completions into live voice sessions#10257
skanderkaroui wants to merge 12 commits into
mainfrom
claude/voice-completion-delivery

Conversation

@skanderkaroui

@skanderkaroui skanderkaroui commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Problem

A voice-spawned background agent's completion never reaches the live voice conversation. The kernel journals the run and the floating pill turns green, but nothing converts "done" into conversation input for the realtime model — so it cannot mention the result or answer "what did it find?". The pull API built for exactly this (DesktopCoordinatorService.peekCompletedAgentDelta, with per-surface seen-id + high-water checkpoints and a prompt that already says "Do not read raw ids aloud") had zero callers.

Change

AgentCompletionVoiceDelivery (new, Chat/) wires that orphaned pipeline to the realtime hub:

  • Observes AgentRuntimeStatusStore projections for transitions into a terminal state on background surfaces only (floating_bar/service/workstream) — ordinary main-chat/voice answers never trigger kernel delta reads.
  • Peeks the canonical kernel completion delta under the realtime_voice checkpoint namespace and injects the existing delta prompt into the live provider session as silent context via the same buffered text-input path PTT uses (sendBackgroundAgentContext, new thin wrapper beside sendTestTextInput). No synthetic response is requested — the voice turn coordinator's output-lease authority and the one-start/one-terminal turn analytics contract are untouched; the model mentions the completion at its next natural turn.
  • Acknowledges the checkpoint only after the session confirms the send; undelivered completions retry on the next transition or on hubDidConnect (which also drains completions that finished while no session was live).

Hooks are minimal: 1 line in hubDidConnect, 1 startup call in OmiApp, a 2-member controller extension in its own file.

Product invariants affected

  • INV-AUTH-1 — path-matched (RealtimeHub*); no session-death or auth behavior changes. The new code performs no auth calls; it forwards text into an already-authenticated session.
  • INV-CHAT-1 — path-matched; no chat write-path changes. Nothing journals, appends timelines, or creates turns — injection is provider-conversation context only, and checkpoint state is the pre-existing delta bookkeeping.

Verification

  • xcrun swift build -c debug --package-path Desktop clean.
  • New behavioral suite AgentCompletionVoiceDeliveryTests (9 tests, injected seams, no wall-clock waits): delivery+ack ordering; checkpoint unadvanced on failed inject; no-op without a live session; primary-surface terminals never trigger; repeated terminal projections fire once; cleared-and-recreated surface is a fresh transition; connect drains pending; in-flight coalescing; empty delta acks nothing.
  • ./scripts/agent-logic-harness.sh — all four gates green.
  • python3 scripts/check_desktop_test_quality.py — at/below baseline.
  • make preflight green at HEAD.

Not exercised live: a real provider voice session observing a completion on a named bundle (needs live OpenAI/Gemini audio session). Follow-up before merge or as merge-gate evidence: spawn an agent by voice on a named bundle, wait for completion, confirm the next model turn mentions it.

Deliberate scope cut

Immediate spoken announcement (model proactively speaking mid-idle) is out of scope: hubDidReceiveAudio/hubDidEmitText drop events without an accepted turn identity, so proactive speech requires a VoiceTurnCoordinator-owned announcement turn — tracked as follow-up work, not smuggled around the output authority here.

Ratchet notes

RealtimeHubSession.swift (+7) and OmiApp.swift (+3) baselines raised with one-line justifications; the controller addition lives in a new extension file instead of growing RealtimeHubController.swift past its threshold.

Failure class (fixes)

Failure-Class: none

The two follow-up fix: commits are specific instance fixes (an ack-before-delivery bug and a test-triggered runtime reach), each with a regression test. No existing failure class matches and neither warrants a new reusable guard class.

@Git-on-my-level Git-on-my-level left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for wiring this path up — the overall shape is promising, and I like that the completion delta is treated as untrusted context rather than forcing a synthetic voice response.

I do need to block this head on the delivery/acknowledgement contract: AgentCompletionVoiceDelivery acknowledges the kernel checkpoint whenever sendBackgroundAgentContext returns true, but RealtimeHubSession.sendTextInput also returns true when it merely buffers text because the socket is not open or Gemini has no activity window. That means a completion can be marked seen before it is actually delivered; if the session closes before the buffered text flushes, close() clears pendingTextInputs and the completion is lost permanently. This contradicts the PR invariant that the checkpoint advances only after confirmed delivery.

Please make the background-completion path distinguish “accepted into an in-memory buffer” from “actually sent to the provider conversation,” or otherwise delay acknowledgeCompletedAgentDelta until the buffered context is flushed successfully. A regression test should cover the socket/activity-window buffered path plus close-before-flush so this exactly-once guarantee does not regress.

Also, formal approval is intentionally withheld because this is a nontrivial desktop voice-agent behavior change and the Desktop Swift checks are still pending; it will still need maintainer/product review after the delivery-loss bug is fixed.


by AI on behalf of David — if you need David’s attention urgently, please @Git-on-my-level and escalate with need human response.

@Git-on-my-level Git-on-my-level added needs-tests PR introduces logic that should be covered by tests needs-maintainer-review Needs a human maintainer to sign off before merge labels Jul 22, 2026
@skanderkaroui
skanderkaroui force-pushed the claude/voice-completion-delivery branch from a259fb0 to 4d41f40 Compare July 22, 2026 06:58
@skanderkaroui

Copy link
Copy Markdown
Collaborator Author

Thanks — good catch, that was a real exactly-once break and I've fixed it in 3a59488 (fix(desktop): only ack a voice completion after it reaches the provider).

What changed: sendBackgroundAgentContext no longer delegates to the buffering sendTextInput. It writes to the live provider only when the session can accept it now (isOpen, and for Gemini an open activity window) and returns true; otherwise it returns false without buffering. So the checkpoint advances only after a confirmed provider write, and there is nothing in pendingTextInputs for stop()/close() to drop. Not-ready completions stay unacknowledged and retry on the next terminal transition or session connect.

Bound (documented, not a loss): a completion arriving while a live Gemini session has no open activity window is deferred until the next window/turn or reconnect — never acknowledged until actually sent.

Regression tests (RealtimeHubSessionInputLifecycleTests, the DEBUG-seam suite), covering exactly the buffered/close-before-flush path you flagged:

  • background context before transport ready → returns false, buffers nothing
  • open OpenAI session → returns true, buffers nothing
  • Gemini open socket, no activity window → returns false, buffers nothing

(new pendingTextInputCount field on the DEBUG input-lifecycle snapshot asserts "nothing buffered").

Separately, I also fixed the pending Desktop Swift CI failure it was blocked on: the Test Suites job was hanging 6h because the hubDidConnect hook drove the shared delivery singleton into the real agent runtime during RealtimeHub unit tests. The service is now inert until start() (app-launch only), so those tests can't reach the runtime (9befcb8). Agreed this still needs maintainer/product review — the live named-bundle voice verification is still outstanding and noted in the PR body.

@skanderkaroui
skanderkaroui force-pushed the claude/voice-completion-delivery branch from 9befcb8 to 45bdcc3 Compare July 22, 2026 16:33
…essions

A voice-spawned background agent's completion was invisible to the realtime
voice model: the kernel journaled the run and the floating pill turned green,
but no observer converted "done" into conversation input for the live provider
session, so the model could not mention the result or answer "what did it
find". The pull API built for exactly this (peekCompletedAgentDelta, with
per-surface seen-id + high-water checkpoints) had zero callers.

AgentCompletionVoiceDelivery wires that orphaned pipeline to the realtime hub:

- Watches AgentRuntimeStatusStore projections for transitions into a terminal
  state on background surfaces only (floating_bar/service/workstream), so
  ordinary chat/voice answers never trigger kernel delta reads.
- Reads the canonical completion delta from the kernel under the
  realtime_voice checkpoint namespace and injects the existing delta prompt
  (already fenced as untrusted, already says "do not read raw ids aloud") into
  the live provider session as silent context via the same buffered text-input
  path PTT uses. No synthetic response is requested, so the voice turn
  coordinator's output-lease authority and the one-start/one-terminal turn
  analytics contract are untouched; the model mentions the completion at the
  next natural turn boundary.
- Acknowledges the checkpoint only after the session confirms the send, so an
  undelivered completion is retried (next transition or next session connect)
  instead of lost; hubDidConnect drains completions that finished while no
  session was live.

Verification:
- xcrun swift build -c debug --package-path Desktop: clean
- New behavioral suite AgentCompletionVoiceDeliveryTests (9 tests) passes:
  delivery+ack ordering, checkpoint-unadvanced on failed inject, no-op without
  a live session, primary-surface transitions never trigger, repeated terminal
  projections fire once, cleared-and-recreated surface is a fresh transition,
  connect drains pending, in-flight coalescing, empty delta acks nothing.
- ./scripts/agent-logic-harness.sh: all four gates green (focused Swift
  lifecycle/state, agent runtime, pi-mono-extension, self-check).
- python3 scripts/check_desktop_test_quality.py: at/below baseline.
- Not exercised live: a real voice session observing a completion on a named
  bundle (requires provider audio session); noted in PR for follow-up.
…ompiles

AgentSyncRecoveryTests drives AgentSyncService's startForTesting /
syncOnceForTesting seam, which only exists under #if DEBUG
(AgentSyncService.swift:194). The debug test build compiles, but the
release-mode notification regression step (run-swift-ci.sh
--release-notification-regression) compiles the test bundle in release and
fails on every PR since the suite landed. Wrap the suite in #if DEBUG with the
established omi-release-compile marker, matching APIClientAuthRetryTests and
the other DEBUG-seam suites.

Verification: xcrun swift build --build-tests (debug) clean locally; full
suite + release regression step run on the qualification machine (evidence in
PR thread).
… compiles

Same class of break as the previous commit, surfaced once AgentSyncRecoveryTests
stopped masking it: VoiceTurnCoordinatorTests calls
DesktopDiagnosticsManager.resetForTests(), which only exists under #if DEBUG
(DesktopDiagnosticsManager.swift:866). Eight of the nine test files using that
seam were already #if DEBUG-gated; this one was the recent exception, breaking
the release-mode notification regression compile on every PR.

Verification:
- xcrun swift test -c release --filter UserNotificationCallbackBridgeTests/
  (the exact CI step) now compiles the full test bundle and passes (2 tests).
- Both gated suites still execute in debug: 37 tests, 0 failures
  (VoiceTurnCoordinatorTests 28, AgentSyncRecoveryTests 9).
Review by David (Git-on-my-level) caught a real exactly-once break:
AgentCompletionVoiceDelivery advanced the kernel checkpoint whenever
sendBackgroundAgentContext returned true, but that path delegated to
RealtimeHubSession.sendTextInput, which also returns true when it merely
BUFFERS the text (socket not open, or Gemini has no activity window). A
completion could be marked seen while only sitting in pendingTextInputs — and
stop()/close() clears that buffer, losing the completion permanently while the
checkpoint stayed advanced.

sendBackgroundAgentContext no longer buffers. It writes to the live provider
only when the session can accept it now (isOpen, and for Gemini an open
activity window) and returns true; otherwise it returns false without
buffering, so the caller leaves the checkpoint unadvanced and retries on the
next terminal transition or session connect. This restores the "checkpoint
advances only after confirmed delivery" invariant with nothing to lose on
close.

Bound (not a loss): a completion arriving while a live Gemini session has no
open activity window is deferred, not delivered, until the next window/turn or
reconnect. It is never acknowledged until actually sent.

Tests (RealtimeHubSessionInputLifecycleTests, DEBUG-seam suite): background
context before transport ready / open OpenAI / Gemini-without-window each assert
the return value and that nothing was buffered (new pendingTextInputCount field
on the DEBUG input-lifecycle snapshot).

Failure-Class: none
The Desktop Swift Test Suites job hung for 6h (killed at the CI cap) once the
release-compile gate was fixed and the full suite could finally run. Root
cause: the hubDidConnect hook calls AgentCompletionVoiceDelivery.shared
.voiceSessionDidConnect(), and the shared instance's default (production) seams
spawn a Task into DesktopCoordinatorService.shared -> AgentRuntimeProcess.shared,
which tries to launch and waitForInit the real Node agent runtime. RealtimeHub
unit tests drive hubDidConnect, so those tests reached into the runtime; with no
app bundle/dist present that launch could block indefinitely. The hang was
nondeterministic (the first CI run passed step 7, the rebased run hung; the
mini's different sandbox never hit it).

The service now stays fully inert — no status subscription, no delivery, no
runtime reach — until start() runs, which only OmiApp calls at launch. Tests
never start it, so voiceSessionDidConnect() is a guaranteed no-op there.

Test: voiceSessionDidConnect before start schedules no work and reaches no seam.

Failure-Class: none
@skanderkaroui
skanderkaroui force-pushed the claude/voice-completion-delivery branch from 45bdcc3 to d63fb3d Compare July 22, 2026 17:01

@Git-on-my-level Git-on-my-level left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the follow-up fixes — the original ack-before-delivery issue is much improved now that sendBackgroundAgentContext refuses instead of buffering, and the new unit coverage around closed sockets/Gemini idle windows is useful.

I still need to request changes on one remaining delivery hole: when a Gemini session is already connected but idle, sendBackgroundAgentContext correctly returns false because there is no open activity window, so AgentCompletionVoiceDelivery leaves the checkpoint unadvanced. But nothing schedules another delivery when the next activity window opens. The only retry triggers I see are another background-agent terminal transition and voiceSessionDidConnect(). beginInputTurn() opens the Gemini activity window and flushes ordinary buffered text inputs, but this background-completion path deliberately does not buffer and AgentCompletionVoiceDelivery is not notified there. So a completion that lands while Gemini is warm-idle can still fail to reach the very next natural voice turn — exactly the follow-up moment this feature is meant to support — until an unrelated terminal transition or reconnect happens.

Please add a retry trigger at the point the realtime session becomes able to accept background context again (for example when a Gemini activity window opens / input turn begins, while keeping the no-buffer/ack-after-send invariant), and cover that end-to-end in tests: completion delivery fails while Gemini is connected idle, the next user turn/window opens, delivery is retried, and the checkpoint advances only after the send succeeds.

Formal approval remains withheld because this is a nontrivial desktop voice-agent behavior change with outstanding live named-bundle verification/product sign-off.


by AI on behalf of David — if you need David’s attention urgently, please @Git-on-my-level and escalate with need human response.

…e checkpoint

A rebase reintroduced the ack-before-delivery bug: sendBackgroundAgentContext
delegated to the buffering sendTextInput, which returns true after *buffering*
when the session is warm-idle or the socket is closed. The delivery service acks
the exactly-once kernel checkpoint on that true, but stopOnQueue/abandonInputTurn
clear pendingTextInputs — so a completion could be acked and then dropped
(acked-but-lost).

Give background context its own non-buffering path: refuse (return false, leave
the checkpoint unadvanced) when the session can't accept context now, and when it
can, resume the continuation from the provider send's completion so true means a
*confirmed* send, not a fire-and-forget enqueue. Also emit hubDidOpenInputWindow
when a warm Gemini activity window opens, so a refused completion can be retried
(consumed in the next commit).

- canAcceptInjectedContext capability check; shared textInputWire builder
- tests: refuse-while-warm-idle -> false; confirmed-send failure -> false (DEBUG seam)
…reopens

sendBackgroundAgentContext now refuses (leaves the checkpoint unadvanced) while a
warm Gemini session is idle. The only delivery triggers were a terminal
transition and session connect, so a completion landing while connected-idle was
never retried when the next natural turn opened the activity window.

Route the new hubDidOpenInputWindow capability signal to the delivery service so
it retries then — the checkpoint still advances only after a confirmed send.

- RealtimeHubController forwards hubDidOpenInputWindow -> voiceSessionDidOpenInputWindow
- test: connected-idle -> window opens -> retried -> acked exactly once
Resolve conflicts from main's RealtimeHubSession type split (RealtimeHubSessionTypes.swift)
and the hubDidError -> RealtimeHubTransportFailure refactor:
- hubDidOpenInputWindow moves onto the relocated RealtimeHubSessionDelegate protocol
  (+ default no-op) in RealtimeHubSessionTypes.swift; the confirmed-send
  sendBackgroundAgentContext and beginInputTurn signal stay in RealtimeHubSession.swift.
- Line-count baseline: RealtimeHubSession.swift 1584 -> 1639 (main's Ticket 14 body plus
  the confirmed-send/hubDidOpenInputWindow additions).
…start

voiceSessionDidOpenInputWindow() reached scheduleDelivery() without the
hasStarted guard that voiceSessionDidConnect() has. beginInputTurn emits
hubDidOpenInputWindow, which RealtimeHub unit tests exercise, so before
start() the shared delivery singleton drove the live agent runtime mid-test
and the Desktop Swift Test Suites job hung (reintroducing the regression
9befcb8 fixed for voiceSessionDidConnect). Guard it the same way and add an
inert-before-start regression test.
Only the desktop-swift-floatingcontrolbar line-count baseline conflicted: main
dropped the now-1497-line RealtimeHubController.swift entry (below the 1500
threshold) and this branch raised RealtimeHubSession.swift to 1639 for the
confirmed-send/hubDidOpenInputWindow additions. Keep main's structure plus the
1639 raise.
…ency

RealtimeVoiceContextSingleFlightTests constructed a nonisolated Gate class
inside @mainactor methods and sent it into the nonisolated single-flight
closures, which fails the Swift 6 sending/data-race check on the current
toolchain (Desktop Swift Test Suites could not compile the test target).
Making Gate an actor makes it Sendable and serializes its state, also closing
the latent startCount/continuation race the class form had. Verified on macOS:
test target builds clean and the suite passes 3/3.

@Git-on-my-level Git-on-my-level left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the quick follow-up. I re-reviewed the current head (7348c68) against the two prior automation blockers:

  • The original ack-before-delivery issue now looks addressed: sendBackgroundAgentContext refuses instead of buffering when the provider cannot accept context, and acknowledgement stays after a successful provider send.
  • The Gemini warm-idle retry hole now looks addressed: beginInputTurn() opens the activity window and notifies AgentCompletionVoiceDelivery, with coverage for the failed-while-idle → retry-on-window-open → ack-after-send path.

I’m leaving this as a positive signal rather than formal approval because this is still a nontrivial desktop voice behavior change, the current Desktop Swift CI is not green (Desktop Swift Test Suites was cancelled and the aggregate build/test gate is failing), and the PR body still calls out that live named-bundle/provider verification has not been exercised. Please keep needs-maintainer-review/needs-tests until CI and the live voice-session validation are clean.


by AI on behalf of David — if you need David’s attention urgently, please @Git-on-my-level and escalate with need human response.

@Git-on-my-level
Git-on-my-level dismissed stale reviews from themself July 24, 2026 07:29

Current head 7348c68 addresses this automation review blocker; replacing with the latest review status.

Take main's RealtimeVoiceContextSingleFlightTests rewrite (event-driven Gate
that stores the continuation before bumping startCount) — it fixes the
startCount/continuation race whose missed wakeup hung the suite forever and
timed out the 1h Desktop Swift Test Suites job; it supersedes this branch's
actor-based fix for the same file. Baselines: OmiApp 1596->1599 (start() wiring
on top of main's onboarding-journal extraction), RealtimeHubSession 1584->1639.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-maintainer-review Needs a human maintainer to sign off before merge needs-tests PR introduces logic that should be covered by tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants