Skip to content

feat(voice): realtime ElevenLabs voice agent alongside classic path - #5489

Merged
M3gA-Mind merged 21 commits into
tinyhumansai:mainfrom
YellowSnnowmann:feat/5399-voice-elevenlabs-realtime
Aug 13, 2026
Merged

feat(voice): realtime ElevenLabs voice agent alongside classic path#5489
M3gA-Mind merged 21 commits into
tinyhumansai:mainfrom
YellowSnnowmann:feat/5399-voice-elevenlabs-realtime

Conversation

@YellowSnnowmann

@YellowSnnowmann YellowSnnowmann commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Add the ElevenLabs Voice Agents realtime path as an always-on "Start Voice Chat" control on the Human tab, driven by the local orchestrator over the voice:harness relay.
  • Pin a fast, non-thinking voice model per turn so the first spoken token lands inside the provider's response-time ceiling; stream reply tokens live.
  • Ack-and-defer slow tool turns (email/calendar): speak a short acknowledgement, finish in the background, deliver the answer to chat, and read it back aloud.
  • Scope each voice turn with the same approval-chat context + thread as web chat, so composio_connect reaches its already-connected path instead of surfacing a false "reconnect your Gmail" auth error.
  • Keep triggered memory recall on voice so spoken answers use the user's remembered context.
  • Make the realtime control always visible on the Human tab and remove the now-unused voice-mode settings toggle.

Problem

The classic voice chat has known fragility (TTS 400s, 3-4s text→speech delay, tab lock during TTS). The realtime ElevenLabs path replaces that with a low-latency, server-driven voice session. A voice-only regression also surfaced: email summaries failed with a fabricated "your Gmail connection is throwing an auth error, reconnect it" because the voice turn ran without the chat approval context that composio_connect requires — while the identical request worked in tap-and-speak.

Solution

The desktop core relays each ElevenLabs turn over voice:harness and runs the local orchestrator, streaming spoken tokens back. Latency is handled by a fast per-turn model plus an audible keepalive filler, with an ~8s ack-defer that closes the spoken turn and finishes long work in the background (delivered to chat and read back). Each voice turn is now scoped with APPROVAL_CHAT_CONTEXT + a thread id (mirroring web chat) on the same proactive:voice surface used for deferred delivery — this fixes the Gmail confabulation and enables self-heal / delegation. Memory recall stays on for voice; its latency is covered by the keepalive and background-defer.

Intentional deviation from #5399: the issue asks to replace the old path and remove it behind a rollback flag. For staging we keep the old path unchanged and ship the new path as a separate button, so both can be compared side by side. Removing the old path is deferred to a follow-up.

Submission Checklist

  • Tests added or updated — unit tests for the speak-back loop guard (should_arm_speak_back) covering the re-arm-suppression edge case; the pure prompt/history/delta helpers remain unit-tested. The async relay/orchestration is verified by a live end-to-end run.
  • Diff coverage ≥ 80% — not met on the changed lines: the async voice-harness orchestration (tokio timing + global event bus) is not unit-testable in isolation and is covered by live e2e instead. Flagged for reviewer guidance.
  • Coverage matrix updated — N/A: no feature-matrix rows change (adds a parallel voice path behind an existing surface).
  • All affected feature IDs listed in ## RelatedN/A: no matrix IDs affected.
  • No new external network dependencies introduced — mock policy unaffected.
  • Manual smoke checklist updated — N/A: staging-only, classic path untouched; a follow-up will update it on old-path removal.
  • Linked issue closed via Closes #NNNN/A: this PR intentionally does not close Replace current voice chat with ElevenLabs Voice Agents #5399 (old-path removal deferred).

Impact

  • Desktop only. The realtime voice path is additive; the classic path is byte-for-byte unchanged.
  • Latency: first spoken token ~1s (fast model + keepalive) vs the classic path's 3-4s.
  • No migration; removing the settings toggle does not alter any persisted voiceMode value.
  • Requires production configuration (not code): the ElevenLabs agent Custom-LLM URL must target the production backend, and the inference config must resolve the voice model in prod.

Related

Reviewer notes

  • The pre-push hook was bypassed (--no-verify) because it fails on cargo clippy for app/src-tauri (the desktop shell), which this PR does not modify; those lints are unrelated to the voice changeset.
  • cargo test (full lib suite) does not compile from this base due to a pre-existing dead import in src/openhuman/memory/guard/provider_tests.rs (the in-flight core::event_bustinybus migration), unrelated to this PR.
  • Prettier was not runnable locally (missing @trivago/prettier-plugin-sort-imports); the TS changes are import-sort-preserving and pass pnpm typecheck.

AI Authored PR Metadata (required for Codex/Linear PRs)

N/A — human-authored.

Linear Issue

  • Key: N/A
  • URL: N/A

Commit & Branch

  • Branch: feat/5399-voice-elevenlabs-realtime
  • Base: main

Summary by CodeRabbit

  • New Features

    • Realtime voice controls are now available directly on the Human tab.
    • Voice interactions provide faster spoken acknowledgments and streamed progress updates.
    • Completed responses can be read aloud during active voice sessions.
    • Longer-running voice tasks continue in the background while final results appear in chat.
    • Voice commands provide immediate spoken guidance when using tools.
  • Bug Fixes

    • Reduced duplicate voice updates and improved handling of interrupted or inactive sessions.
    • Improved continuity between voice and chat conversations.
    • Improved reliability for extended voice tasks and read-back responses.

Add the ElevenLabs Voice Agents realtime path as an always-on
"Start Voice Chat" control on the Human tab, driven by the local
orchestrator over the voice:harness relay. The classic push-to-talk
path is kept unchanged so both run side by side in staging.

- Pin a fast, non-thinking voice model per turn (set_model_name) so the
  first spoken token lands inside the provider's response-time ceiling.
- Ack-and-defer slow turns (email/calendar): speak a short ack, finish
  in the background, deliver the answer to chat and read it back aloud.
