Skip to content

Commit 883ef6f

Browse files
committed
fix: preserve realtime voice consult context
1 parent af6a178 commit 883ef6f

2 files changed

Lines changed: 91 additions & 6 deletions

File tree

src/lib/realtime-voice-gateway-relay.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import { describe, expect, it } from "vitest";
22
import {
3+
buildRealtimeVoiceContext,
4+
clearRealtimeVoiceContextForTest,
35
DESKTOP_REALTIME_BARGE_IN_PROFILE,
46
MOBILE_REALTIME_BARGE_IN_PROFILE,
57
detectRealtimeBargeIn,
8+
recordRealtimeVoiceContext,
69
resolveRealtimeBargeInProfile,
710
withRealtimeScreenContext,
811
} from "./realtime-voice-gateway-relay";
@@ -137,4 +140,33 @@ describe("realtime gateway relay screen context", () => {
137140

138141
usePageContextStore.getState().clearContext();
139142
});
143+
144+
it("adds recent realtime voice context to consult args", () => {
145+
expect(withRealtimeScreenContext(
146+
{ question: "What was I asking about?" },
147+
"Recent realtime voice context for this CrewCMD session:\nuser: Read the README",
148+
)).toEqual({
149+
question: "What was I asking about?",
150+
context: "Recent realtime voice context for this CrewCMD session:\nuser: Read the README",
151+
});
152+
});
153+
});
154+
155+
describe("realtime gateway relay voice context", () => {
156+
it("keeps recent final voice turns by session", () => {
157+
clearRealtimeVoiceContextForTest();
158+
159+
recordRealtimeVoiceContext("main", { role: "user", text: "Read the README", final: true });
160+
recordRealtimeVoiceContext("main", { role: "assistant", text: "I am checking it now.", final: false });
161+
recordRealtimeVoiceContext("main", { role: "assistant", text: "product-videogen is a video engine.", final: true });
162+
163+
expect(buildRealtimeVoiceContext("main")).toBe([
164+
"Recent realtime voice context for this CrewCMD session:",
165+
"user: Read the README",
166+
"assistant: product-videogen is a video engine.",
167+
].join("\n"));
168+
expect(buildRealtimeVoiceContext("other")).toBeNull();
169+
170+
clearRealtimeVoiceContextForTest();
171+
});
140172
});

src/lib/realtime-voice-gateway-relay.ts

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const MOBILE_BARGE_IN_RMS_THRESHOLD = 0.055;
2020
const MOBILE_BARGE_IN_PEAK_THRESHOLD = 0.16;
2121
const MOBILE_BARGE_IN_FRAMES = 4;
2222
const MOBILE_BARGE_IN_GRACE_MS = 750;
23+
const REALTIME_VOICE_CONTEXT_LIMIT = 8;
2324

2425
export interface RealtimeBargeInProfile {
2526
rmsThreshold: number;
@@ -63,6 +64,13 @@ export interface RealtimeGatewayRelayCallbacks {
6364
onError?: (message: string) => void;
6465
}
6566

67+
interface RealtimeVoiceContextEntry {
68+
role: "user" | "assistant";
69+
text: string;
70+
}
71+
72+
const realtimeVoiceContextBySession = new Map<string, RealtimeVoiceContextEntry[]>();
73+
6674
type GatewayRelayEvent =
6775
| { relaySessionId?: string; type?: "ready" }
6876
| { relaySessionId?: string; type?: "audio"; audioBase64?: string }
@@ -232,11 +240,13 @@ export class RealtimeGatewayRelaySession {
232240
case "transcript":
233241
if (event.role && event.text) {
234242
if (event.role === "assistant" && this.pendingToolCalls > 0) return;
235-
this.callbacks.onTranscript?.({
243+
const transcript = {
236244
role: event.role,
237245
text: event.text,
238246
final: event.final ?? false,
239-
});
247+
};
248+
recordRealtimeVoiceContext(this.session.sessionKey, transcript);
249+
this.callbacks.onTranscript?.(transcript);
240250
}
241251
return;
242252
case "toolCall":
@@ -310,9 +320,17 @@ export class RealtimeGatewayRelaySession {
310320
sessionKey,
311321
callId,
312322
name,
313-
args: withRealtimeScreenContext(event.args ?? {}),
323+
args: withRealtimeScreenContext(
324+
event.args ?? {},
325+
buildRealtimeVoiceContext(this.session.sessionKey),
326+
),
314327
});
315328
if (result.finalText?.trim()) {
329+
recordRealtimeVoiceContext(this.session.sessionKey, {
330+
role: "assistant",
331+
text: result.finalText,
332+
final: true,
333+
});
316334
this.callbacks.onTranscript?.({
317335
role: "assistant",
318336
text: result.finalText.trim(),
@@ -358,20 +376,55 @@ export class RealtimeGatewayRelaySession {
358376
}
359377
}
360378

361-
export function withRealtimeScreenContext(args: unknown) {
379+
export function withRealtimeScreenContext(args: unknown, voiceContext?: string | null) {
362380
const screenContext = formatPageContextForPrompt(buildCurrentPageContextForRealtime());
363-
if (!screenContext) return args;
381+
const realtimeVoiceContext = typeof voiceContext === "string" && voiceContext.trim()
382+
? voiceContext.trim()
383+
: null;
384+
if (!screenContext && !realtimeVoiceContext) return args;
364385

365386
const normalized = normalizeRealtimeToolArgs(args);
366387
const existingContext = typeof normalized.context === "string" && normalized.context.trim()
367388
? normalized.context.trim()
368389
: null;
369390
return {
370391
...normalized,
371-
context: [existingContext, screenContext].filter(Boolean).join("\n\n"),
392+
context: [existingContext, screenContext, realtimeVoiceContext].filter(Boolean).join("\n\n"),
372393
};
373394
}
374395

396+
export function recordRealtimeVoiceContext(
397+
sessionKey: string | undefined,
398+
event: { role: "user" | "assistant"; text: string; final: boolean },
399+
) {
400+
if (!event.final) return;
401+
const key = normalizeRealtimeSessionKey(sessionKey);
402+
const text = event.text.trim();
403+
if (!text) return;
404+
405+
const entries = realtimeVoiceContextBySession.get(key) ?? [];
406+
entries.push({ role: event.role, text });
407+
realtimeVoiceContextBySession.set(key, entries.slice(-REALTIME_VOICE_CONTEXT_LIMIT));
408+
}
409+
410+
export function buildRealtimeVoiceContext(sessionKey: string | undefined) {
411+
const entries = realtimeVoiceContextBySession.get(normalizeRealtimeSessionKey(sessionKey));
412+
if (!entries?.length) return null;
413+
414+
return [
415+
"Recent realtime voice context for this CrewCMD session:",
416+
...entries.map((entry) => `${entry.role}: ${entry.text}`),
417+
].join("\n");
418+
}
419+
420+
export function clearRealtimeVoiceContextForTest() {
421+
realtimeVoiceContextBySession.clear();
422+
}
423+
424+
function normalizeRealtimeSessionKey(sessionKey: string | undefined) {
425+
return typeof sessionKey === "string" && sessionKey.trim() ? sessionKey.trim() : "main";
426+
}
427+
375428
function normalizeRealtimeToolArgs(args: unknown): Record<string, unknown> {
376429
if (args && typeof args === "object" && !Array.isArray(args)) return { ...args as Record<string, unknown> };
377430
if (typeof args === "string") {

0 commit comments

Comments
 (0)