fix(talk): enforce relay terminal precedence

This commit is contained in:
Vincent Koc
2026-08-02 19:26:37 +08:00
parent ea29bbcc64
commit eec0d0310c
3 changed files with 32 additions and 5 deletions
@@ -2,16 +2,21 @@ import { describe, expect, it, vi } from "vitest";
import { RelayToolCallLedger } from "./talk-realtime-relay-tool-call-ledger.js";
describe("RelayToolCallLedger", () => {
it("shares one retained identity across cancellation and terminal outcomes", () => {
it("preserves the first cancellation turn until agent completion wins", () => {
const ledger = new RelayToolCallLedger({ onOverflow: vi.fn(), maxEntries: 2 });
expect(ledger.tryAdmit(["call-1"])).toBe(true);
expect(ledger.markCancelled(["call-1"], "turn-1")).toBe(true);
expect(ledger.markCancelled(["call-1"], "turn-2")).toBe(true);
expect(ledger.cancelledTurnId("call-1")).toBe("turn-1");
expect(ledger.markAgentCompleted(["call-1"])).toBe(true);
expect(ledger.hasCancelled("call-1")).toBe(false);
expect(ledger.markCancelled(["call-1"], "turn-3")).toBe(true);
expect(ledger.markProviderCompleted(["call-1"])).toBe(true);
expect(ledger.size).toBe(1);
expect(ledger.cancelledTurnId("call-1")).toBe("turn-1");
expect(ledger.cancelledTurnId("call-1")).toBeUndefined();
expect(ledger.isAgentCompleted("call-1")).toBe(true);
expect(ledger.isProviderCompleted("call-1")).toBe(true);
});
@@ -35,9 +40,9 @@ describe("RelayToolCallLedger", () => {
const ledger = new RelayToolCallLedger({ onOverflow: vi.fn(), maxEntries: 1 });
ledger.markCancelled(["call-1"], "turn-1");
ledger.deleteCancelled("call-1");
ledger.markAgentCompleted(["call-1"]);
ledger.markProviderCompleted(["call-1"]);
ledger.deleteCancelled("call-1");
ledger.deleteAgentCompleted("call-1");
ledger.clearProviderCompleted();
@@ -73,7 +73,10 @@ export class RelayToolCallLedger {
markAgentCompleted(callIds: Iterable<string>): boolean {
return this.mark(callIds, (entry) => {
// Completion is terminal for the owner-facing call. Do not let stale
// cancellation metadata route a later result through cancellation again.
entry.agentCompleted = true;
delete entry.cancelledTurnId;
});
}
@@ -111,7 +114,11 @@ export class RelayToolCallLedger {
markCancelled(callIds: Iterable<string>, turnId: string): boolean {
return this.mark(callIds, (entry) => {
entry.cancelledTurnId = turnId;
// The first cancel owns the original turn until completion. Retries and
// late cancellation events must not replace or revive that lifecycle fact.
if (!entry.agentCompleted && entry.cancelledTurnId === undefined) {
entry.cancelledTurnId = turnId;
}
});
}
+16 -1
View File
@@ -3988,6 +3988,7 @@ describe("talk realtime gateway relay", () => {
it("fails closed when retained relay tool-call identities reach their hard cap", () => {
let bridgeRequest: RealtimeVoiceBridgeCreateRequest | undefined;
const close = vi.fn();
const submitToolResult = vi.fn();
const provider: RealtimeVoiceProviderPlugin = {
id: "relay-test",
label: "Relay Test",
@@ -3999,7 +4000,7 @@ describe("talk realtime gateway relay", () => {
sendAudio: vi.fn(),
setMediaTimestamp: vi.fn(),
handleBargeIn: vi.fn(),
submitToolResult: vi.fn(),
submitToolResult,
acknowledgeMark: vi.fn(),
close,
isConnected: vi.fn(() => true),
@@ -4017,6 +4018,13 @@ describe("talk realtime gateway relay", () => {
});
const retainedSession = relaySessions.get(session.relaySessionId);
expect(retainedSession).toBeDefined();
const toolCallEventCount = () =>
broadcastToConnIds.mock.calls.filter(
([, payload]) =>
typeof payload === "object" &&
payload !== null &&
(payload as { type?: string }).type === "toolCall",
).length;
for (let index = 0; index < MAX_RELAY_TOOL_CALL_IDENTITIES; index += 1) {
bridgeRequest?.onToolCall?.({
@@ -4029,6 +4037,7 @@ describe("talk realtime gateway relay", () => {
expect(retainedSession?.toolCalls.size).toBe(MAX_RELAY_TOOL_CALL_IDENTITIES);
expect(relaySessions.has(session.relaySessionId)).toBe(true);
expect(toolCallEventCount()).toBe(MAX_RELAY_TOOL_CALL_IDENTITIES);
bridgeRequest?.onToolCall?.({
itemId: "item-overflow",
@@ -4039,6 +4048,9 @@ describe("talk realtime gateway relay", () => {
expect(close).toHaveBeenCalledOnce();
expect(relaySessions.has(session.relaySessionId)).toBe(false);
expect(retainedSession?.toolCalls.has("call-overflow")).toBe(false);
expect(toolCallEventCount()).toBe(MAX_RELAY_TOOL_CALL_IDENTITIES);
expect(submitToolResult).not.toHaveBeenCalled();
expect(
broadcastToConnIds.mock.calls.filter(
([, payload]) =>
@@ -4064,6 +4076,9 @@ describe("talk realtime gateway relay", () => {
});
expect(close).toHaveBeenCalledOnce();
expect(retainedSession?.toolCalls.size).toBe(MAX_RELAY_TOOL_CALL_IDENTITIES);
expect(retainedSession?.toolCalls.has("call-late")).toBe(false);
expect(toolCallEventCount()).toBe(MAX_RELAY_TOOL_CALL_IDENTITIES);
expect(submitToolResult).not.toHaveBeenCalled();
});
it("caps active relay sessions per browser connection", () => {