mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-24 19:35:28 -06:00
fix(talk): enforce relay terminal precedence
This commit is contained in:
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user