fix(talk): retire pending transport on overflow

This commit is contained in:
Vincent Koc
2026-08-01 15:01:35 +08:00
parent b85062825b
commit 91e86a329c
2 changed files with 59 additions and 6 deletions
@@ -308,6 +308,56 @@ describe("RealtimeTalkSession lifecycle", () => {
session.stop();
});
it("stops a pending replacement when transcript overflow closes the active call", async () => {
const warn = vi.spyOn(console, "warn").mockImplementation(() => undefined);
const replacementStart = createDeferred<void>();
const firstTranscript = createDeferred<void>();
const request = vi.fn(async (method: string, params?: { entryId?: string }) => {
if (method === "talk.client.create") {
return {
provider: "openai",
transport: "webrtc",
voiceSessionId: "voice-overflow-replacement",
clientSecret: "secret",
};
}
if (method === "talk.client.transcript" && params?.entryId === "1") {
await firstTranscript.promise;
}
return { ok: true };
});
const onStatus = vi.fn();
const session = new RealtimeTalkSession({ request } as never, "agent:main:main", {
onStatus,
});
await session.start();
const activeContext = transcriptContext(transportMock.webRtcContexts);
transportMock.start.mockImplementationOnce(async () => await replacementStart.promise);
const replacement = session.start();
await vi.waitFor(() => expect(transportMock.webRtcContexts).toHaveLength(2));
for (let index = 0; index < 42; index += 1) {
activeContext.callbacks.onTranscript?.({
role: "user",
text: "x".repeat(9_000),
final: true,
});
}
expect(onStatus).toHaveBeenCalledWith("error", expect.stringContaining("could not keep up"));
expect(warn).toHaveBeenCalledOnce();
expect(transportMock.webRtcStops[0]).toHaveBeenCalledWith();
expect(transportMock.webRtcStops[1]).toHaveBeenCalledWith({ emitClosed: false });
replacementStart.resolve();
await replacement;
firstTranscript.resolve();
await vi.waitFor(() =>
expect(request.mock.calls.some(([method]) => method === "talk.client.close")).toBe(true),
);
warn.mockRestore();
});
it("releases newly allocated owners after transport startup failures", async () => {
let createCount = 0;
const request = vi.fn(async (method: string) => {
+9 -6
View File
@@ -168,9 +168,7 @@ export class RealtimeTalkSession {
let ownerTransferred = false;
try {
const lifecycleGeneration = ++this.lifecycleGeneration;
const supersededPendingTransport = this.pendingTransport;
this.pendingTransport = null;
supersededPendingTransport?.stop({ emitClosed: false });
this.stopPendingTransport();
this.closed = false;
this.callbacks.onStatus?.("connecting");
const existingTransport = this.transport;
@@ -393,9 +391,7 @@ export class RealtimeTalkSession {
this.videoEnabled = false;
activeRealtimeTalkSessions.delete(this);
this.callbacks.onStatus?.("idle");
const pendingTransport = this.pendingTransport;
this.pendingTransport = null;
pendingTransport?.stop({ emitClosed: false });
this.stopPendingTransport();
const detached = this.detachVoiceSession();
this.transport?.stop();
this.transport = null;
@@ -404,6 +400,12 @@ export class RealtimeTalkSession {
}
}
private stopPendingTransport(): void {
const pendingTransport = this.pendingTransport;
this.pendingTransport = null;
pendingTransport?.stop({ emitClosed: false });
}
private closeUnadoptedVoiceSession(
voiceSessionId: string,
transport: string,
@@ -511,6 +513,7 @@ export class RealtimeTalkSession {
this.videoOperation += 1;
this.videoEnabled = false;
activeRealtimeTalkSessions.delete(this);
this.stopPendingTransport();
const detached = this.detachVoiceSession();
// Retire the overflowing transport before accepted-write and close failures
// settle so the first terminal persistence error keeps precedence.