From 3c158e63b59200c31f346a2c587c1019bf659ffd Mon Sep 17 00:00:00 2001 From: Shanu Date: Fri, 14 Aug 2026 13:17:27 +0530 Subject: [PATCH 1/2] fix(voice): make the deadline line a neutral acknowledgement "I'll have that for you in a moment" states an outcome the turn cannot guarantee the shape of. The answer may follow a second later or thirty, and on a turn the caller expected to be trivial - "no, not now" - a promise of future delivery reads as the assistant having misunderstood the question rather than as it working. A short acknowledgement says the only thing actually known at that point: work is still going. Rotated so a caller who hits the deadline twice in one call does not hear the same words back, and each line ends in a full stop because the provider synthesises on sentence boundaries - an unterminated line is buffered rather than spoken, which is the same defect that kept the relay's fillers inaudible for eight seconds. The answer itself is unaffected: it still lands in chat and is read aloud while the call is up. --- src/openhuman/voice/realtime_harness.rs | 64 +++++++++++++++++++++++-- 1 file changed, 59 insertions(+), 5 deletions(-) diff --git a/src/openhuman/voice/realtime_harness.rs b/src/openhuman/voice/realtime_harness.rs index 4515eddacc..f3f3841287 100644 --- a/src/openhuman/voice/realtime_harness.rs +++ b/src/openhuman/voice/realtime_harness.rs @@ -13,7 +13,7 @@ //! audit-trail path rather than running with trusted-CLI semantics. use std::collections::HashSet; -use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::{Arc, LazyLock, Mutex}; use std::time::Duration; @@ -47,7 +47,7 @@ const TURN_TIMEOUT_SECS: u64 = 180; /// token in ~11-12s, and a slow tool action (email/calendar summary can be /// 20-30s of Composio round-trips) will never fit that window. Once this elapses /// we close the voice turn cleanly — the caller has heard the relay's spoken -/// filler and is told the answer is still coming (see VOICE_HANDOFF_LINE) — and +/// filler and is told the answer is still coming (see VOICE_HANDOFF_LINES) — and /// let the orchestrator finish in the background, /// delivering its answer into the user's in-app chat and, while the call is /// still up, reading it aloud. Sits under the provider's cancel deadline so @@ -67,9 +67,33 @@ const VOICE_ACK_DEADLINE_SECS: u64 = 8; /// slow turn ended on the relay's ellipsis padding and nothing else, which sounds /// like the assistant losing the thread rather than working on an answer. /// -/// Both delivery paths honour the promise: the answer is posted to chat and, +/// Neutral rather than a promise. "I'll have that for you in a moment" states an +/// outcome the turn cannot guarantee the shape of: the answer may follow a second +/// later, or thirty, and on a turn the caller expected to be trivial ("no, not +/// now") a promise of future delivery reads as the assistant having +/// misunderstood. A short acknowledgement says the same thing about the only fact +/// known at this point — work is still going — without committing to when. +/// +/// Rotated per turn so a caller who hits the deadline twice in a call does not +/// hear the same words back. Each ends in a full stop deliberately: the provider +/// synthesises on sentence boundaries, so an unterminated line is buffered rather +/// than spoken (see `VOICE_FILLERS` in the backend relay). +/// +/// The answer itself still arrives on both delivery paths — posted to chat and, /// while the call is still up, read aloud. -const VOICE_HANDOFF_LINE: &str = "I'll have that for you in a moment. "; +const VOICE_HANDOFF_LINES: [&str; 4] = [ + "Still on it. ", + "Almost there. ", + "Still working on it. ", + "Bear with me. ", +]; +static VOICE_HANDOFF_CURSOR: AtomicUsize = AtomicUsize::new(0); + +/// Next handoff line, rotating. Pure apart from the cursor; unit-tested. +fn next_handoff_line() -> &'static str { + VOICE_HANDOFF_LINES + [VOICE_HANDOFF_CURSOR.fetch_add(1, Ordering::Relaxed) % VOICE_HANDOFF_LINES.len()] +} /// Sent for a turn we have nothing to say to. /// @@ -457,7 +481,7 @@ pub async fn handle_voice_harness_turn(correlation_id: String, messages: Vec Date: Fri, 14 Aug 2026 13:46:36 +0530 Subject: [PATCH 2/2] fix(voice): drop the completion claim from the deadline lines Review caught that "Almost there." undoes the point of the change. The turn has no evidence it is nearly done - it may be blocked, or thirty seconds from an answer - so the phrase promises progress the same way the line it replaced promised delivery. "Still going." states only what is known. The no-ellipsis assertion rejected U+2026 but not "...", so "Still on it... " passed the period and trailing-space checks while breaking the contract the test exists to hold. Verified the strengthened form fails on exactly that input. --- src/openhuman/voice/realtime_harness.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/openhuman/voice/realtime_harness.rs b/src/openhuman/voice/realtime_harness.rs index f3f3841287..8942af72b3 100644 --- a/src/openhuman/voice/realtime_harness.rs +++ b/src/openhuman/voice/realtime_harness.rs @@ -83,7 +83,7 @@ const VOICE_ACK_DEADLINE_SECS: u64 = 8; /// while the call is still up, read aloud. const VOICE_HANDOFF_LINES: [&str; 4] = [ "Still on it. ", - "Almost there. ", + "Still going. ", "Still working on it. ", "Bear with me. ", ]; @@ -916,8 +916,8 @@ mod tests { "trailing space keeps speech unglued: {line:?}" ); assert!( - !line.contains('…'), - "an ellipsis is not a sentence end: {line:?}" + !line.contains("...") && !line.contains('…'), + "an ellipsis is not a sentence end, in either form: {line:?}" ); } }