Skip to content

Commit e29ed35

Browse files
authored
Merge pull request #33 from inkbox-ai/fix/hosted-voice-delivery
Stabilize hosted voice delivery and update dependencies
2 parents 309fa7f + b70045e commit e29ed35

7 files changed

Lines changed: 51 additions & 23 deletions

File tree

.github/workflows/live-voice.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ jobs:
9494
HOSTED_MARKER="$(node scripts/nato-marker.mjs "$GITHUB_RUN_ID" "$GITHUB_RUN_ATTEMPT")"
9595
echo "HOSTED_POST_CALL_MARKER=$HOSTED_MARKER" >> "$GITHUB_ENV"
9696
export VOICE_DRIVER_LINE="After we hang up, send me one SMS containing exactly these three words: $HOSTED_MARKER. Create one post-call action now. Set both the action title and the action details to this exact five-word phrase: Send SMS $HOSTED_MARKER. Wait for the action tool to succeed, then read the exact three-word SMS body back to me. Do not paraphrase, omit a word, or send the SMS during the call."
97+
export VOICE_DRIVER_QUIET_AFTER_TRANSCRIPT=2
9798
export VOICE_DRIVER_LISTEN=180
9899
export VOICE_DRIVER_AUTO_STOP=false
99100
fi

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Unreleased
44

5+
- Stabilizes hosted voice checks by waiting for a quiet greeting and verifying the caller request on both call legs.
6+
- Updates transitive HTTP and development dependencies to patched versions.
57
- Runs live test files sequentially to avoid interference between channel checks.
68
- Adds negotiated 16 kHz PCM call audio with streaming resampling and legacy call compatibility.
79
- Uses the published, pinned SDK in CI and gives periodic progress delivery time to finish before the live task ends.

package-lock.json

Lines changed: 15 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@inkbox/opencode-plugin",
3-
"version": "0.2.12",
3+
"version": "0.2.13",
44
"private": true,
55
"description": "Inkbox for opencode \u2014 give your agent an email address, a phone number (SMS/MMS + voice), iMessage, contacts, notes, and an encrypted credential vault.",
66
"license": "MIT",

tests/contract/live-harness.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { describe, expect, it } from "vitest";
44
const liveAut = readFileSync("scripts/live-aut.sh", "utf8");
55
const liveChannels = readFileSync(".github/workflows/live-channels.yml", "utf8");
66
const liveVoice = readFileSync(".github/workflows/live-voice.yml", "utf8");
7+
const voiceDriver = readFileSync("tests/live/voice-driver.mjs", "utf8");
78
const liveStack = readFileSync(".github/workflows/live-stack.yml", "utf8");
89

910
function shellCommands(source: string): string[] {
@@ -63,6 +64,12 @@ describe("live harness readiness bounds", () => {
6364
);
6465
});
6566