- Scope each voice turn with the same approval-chat context + thread as
  chat, so composio_connect reaches its already-connected path instead
  of surfacing a false "reconnect your Gmail" auth error.
- Keep triggered memory recall on voice so spoken answers use the user's
  remembered context; the added latency is covered by the relay's
  audible keepalive and the background-defer.
- Stream reply tokens live; guard speak-back from re-arming on a
  read-back turn (should_arm_speak_back, unit-tested).
- Make the realtime control always visible on the Human tab and remove
  the now-unused voice-mode settings toggle.

Addresses tinyhumansai#5399. Old-path removal (AC7) is intentionally deferred: old
and new run side by side in staging for comparison.
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Realtime voice controls now run from the Human tab. Active voice sessions support socket-based read-back. Rust voice turns use background execution, fast model selection, streaming progress, acknowledgment deadlines, scoped chat context, deferred delivery, and read-back safeguards. Memory-diff exports are feature-gated.

Changes

Realtime voice flow

Layer / File(s) Summary
Client voice controls and read-back
app/src/components/settings/panels/VoicePanel.tsx, app/src/features/human/HumanPage.tsx, app/src/features/human/voice/useRealtimeVoiceSession.ts
Realtime voice controls were removed from VoicePanel and enabled unconditionally on HumanPage. Active sessions now handle voice_speak events and send non-empty responses to ElevenLabs for read-back.
Voice-turn configuration and execution context
src/openhuman/agent/harness/session/runtime.rs, src/openhuman/agent/harness/session/turn/core.rs, src/openhuman/voice/realtime_harness.rs
Voice turns now define dedicated timing, model, progress, directive, approval, and thread settings. Agent supports per-turn model overrides.
Background completion and deferred delivery
src/openhuman/voice/realtime_harness.rs
Voice execution uses bounded background processing, streaming filters, duplicate suppression, acknowledgment deadlines, deferred chat delivery, and guarded voice_speak publishing. Tests cover progress filtering and read-back behavior.

Memory-diff and test maintenance

Layer / File(s) Summary
Feature-gated memory-diff exports
src/openhuman/memory/diff/mod.rs, src/openhuman/memory/diff/stub.rs
MemoryDiffTool is exported only when memory-git is enabled. The disabled stub imports diff types from tinycortex.
Bitcoin test formatting
src/openhuman/web3/wallet/chains/btc.rs
The Bitcoin address validation test was reformatted without behavior changes.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant HumanPage
  participant useRealtimeVoiceSession
  participant realtime_harness
  participant socketService

  HumanPage->>useRealtimeVoiceSession: enable realtime voice
  realtime_harness->>socketService: publish voice_speak response
  socketService-->>useRealtimeVoiceSession: deliver voice_speak payload
  useRealtimeVoiceSession->>useRealtimeVoiceSession: send read-back text to active voice session
  realtime_harness->>socketService: deliver deferred result to proactive:voice chat
Loading

Possibly related PRs

Suggested labels: feature, rust-core, agent

Poem

I twitch my nose; the voice runs bright,
Fast turns speak, then chat holds tight.
Socket read-back hops along,
Deferred results join the song.
No echoed words return tonight.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding a realtime ElevenLabs voice agent alongside the existing classic voice path.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@YellowSnnowmann
YellowSnnowmann marked this pull request as ready for review August 10, 2026 17:11
@YellowSnnowmann
YellowSnnowmann requested a review from a team August 10, 2026 17:11
@coderabbitai coderabbitai Bot added agent Built-in agents, prompts, orchestration, and agent runtime in src/openhuman/agent/. feature Net-new user-facing capability or product behavior. rust-core Core Rust runtime in src/: CLI, core_server, shared infrastructure. labels Aug 10, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
app/src/features/human/HumanPage.tsx (1)

36-37: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the constant gate and update the stale overlay comment.

realtimeEnabled is now a hard-coded true, so the guard at Line 105 can never be false. Drop the variable and render RealtimeVoiceControls directly. The comment above Line 105 still states the overlay is "shown only when the flag + realtime mode are on", which no longer matches the code.

♻️ Proposed cleanup
-  // Realtime voice controls are always shown — no settings/flag gate.
-  const realtimeEnabled = true;

Then simplify the render block (Lines 102-109):

      {/* Realtime voice-chat controls (`#5399`) — always shown; the classic
          push-to-talk path below is untouched. */}
      <div className="absolute bottom-8 left-0 right-[436px] z-10 flex justify-center">
        <RealtimeVoiceControls />
      </div>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/features/human/HumanPage.tsx` around lines 36 - 37, Remove the
hard-coded realtimeEnabled constant and its conditional guard, then render
RealtimeVoiceControls directly in the overlay block. Update the stale overlay
comment to state that realtime voice-chat controls are always shown, while
leaving the classic push-to-talk path unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/openhuman/voice/realtime_harness.rs`:
- Around line 160-167: Update the VOICE_DIRECTIVE tool/delegation preface so the
chat promise is conditional on the action taking a while, rather than guaranteed
for every tool turn. Preserve the requirement to announce the action in one
short spoken sentence before proceeding.
- Around line 247-259: Update the deferred-result handling around
deliver_voice_result_to_chat so read-back turns identified by the
VOICE_READBACK_PREFIX skip chat delivery entirely, while genuine deferred turns
retain existing chat and speak-back behavior. Ensure the same guard applies in
the related delivery path near should_arm_speak_back, and add a test asserting
deferred read-back turns perform no chat delivery.
- Around line 255-257: The deferred-turn Err arm should publish a user-facing
failure message instead of only logging the error. Add a
deliver_voice_failure_to_chat helper near deliver_voice_result_to_chat that
emits a proactive_message with the fixed failure text, VOICE_CHAT_CLIENT_ID,
VOICE_CHAT_THREAD_ID, and success=false, then call it from the Err(err) branch
while retaining the warning log.