67+
it("waits for the hosted greeting to go quiet before speaking", () => {
68+
expect(liveVoice).toContain("export VOICE_DRIVER_QUIET_AFTER_TRANSCRIPT=2");
69+
expect(voiceDriver).toContain("lastTranscriptAt + QUIET_AFTER_TRANSCRIPT_MS");
70+
expect(voiceDriver).not.toContain("speak now if the greeting beat our timer");
71+
});
72+
6673
it("keeps failure diagnostics content-free and out of public artifacts", () => {
6774
for (const workflow of [liveChannels, liveVoice]) {
6875
expect(workflow).toContain("Report content-free failure state");

tests/live/voice-driver.mjs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
// file (ws url + phone-number id) the test reads.
1212
//
1313
// Env: REMOTE_INKBOX_API_KEY, INKBOX_BASE_URL, VOICE_DRIVER_STATE,
14-
// VOICE_DRIVER_LINE, VOICE_DRIVER_SPEAK_AFTER (s), VOICE_DRIVER_LISTEN (s),
14+
// VOICE_DRIVER_LINE, VOICE_DRIVER_SPEAK_AFTER (s),
15+
// VOICE_DRIVER_QUIET_AFTER_TRANSCRIPT (s), VOICE_DRIVER_LISTEN (s),
1516
// VOICE_DRIVER_AUTO_STOP (false lets the test own hangup timing)
1617
import { writeFileSync } from "node:fs";
1718
import { Inkbox } from "@inkbox/sdk";
@@ -32,6 +33,8 @@ const GREETING = process.env.VOICE_DRIVER_GREETING || "Hello?";
3233
// then give the agent a turn and hang up (a dropped WS does NOT end the call — an
3334
// explicit stop is required or the leg lingers to the server max-duration cap).
3435
const SPEAK_AFTER_MS = Number(process.env.VOICE_DRIVER_SPEAK_AFTER || "5") * 1000;
36+
const QUIET_AFTER_TRANSCRIPT_MS =
37+
Number(process.env.VOICE_DRIVER_QUIET_AFTER_TRANSCRIPT || "0") * 1000;
3538
const LISTEN_MS = Number(process.env.VOICE_DRIVER_LISTEN || "12") * 1000;
3639
const AUTO_STOP = process.env.VOICE_DRIVER_AUTO_STOP !== "false";
3740

@@ -58,6 +61,7 @@ async function callWsHandler(ws) {
5861
});
5962
console.log("call WS accepted");
6063
let spoke = false;
64+
let lastTranscriptAt = 0;
6165
const say = async (text) => {
6266
await ws.send(JSON.stringify({ event: "text", delta: text }));
6367
await ws.send(JSON.stringify({ event: "text", done: true }));
@@ -70,7 +74,13 @@ async function callWsHandler(ws) {
7074
};
7175
const runTurn = async () => {
7276
await say(GREETING);
73-
await sleep(SPEAK_AFTER_MS);
77+
const earliestSpeechAt = Date.now() + SPEAK_AFTER_MS;
78+
while (!spoke) {
79+
const speechAt = Math.max(earliestSpeechAt, lastTranscriptAt + QUIET_AFTER_TRANSCRIPT_MS);
80+
const delay = speechAt - Date.now();
81+
if (delay <= 0) break;
82+
await sleep(delay);
83+
}
7484
await speak(LINE);
7585
await sleep(LISTEN_MS);
7686
if (!AUTO_STOP) return;
@@ -92,9 +102,9 @@ async function callWsHandler(ws) {
92102
if (ev.event === "start") {
93103
console.log("call start");
94104
void runTurn();
95-
} else if (ev.event === "transcript" && ev.is_final) {
96-
console.log("heard (final):", ev.text);
97-
await speak(LINE); // speak now if the greeting beat our timer
105+
} else if (ev.event === "transcript") {
106+
lastTranscriptAt = Date.now();
107+
if (ev.is_final) console.log("heard (final):", ev.text);
98108
} else if (ev.event === "stop") {
99109
console.log("call stop");
100110
break;

tests/live/voice.test.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -452,14 +452,21 @@ describe.skipIf(!LIVE || !REAL_MODEL)("live voice", () => {
452452
.join(" ")
453453
.toLowerCase()
454454
.replace(/[^a-z0-9]+/g, " ");
455+
const autCaller = autSegments.remote
456+
.join(" ")
457+
.toLowerCase()
458+
.replace(/[^a-z0-9]+/g, " ");
455459
const openActions = (currentAutCall.postCallActionItems ?? []).filter(
456460
(item: any) => String(item.status ?? "").toLowerCase() === "open",
457461
);
458462
const actionEvidence = openActions.map((item: any) =>
459463
[item.action, item.details].filter(Boolean).join(" "),
460464
);
461-
const callerReady =
465+
const driverCallerReady =
462466
hasAfterCallSmsIntent(caller) && containsVoiceMarker(caller, HOSTED_MARKER);
467+
const autCallerReady =
468+
hasAfterCallSmsIntent(autCaller) && containsVoiceMarker(autCaller, HOSTED_MARKER);
469+
const callerReady = driverCallerReady && autCallerReady;
463470
const matchingActions = actionEvidence.filter(
464471
(value: string) => hasSmsIntent(value) && containsVoiceMarker(value, HOSTED_MARKER),
465472
);
@@ -477,7 +484,8 @@ describe.skipIf(!LIVE || !REAL_MODEL)("live voice", () => {
477484
markerActionCount === 1;
478485
progress.last =
479486
`agent_segments=${autSegments.local.length} two_way_ready=${twoWayReady} ` +
480-
`caller_ready=${callerReady} ` +
487+
`caller_ready=${callerReady} driver_caller_ready=${driverCallerReady} ` +
488+
`aut_caller_ready=${autCallerReady} ` +
481489
`action_ready=${actionReady} open_actions=${openActions.length} ` +
482490
`sms_actions=${smsActionCount} marker_actions=${markerActionCount}`;
483491
if (twoWayReady && callerReady && actionReady) break;

0 commit comments

Comments
 (0)