---

Nitpick comments:
In `@app/src/features/human/HumanPage.tsx`:
- Around line 36-37: Remove the hard-coded realtimeEnabled constant and its
conditional guard, then render RealtimeVoiceControls directly in the overlay
block. Update the stale overlay comment to state that realtime voice-chat
controls are always shown, while leaving the classic push-to-talk path
unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f0ef4c67-782b-4d8c-8b2d-6cf52a34f846

📥 Commits

Reviewing files that changed from the base of the PR and between c82715b and 2748458.

📒 Files selected for processing (6)
  • app/src/components/settings/panels/VoicePanel.tsx
  • app/src/features/human/HumanPage.tsx
  • app/src/features/human/voice/useRealtimeVoiceSession.ts
  • src/openhuman/agent/harness/session/runtime.rs
  • src/openhuman/agent/harness/session/turn/core.rs
  • src/openhuman/voice/realtime_harness.rs

Comment thread src/openhuman/voice/realtime_harness.rs Outdated
Comment thread src/openhuman/voice/realtime_harness.rs
Comment thread src/openhuman/voice/realtime_harness.rs
- Make the VOICE_DIRECTIVE chat promise conditional ("if it takes a
  while, I'll follow up in your chat"), so a fast in-window turn no
  longer promises a chat entry that never appears.
- Skip chat delivery entirely for a deferred read-back turn (not just the
  spoken copy), so the echoed answer is never re-posted to proactive:voice
  and a read-back turn can't duplicate the chat message.
- Post a failure notice to the voice chat thread when a deferred turn
  errors after the spoken turn already closed, so the promised chat
  message always appears instead of only a warn log.
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/openhuman/voice/realtime_harness.rs (1)

305-312: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a diagnostic log to the aborted-sender arm.

This arm runs when the background task drops the sender without a value, which means the task panicked or was aborted. The handler emits voice:harness:done and returns with no log line. A silent turn with no spoken text and no chat delivery is then hard to diagnose from logs. Add a warn! with the correlation id.

♻️ Proposed diagnostic
         Ok(Err(_recv)) => {
             // Sender dropped without a value (task aborted). End cleanly.
+            warn!("[voice-harness] turn task ended without a result (panicked or aborted) correlation={correlation_id}");
             emit_event(
                 "voice:harness:done",
                 json!({ "correlationId": correlation_id }),
             )
             .await;
         }

As per coding guidelines: "Add verbose, grep-friendly Rust diagnostics using log or tracing at debug/trace, including correlation fields".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/voice/realtime_harness.rs` around lines 305 - 312, Add a
warning log in the Ok(Err(_recv)) aborted-sender arm before emitting
voice:harness:done, including correlation_id as a structured field and
indicating that the background task ended without producing a value; preserve
the existing event emission and clean return behavior.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/openhuman/voice/realtime_harness.rs`:
- Around line 494-498: Update the empty `spoken` branch in the deferred-turn
handler to call `deliver_voice_failure_to_chat` with the relevant correlation
context before returning, instead of only logging and exiting. Preserve the
warning and ensure the failure message is routed to the `proactive:voice` thread
just as voice delivery failures are handled elsewhere.
- Around line 544-548: Update the failure response constructed by
deliver_voice_failure_to_chat so its success field is false rather than true,
while preserving the existing failure message and default fields.

---

Nitpick comments:
In `@src/openhuman/voice/realtime_harness.rs`:
- Around line 305-312: Add a warning log in the Ok(Err(_recv)) aborted-sender
arm before emitting voice:harness:done, including correlation_id as a structured
field and indicating that the background task ended without producing a value;
preserve the existing event emission and clean return behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 95f780ee-e161-4b6f-9512-2b52b5c2baf5

📥 Commits

Reviewing files that changed from the base of the PR and between c82715b and 4c98670.

📒 Files selected for processing (6)
  • app/src/components/settings/panels/VoicePanel.tsx
  • app/src/features/human/HumanPage.tsx
  • app/src/features/human/voice/useRealtimeVoiceSession.ts
  • src/openhuman/agent/harness/session/runtime.rs
  • src/openhuman/agent/harness/session/turn/core.rs
  • src/openhuman/voice/realtime_harness.rs
🚧 Files skipped from review as they are similar to previous changes (4)
  • app/src/components/settings/panels/VoicePanel.tsx
  • app/src/features/human/voice/useRealtimeVoiceSession.ts
  • app/src/features/human/HumanPage.tsx
  • src/openhuman/agent/harness/session/turn/core.rs

Comment thread src/openhuman/voice/realtime_harness.rs
Comment thread src/openhuman/voice/realtime_harness.rs
This PR's rust changes make CI run the whole-repo fmt check and the
gates-off smoke build, which surface two latent issues on the base that
are unrelated to the voice change itself:

- web3/wallet/chains/btc.rs: apply rustfmt (toolchain 1.96.1) to a stray
  unformatted line.
- memory/diff/mod.rs: gate `pub use tools::MemoryDiffTool` behind
  `memory-git` (its `tools` module is already `#[cfg(feature = "memory-git")]`),
  so the slim build no longer references a compiled-out module.
- memory/diff/stub.rs: import Checkpoint/CrossSourceDiff/Snapshot from
  `tinycortex::memory::diff::types` (where mod.rs re-exports them) instead
  of a non-existent `super::types`, so the gates-off build resolves them.
…failure flag

- Route an empty deferred reply through deliver_voice_failure_to_chat so
  the chat follow-up the spoken preface promised always appears, instead
  of a silent early return.
- Set success=false on the deferred-failure notice so a client that
  branches on the flag reads it as a failed turn rather than a success.
coderabbitai[bot]
coderabbitai Bot previously approved these changes Aug 10, 2026
Add tests for the voice_speak subscription: reads a deferred result aloud
while a call is live, ignores it when no call is live or the payload is
empty, and unsubscribes on unmount. Covers the changed lines so the diff
meets the coverage gate.
Under gates-off the memory diff controllers compile out (the stub returns
none), but core/all.rs registered the memory_diff capability
unconditionally -- leaving a stale namespace with no controllers behind
it, which the gates-off capability-map guard tests reject. Gate the
push_cap on memory-git so the namespace is absent when the feature is,
matching the already-passing memory_diff_controllers_absent test.
- HumanPage.realtimeMode.test.tsx: the realtime controls are now shown
  unconditionally (the former build-flag + voice-mode gate was removed),
  so assert they render regardless of the persisted mode instead of
  hiding on the classic default.
- Remove VoicePanel.realtimeMode.test.tsx: it exercised the voice-mode
  toggle this PR removed from the panel.
- Revert src/core/all.rs and src/openhuman/memory/diff/{mod,stub}.rs to
  upstream. The slim (--no-default-features) capability surface needs a
  coordinated fix across core/all.rs, tools/ops.rs and the memory-diff
  stub; that belongs in a dedicated cleanup, not this voice change.
… build

With `memory-git` off the diff tool + controllers compile out, but several
sites still advertised the `diff` capability, so the gates-off and rss-bench
builds did not even compile / were inconsistent (an earlier one-sided gate
made it worse: the table claimed diff gated something the registry did not).
Gate all four sites in lockstep, all on `memory-git` (default ON, so the
shipped build is byte-identical):

- memory/diff/mod.rs: gate the `MemoryDiffTool` re-export (its `tools` module
  is already `#[cfg(feature = "memory-git")]`) and fix the stub's type import
  to `tinycortex::memory::diff::types` so the slim build compiles.
- core/all.rs: gate the `memory_diff` capability + controller registration.
- tools/ops.rs: gate the `"memory_diff" => Capability::Diff` table arm so the
  capability table and the live registry agree (arm falls through to the
  existing `_ => None`).
…y gating

Retain the minimal fix that lets the slim / rss-bench build compile
(gate the MemoryDiffTool re-export whose module is already feature-gated,
and point the stub's types at tinycortex::memory::diff::types). Revert the
core/all.rs + tools/ops.rs capability gating: making the gates-off
capability surface consistent needs a coordinated change across the
registry, the capability_for table, and the representative-tool table
(the guard tests are interlocked), which belongs in a dedicated cleanup
rather than this voice PR.
Address the two CodeRabbit nitpicks on the realtime voice path:
- HumanPage: remove the hard-coded realtimeEnabled=true constant and its
  dead conditional, render RealtimeVoiceControls directly, and fix the stale
  overlay comment that still referenced a flag/mode gate.
- realtime_harness: log a warn! with the correlation id in the aborted-sender
  arm (task panicked or aborted) so a silent voice turn is traceable.
Resolve memory/diff conflicts by taking main's version: main extracted the
diff domain into the tinymemory_core crate and already gates MemoryDiffTool
behind memory-git with a registration-only stub, which supersedes this
branch's transitional slim-build gating (both prior fixes are subsumed).
@YellowSnnowmann

Copy link
Copy Markdown
Collaborator Author

Pushed ea6c3d71 — merged latest main and cleared the two outstanding nitpicks.

Merge / conflict resolution

  • Merged upstream/main. The only conflicts were in memory/diff/{mod,stub}.rs. Resolved by taking main's version: main extracted the diff domain into the tinymemory_core crate and already gates MemoryDiffTool behind memory-git with a registration-only stub, which supersedes this branch's earlier transitional slim-build gating (both of that branch's fixes are subsumed). None of the voice files conflicted.
  • This also clears the earlier red Rust checks (Feature-Gate Smoke (gates off), Rust Core Coverage): those were the memory capability map/table tests panicking because the branch's stale memory_diff gating was inconsistent with main's capability system — all in files this PR never touches, so merging main is the fix.

Nitpicks

  • HumanPage.tsx: removed the hard-coded realtimeEnabled = true constant and its dead conditional, render RealtimeVoiceControls directly, and fixed the stale overlay comment that still referenced a flag/mode gate.
  • realtime_harness.rs: added a warn! with the correlation id to the aborted-sender arm (Ok(Err(_recv))) so a turn whose task panicked or was aborted is traceable instead of silent.

Verified locally: full product-feature cargo check is clean and pnpm typecheck passes.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Current commit and merge base =="
git rev-parse --short HEAD
BASE="$(git merge-base HEAD upstream/main 2>/dev/null || git merge-base HEAD origin/main)"
echo "$BASE"

echo "== Changed files since merge base =="
git diff --name-status "$BASE"...HEAD

echo "== Outlines for primary changed files =="
for file in \
  app/src/features/human/HumanPage.tsx \
  app/src/features/human/voice/useRealtimeVoiceSession.ts \
  src/openhuman/voice/realtime_harness.rs \
  src/openhuman/agent/harness/session/runtime.rs
do
  if [ -f "$file" ]; then
    echo "--- $file ---"
    ast-grep outline "$file" --items all || true
  fi
done

echo "== HumanPage current content =="
if [ -f app/src/features/human/HumanPage.tsx ]; then
  cat -n app/src/features/human/HumanPage.tsx
fi

echo "== Realtime harness definitions, tests, and relevant event paths =="
if [ -f src/openhuman/voice/realtime_harness.rs ]; then
  rg -n -C 8 \
    'voice_speak|full_response|deferred|spawn|JoinHandle|oneshot|warn!|correlation|READBACK|#[[:space:]]*test|tokio::test' \
    src/openhuman/voice/realtime_harness.rs
fi

echo "== Hook event subscription and cleanup =="
if [ -f app/src/features/human/voice/useRealtimeVoiceSession.ts ]; then
  rg -n -C 8 \
    'voice_speak|full_response|socketService|useEffect|cleanup|return \(\)' \
    app/src/features/human/voice/useRealtimeVoiceSession.ts
fi

echo "== Current PR diff for primary files =="
git diff --unified=60 "$BASE"...HEAD -- \
  app/src/features/human/HumanPage.tsx \
  app/src/features/human/voice/useRealtimeVoiceSession.ts \
  src/openhuman/voice/realtime_harness.rs \
  src/openhuman/agent/harness/session/runtime.rs

Length of output: 97768


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Locate web-channel event routing and frontend consumers =="
rg -n -C 10 \
  'publish_web_channel_event\s*\(|struct WebChannelEvent|enum WebChannelEvent|voice_speak|proactive_message' \
  src app --glob '*.rs' --glob '*.ts' --glob '*.tsx'

echo "== Socket service API and event-subscription conventions =="
socket_file="$(fd -a '^socketService\.(ts|tsx)$' app | head -n 1 || true)"
if [ -n "$socket_file" ]; then
  echo "--- $socket_file ---"
  ast-grep outline "$socket_file" --items all || true
  cat -n "$socket_file"
fi

echo "== Realtime hook tests =="
test_file="$(fd -a '^useRealtimeVoiceSession\.test\.ts$' app | head -n 1 || true)"
if [ -n "$test_file" ]; then
  echo "--- $test_file ---"
  cat -n "$test_file"
fi

echo "== Agent history seeding implementation and tests =="
runtime_file="src/openhuman/agent/harness/session/runtime.rs"
rg -n -C 16 \
  'fn seed_resume_from_messages|seed_resume_from_messages\s*\(|fn seed_resume_from_thread_transcript' \
  "$runtime_file"

echo "== Voice relay and handling registration =="
rg -n -C 10 \
  'voice:harness|handle_voice_harness_turn|harness:delta|harness:done|harness:error' \
  src app --glob '*.rs' --glob '*.ts' --glob '*.tsx'

echo "== Current diff for tests and all deleted/modified voice-mode files =="
BASE="$(git merge-base HEAD upstream/main 2>/dev/null || git merge-base HEAD origin/main)"
git diff --unified=40 "$BASE"...HEAD -- \
  app/src/components/settings/panels/VoicePanel.tsx \
  app/src/components/settings/panels/__tests__/VoicePanel.realtimeMode.test.tsx \
  app/src/features/human/HumanPage.realtimeMode.test.tsx \
  app/src/features/human/voice/useRealtimeVoiceSession.test.ts \
  src/openhuman/agent/harness/session/turn/core.rs

Length of output: 184121


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Socket.IO WebChannelEvent emitter and room routing =="
rg -n -C 20 \
  'fn emit_web_channel_event|emit_web_channel_event\s*\(|client_id.*room|thread_id.*room|join.*room|room.*client_id|room.*thread_id' \
  src/core/socketio.rs src --glob '*.rs'

echo "== WebChannelEvent defaults and serialization =="
sed -n '132,235p' src/core/socketio.rs

echo "== Voice signed-URL API endpoint and token/session response shape =="
rg -n -C 12 \
  'fetchVoiceAgentSignedUrl|voice-agent|signedUrl|signed_url|userToken|user_token' \
  app/src src --glob '*.ts' --glob '*.tsx' --glob '*.rs'

echo "== Web-channel consumers and event filtering by client or thread =="
rg -n -C 12 \
  'client_id|thread_id|proactive_message|socketService\.on' \
  app/src/providers/ChatRuntimeProvider.tsx app/src/services/chatService.ts \
  --glob '*.ts' --glob '*.tsx'

Length of output: 200084


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Web-channel event emitter definitions =="
rg -n 'emit_web_channel_event|subscribe_web_channel_events' src/core/socketio.rs src --glob '*.rs' | head -n 80

echo "== Socket.IO emitter implementation =="
line="$(rg -n 'fn emit_web_channel_event|async fn emit_web_channel_event' src/core/socketio.rs | head -n 1 | cut -d: -f1)"
if [ -n "$line" ]; then
  start=$((line - 35))
  end=$((line + 135))
  sed -n "${start},${end}p" src/core/socketio.rs
fi

echo "== Event-bus forwarding call site =="
rg -n -C 25 'subscribe_web_channel_events\s*\(' src/core/socketio.rs src --glob '*.rs' | head -n 220

echo "== Voice signed-URL frontend API =="
voice_api="$(fd -a '^voiceAgentApi\.ts$' app | head -n 1 || true)"
if [ -n "$voice_api" ]; then
  cat -n "$voice_api"
fi

Length of output: 27536


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Socket.IO connection handler and system-room membership =="
sed -n '430,530p' src/core/socketio.rs

echo "== All system-room joins and system-event routing references =="
rg -n -C 6 \
  'join_room_logged\(.*system|join\("system"|room.*system|client_id = "system"|client_id: "system"' \
  src/core/socketio.rs src --glob '*.rs' | head -n 260

Length of output: 20878


@YellowSnnowmann I found one blocking issue.

voice_speak broadcasts deferred answers to all active voice sessions.

deliver_voice_result_to_chat publishes voice_speak with client_id: "system" in src/openhuman/voice/realtime_harness.rs at Line 528. Every authenticated socket joins the "system" room in src/core/socketio.rs at Line 497. The handler in app/src/features/human/voice/useRealtimeVoiceSession.ts at Line 147 reads every received non-empty event into its live ElevenLabs session.

As a result, if two Human tabs or clients have active voice calls against the same core, a deferred email or calendar response from one call is spoken in both calls. This can expose the deferred response to the wrong user session.

Route voice_speak to the originating voice session. Include a session-specific or client-specific target in the event payload. Validate that target in useRealtimeVoiceSession before calling sendUserMessage.

You are interacting with an AI system.

The Linux TLS dependency policy check runs `cargo tree --locked` over the
Tauri Cargo world and was failing with "cannot update the lock file ...
because --locked was passed": the shell lock still listed 81 crates the
graph no longer pulls (bitcoin/ethers/docx-rs and friends), left stale when
the tinywallet and tinydocs extractions dropped those deps from the core but
only regenerated the root lock. Reconcile the shell lock (removes the unused
crates, pulls in the module-host deps like ureq); no source changes. Verified
`cargo tree --locked` now passes for both Cargo worlds and the TLS policy
finds no forbidden deps (no openssl in core, no aws-lc anywhere).

@tinysweeper tinysweeper Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

tinysweeper found nothing blocking. Approving.

             $0.0881 · 93,833 in / 26,474 out · 73,745 cached (79%) · z-ai/glm-5.2
critique:    $0.0544 · 36,231 in / 18,875 out · 28,668 cached (79%) · z-ai/glm-5.2
security:    $0.0168 · 25,882 in / 4,219 out  · 20,618 cached (80%) · z-ai/glm-5.2
tests:       $0.0101 · 15,208 in / 2,358 out  · 11,285 cached (74%) · z-ai/glm-5.2
description: $0.0068 · 16,512 in / 1,022 out  · 13,174 cached (80%) · z-ai/glm-5.2

Comment thread src/openhuman/voice/realtime_harness.rs
@tinysweeper

tinysweeper Bot commented Aug 12, 2026

Copy link
Copy Markdown

What this change touches

9 files, +563 -157 across 7 components. The code graph knows nothing about these files yet — normal for newly added files, and a cold index otherwise.

flowchart LR
  n0["src/openhuman/voice<br/>1 file +433 -24<br/>2 findings"]:::flagged
  n1["app/src/features/human/voice<br/>2 files +91 -1"]:::changed
  n2["app/src/components/settings/panels/__tests__<br/>1 file +0 -79"]:::changed
  n3["app/src/features/human<br/>2 files +17 -29"]:::changed
  n4["app/src/components/settings/panels<br/>1 file +2 -24"]:::changed
  n5["src/openhuman/agent/harness/session<br/>1 file +10 -0"]:::changed
  n6["src/openhuman/agent/harness/session/turn<br/>1 file +10 -0"]:::changed
  classDef changed fill:#0d4429,stroke:#238636,color:#e6edf3
  classDef impacted fill:#161b22,stroke:#6e7681,color:#c9d1d9
  classDef flagged fill:#5a1e02,stroke:#d93f0b,color:#ffffff
  classDef blocking fill:#67060c,stroke:#f85149,color:#ffffff
Loading

Green: changed. Grey: untouched, reached through an import or a call. Orange: has findings. Red: has a finding that blocks the merge.

Component Files Lines Findings
src/openhuman/voice changed 1 +433 -24 2 (medium)
app/src/features/human/voice changed 2 +91 -1
app/src/components/settings/panels/__tests__ changed 1 +0 -79
app/src/features/human changed 2 +17 -29
app/src/components/settings/panels changed 1 +2 -24
src/openhuman/agent/harness/session changed 1 +10 -0
src/openhuman/agent/harness/session/turn changed 1 +10 -0
Changed files

src/openhuman/voice

  • src/openhuman/voice/realtime_harness.rs

app/src/features/human/voice

  • app/src/features/human/voice/useRealtimeVoiceSession.test.ts
  • app/src/features/human/voice/useRealtimeVoiceSession.ts

app/src/components/settings/panels/__tests__

  • app/src/components/settings/panels/__tests__/VoicePanel.realtimeMode.test.tsx

app/src/features/human

  • app/src/features/human/HumanPage.realtimeMode.test.tsx
  • app/src/features/human/HumanPage.tsx

app/src/components/settings/panels

  • app/src/components/settings/panels/VoicePanel.tsx

src/openhuman/agent/harness/session

  • src/openhuman/agent/harness/session/runtime.rs

src/openhuman/agent/harness/session/turn

  • src/openhuman/agent/harness/session/turn/core.rs

tinysweeper 0.1.0

@tinysweeper tinysweeper Bot added the priority: p2 Soon. Real but survivable — a rough edge, a gap, a thing that will bite later. label Aug 12, 2026
…reply caller

tinysweeper flagged that the helper's doc said it fires only when a deferred
turn 'errors', but it is also called from the Ok(reply) success path when the
reply trims to empty. Document both callers and note that on the voice path
run_single's returned text is the sole answer channel (the orchestrator folds
tool/subagent output into its final reply), so an empty reply means nothing was
produced for the user — which is why surfacing the promised chat notice rather
than staying silent is correct. No behaviour change.

@tinysweeper tinysweeper Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

tinysweeper found nothing blocking. Approving.

             $0.1042 · 52,957 in / 37,286 out · 39,042 cached (74%) · z-ai/glm-5.2
critique:    $0.0369 · 10,292 in / 14,370 out · 7,846 cached (76%)  · z-ai/glm-5.2
security:    $0.0178 · 10,271 in / 6,191 out  · 7,407 cached (72%)  · z-ai/glm-5.2
tests:       $0.0361 · 15,545 in / 13,163 out · 11,102 cached (71%) · z-ai/glm-5.2
description: $0.0133 · 16,849 in / 3,562 out  · 12,687 cached (75%) · z-ai/glm-5.2

Comment thread src/openhuman/voice/realtime_harness.rs
Comment thread src/openhuman/voice/realtime_harness.rs
…ack loop

A voice turn that returns no spoken content ends the whole call, not just the
turn - the provider reports "LLM Cascade Error: Brain returned no response".
Three paths could do that, and each now speaks a short pause instead: a
recognition artefact ("..." from a pause, which the provider relays as a real
turn), a read-back echo, and a turn whose task ended without a result.

Read-back turns are answered from the prompt rather than by rebuilding an
orchestrator to echo text we were handed - that round-trip costs a tool
registry, memory recall and a model round-trip, far past the turn budget, so
the caller heard filler where their answer should have been.

The concurrency permit is now taken inside the detached task. Acquiring it in
the foreground meant a turn queued behind slower ones started no ack clock and
emitted no done at all, and the provider ended the session over it.

The deferred-failure notice is limited to turns whose answer the user is
actually waiting on. A read-back or a recognition artefact has nothing to
deliver, and posting "I couldn't finish that request" for one reads as the
assistant failing a request nobody made.

Renderer: an answer is read aloud once per call, so a redelivery cannot queue
a second spoken turn behind the first.
The realtime "Start voice chat" control now sits in the chat card, in the slot
the push-to-talk mic occupied, and the floating copy over the mascot stage is
gone - the tab offered two competing voice affordances at once.

Which one renders is a build decision:
- VITE_HUMAN_VOICE_REALTIME (default true) - realtime control.
- VITE_HUMAN_VOICE_REALTIME=false - the classic tap-and-speak composer.
- VITE_HUMAN_VOICE_SHOW_BOTH (default false) - both, stacked, for comparing
  the two paths. Takes precedence over the first flag, so a build that asks to
  see both is not left with one hidden by the other's rollback state.

Kept separate from VOICE_MODE_FLAG_ENABLED, which gates the chat tab's mascot
stage: one surface's rollback must not silently change the other's.

Conversations takes the control as a node rather than importing it, so no
consumer picks up the ElevenLabs SDK in its module graph just by rendering a
conversation.
VITE_HUMAN_VOICE_SHOW_BOTH now restores the realtime button to its own place
over the mascot stage rather than stacking it above tap-and-speak in the card.
Comparing two voice paths is easier when they do not sit on top of each other,
and the card keeps exactly the composer it has when realtime is switched off.

The single-control modes are unchanged: the realtime button still takes the
card slot, so there is never more than one of it on screen.
…venlabs-realtime

# Conflicts:
#	app/src-tauri/Cargo.lock
Addresses two review findings on the realtime harness.

A turn whose task panics or is aborted drops the oneshot sender, so the
foreground takes the lost-result branch and ends the turn. Nothing delivered
to chat on that path - and unlike the ack-deadline case there is no second
chance, because the task died before reaching its own delivery branch. If the
turn had already promised a follow-up, the user waited for a message that was
never coming. The notice is posted from the foreground instead, gated on the
same is_answerable_prompt rule as every other failure notice so a read-back or
a recognition artefact stays silent.

The read-back prefix is a contract spanning Rust and TypeScript, held together
only by a "MUST match" comment in each. Each side's unit tests assert against
its own copy, so a divergence would pass both suites and fail open in a live
call - the loop guard stops recognising read-back turns. A test now reads the
Rust source and pins the two together; verified it fails when they diverge.
coderabbitai[bot]
coderabbitai Bot previously approved these changes Aug 13, 2026
The merge resolution regenerated this lock while the vendored submodules were
still at their pre-merge commits, so it recorded the wrong dependency versions
and CI rejected it under --locked. With the submodules synced to the pointers
the merge brought in, upstream's lock verifies as-is - there was nothing to
resync. Reverts to it.
@M3gA-Mind

Copy link
Copy Markdown
Collaborator

Review — comment only (no approval)

Reviewed 630060f15 against origin/main 0627b6407. Substantial, careful work — the streaming/ack-defer design is coherent, the pure helpers are well factored and genuinely tested, and CI is fully green including Rust Core Coverage and Frontend Checks. Two things I'd want resolved before this ships, neither of which CI or CodeRabbit would catch.

F1 — the voice turn gets a routable approval card without the containment its precedents carry, and the card outlives its turn

run_single_with_timeout scopes ApprovalChatContext { thread_id: "proactive:voice", client_id: "system" } on an AgentTurnOrigin::ExternalChannel { channel: "voice", sender: None }. The gate's ExternalChannel arm (security/approval/gate.rs:750-758) falls through to parking, and its comment states the intent plainly: "We do NOT short-circuit to Allow here — remote inputs are untrusted. Without a routable surface the park TTL-denies." With a thread + client present, thread_to_request["proactive:voice"] is populated and ApprovalRequested reaches ApprovalSurfaceSubscriber with (Some, Some) — so an external-effect tool raised by a voice turn now surfaces an approvable card where it previously failed closed. The doc comment endorses this, so it's deliberate.

Precedent exists and I want to credit itmeet/backend_bot/in_call.rs:475-489 does the same on an ExternalChannel { channel: "meet", sender: None } turn. But it pairs the routable surface with three properties this PR doesn't adopt:

in_call copilot voice (this PR)
routing thread per-meeting (get_or_create_meeting_thread) per-flow-run fixed global constant
per-session key APPROVAL_IN_CALL_CONTEXT { meeting_key } APPROVAL_FLOW_RUN_CONTEXT none
park TTL clamped to 120 s clamped to 180 s gate default, 600 s

Two consequences:

(a) The card outlives its own turn by 6.7×. The voice turn is hard-capped at TURN_TIMEOUT_SECS = 90, but a park raised inside it persists expires_at = now + 600 s. When the 90 s timeout drops the turn future, WaiterGuard::drop (#4774) evicts the waiter and denies the row — while the card is still on screen with ~8.5 minutes of advertised expiry left; clicking Approve then lands on DecideMiss::AlreadyResolved. This is exactly the mismatch resolve_park_ttl was added to prevent — its comment describes the identical bug for copilot streaming ("could advertise the old 10-minute expires_at while only actually waiting 180s", PR #5112 review).

(b) One global routing slot. thread_to_request is one entry per thread and sender is None, so every voice turn (and anything else on proactive:voice) contends for the same slot, last writer wins. clear_thread_route_if_owned exists because that slot is contended; the precedents dodge it by keying on the meeting/run, which a constant can't.

Suggested: scope a voice TTL clamp so the advertised expiry never exceeds the 90 s ceiling (the APPROVAL_IN_CALL_CONTEXT shape is right there), and derive the routing thread from the correlation id.

F2 — composio_connect needed the context's presence, not a routable surface

integrations/composio/tools.rs:834-842 checks APPROVAL_CHAT_CONTEXT.try_with(|_| ()).is_err() — presence only, never reading thread_id/client_id — and the reported Gmail bug is the already-connected short-circuit at :845-847, which returns success without rendering a card. So the confabulation fix doesn't require the real thread/client; that's what additionally hands the gate the routable surface in F1.

Fair counterpoint: the not-yet-connected case genuinely does need a real thread for the connect card to render. If that's intended, worth saying so and pairing it with F1's containment, rather than having it arrive as a side effect.

F3 — set_model_name mutates one half of a documented two-part invariant

builder/factory.rs:513-518 deliberately computes the vision flag "for the (now-final) model" after all pinning, and types.rs:68-73 documents model_vision as tied to model_name. The new setter mutates the name and leaves model_vision describing the previous model; its doc explains the KV-cache reasoning but not this coupling. Blast radius is modest — turn/core.rs:1252 reads turn_models.supports_vision() || self.model_vision and the first operand does track effective_model, so only a stale true leaks (images rehydrated for a model that may not accept them). Since voice turns are seeded with the relayed chat history, an [IMAGE:…] placeholder in scope isn't hypothetical. The vision_preflight diagnostic (#5146) also now names a model that isn't running. The setter is pub, so the next caller inherits the trap — either recompute model_vision or state the constraint.

F4 — the default flags ship realtime instead of tap-and-speak, contradicting the body

The body says the deviation from #5399 is "we keep the old path unchanged and ship the new path as a separate button, so both can be compared side by side", and Impact says "additive". But HUMAN_VOICE_REALTIME_ENABLED is !== 'false'true by default and HUMAN_VOICE_SHOW_BOTH is === 'true'false by default, so resolveHumanVoiceEntry returns 'realtime'showMicComposer={false} → the tap-and-speak MicComposer is not rendered on the Human tab (Conversations.tsx:2332-2344, HumanPage.tsx:76-80). Side-by-side needs VITE_HUMAN_VOICE_SHOW_BOTH=true, which no checked-in env sets. The classic component is unchanged; its entry point on this surface is gone by default. Either flip the default or correct the body — as written, someone approving on "additive, both available" would be approving something else.

F5 — removing the settings toggle strands a live consumer

VoicePanel.tsx drops the only setVoiceMode dispatcher in the app (no other non-test caller). But chatMascot/ChatMascotStage.tsx:39-40 still gates on VOICE_MODE_FLAG_ENABLED && voiceMode === 'realtime', with a comment saying "the gate moves here rather than being dropped … so it still ships dark." mascotSlice initialises voiceMode: 'classic' (:305) and rehydrates to 'classic' (:636), so that surface can now never be lit by any user action. "Removing the toggle does not alter any persisted value" is true but beside the point — keep the toggle, delete the unreachable gate, or say the surface is retired.

F6 / F7 — lower

  • VOICE_MODEL = "chat-v1" is pinned unconditionally, so a deployment where it doesn't resolve fails every voice turn rather than degrading. The body notes the config dependency; there's no code-side guard. Consider pinning only when the model resolves.
  • The deferred path leaves the forwarder running on the assumption that "any late deltas reach a settled relay turn and are dropped harmlessly" — that's backend-relay behaviour that can't be verified from this repo and isn't tested. The background turn can emit deltas for ~80 s more; if the relay doesn't drop them they're spoken into the next open turn, and the same content also arrives via voice_speak, so the user hears it twice. Worth confirming and citing, or gating the forwarder on a settled flag.

Nits

  • The read-back and content-free early returns now run before InFlightGuard::claim (old code claimed first), so relay retries of those two turn types are no longer deduped. Cheap paths, but it looks deliberate and isn't called out.
  • streamed.load() in the deadline arm races the still-running forwarder — a delta landing microseconds earlier can still get VOICE_HANDOFF_LINE stacked on it. Cosmetic.
  • The reviewer note about cargo test not compiling from this base (dead core::event_bus import in memory/guard/provider_tests.rs) is stale — the import is gone and Rust Core Coverage is green on this head.

Credit where due

readbackPrefix.contract.test.ts is a genuinely good cross-language contract test — it reads the Rust source, pins the constant, and includes an explicit anti-vacuity guard so a renamed constant fails loudly instead of passing silently. That guard is the part most people skip, and since that string is the only thing preventing an unbounded speak-back loop, pinning it was the right instinct. The pure helpers are well factored and well tested (including non-Latin scripts in is_content_free, which is the case such helpers usually get wrong). Moving the semaphore acquire inside the detached task is a real bug fix with a comment that explains the failure it fixes. Diff hygiene is clean — no lockfile/vendor//Cargo.toml drift, and the branch explicitly reverted an app/src-tauri/Cargo.lock change it had picked up.

Coverage

The PR is honest that diff coverage isn't met on the async orchestration, and that's accurate: every new test covers a pure helper. Untested are the ack-deadline race, the detached-task handoff, the permit-inside-the-task fix, and the result_tx.send failure branch that chooses between chat delivery, a failure notice, and skipping. That last one carries real user-visible branching and is reachable with just a oneshot and no live provider — worth a test even if the rest stays e2e-only.

Bottom line: good engineering with clear reasoning in the comments. F1 and F4 are the two I'd want addressed before merge — F1 because it changes the fail-closed posture for an external channel and leaves a card that outlives its waiter, F4 because the body describes a different rollout than the defaults implement. F3 and F5 are small and mechanical. Approve/merge is the maintainer's call — deliberately not approving.

@M3gA-Mind
M3gA-Mind merged commit 0d65013 into tinyhumansai:main Aug 13, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent Built-in agents, prompts, orchestration, and agent runtime in src/openhuman/agent/. feature Net-new user-facing capability or product behavior. priority: p2 Soon. Real but survivable — a rough edge, a gap, a thing that will bite later. rust-core Core Rust runtime in src/: CLI, core_server, shared infrastructure.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Replace current voice chat with ElevenLabs Voice Agents

2 participants