

























@@ -1296,6 +1296,338 @@ describe("DiscordVoiceManager", () => {
12961296);
12971297});
129812981299+it("reuses forced agent-proxy answers for late matching consult tool calls", async () => {
1300+agentCommandMock.mockResolvedValueOnce({ payloads: [{ text: "forced answer" }] });
1301+const manager = createManager({
1302+groupPolicy: "open",
1303+voice: {
1304+enabled: true,
1305+mode: "agent-proxy",
1306+realtime: { provider: "openai" },
1307+},
1308+});
1309+1310+await manager.join({ guildId: "g1", channelId: "1001" });
1311+const entry = getSessionEntry(manager) as {
1312+realtime?: {
1313+beginSpeakerTurn: (
1314+context: { extraSystemPrompt?: string; senderIsOwner: boolean; speakerLabel: string },
1315+userId: string,
1316+) => { close: () => void; sendInputAudio: (audio: Buffer) => void };
1317+};
1318+};
1319+const bridgeParams = createRealtimeVoiceBridgeSessionMock.mock.calls.at(-1)?.[0] as
1320+| {
1321+onToolCall?: (
1322+event: {
1323+itemId: string;
1324+callId: string;
1325+name: string;
1326+args: unknown;
1327+},
1328+session: typeof realtimeSessionMock,
1329+) => void;
1330+onTranscript?: (role: "user" | "assistant", text: string, isFinal: boolean) => void;
1331+}
1332+| undefined;
1333+1334+const ownerTurn = entry.realtime?.beginSpeakerTurn(
1335+{ extraSystemPrompt: undefined, senderIsOwner: true, speakerLabel: "Owner" },
1336+"u-owner",
1337+);
1338+ownerTurn?.sendInputAudio(Buffer.alloc(8));
1339+bridgeParams?.onTranscript?.("user", "late question", true);
1340+1341+await new Promise((resolve) => setTimeout(resolve, 260));
1342+1343+bridgeParams?.onToolCall?.(
1344+{
1345+itemId: "item-late",
1346+callId: "call-late",
1347+name: "openclaw_agent_consult",
1348+args: { question: "late question" },
1349+},
1350+realtimeSessionMock,
1351+);
1352+await Promise.resolve();
1353+await Promise.resolve();
1354+1355+expect(agentCommandMock).toHaveBeenCalledTimes(1);
1356+expect(realtimeSessionMock.sendUserMessage).toHaveBeenCalledWith(
1357+expect.stringContaining("forced answer"),
1358+);
1359+expect(realtimeSessionMock.submitToolResult).toHaveBeenCalledWith(
1360+"call-late",
1361+{
1362+status: "already_delivered",
1363+message: "OpenClaw already delivered this answer to Discord voice.",
1364+},
1365+{ suppressResponse: true },
1366+);
1367+});
1368+1369+it("suppresses late forced agent-proxy tool calls when the forced consult rejects", async () => {
1370+let rejectAgentTurn: ((error: unknown) => void) | undefined;
1371+agentCommandMock.mockReturnValueOnce(
1372+new Promise((_, reject) => {
1373+rejectAgentTurn = reject;
1374+}),
1375+);
1376+const manager = createManager({
1377+groupPolicy: "open",
1378+voice: {
1379+enabled: true,
1380+mode: "agent-proxy",
1381+realtime: { provider: "openai" },
1382+},
1383+});
1384+1385+await manager.join({ guildId: "g1", channelId: "1001" });
1386+const entry = getSessionEntry(manager) as {
1387+realtime?: {
1388+beginSpeakerTurn: (
1389+context: { extraSystemPrompt?: string; senderIsOwner: boolean; speakerLabel: string },
1390+userId: string,
1391+) => { close: () => void; sendInputAudio: (audio: Buffer) => void };
1392+};
1393+};
1394+const bridgeParams = createRealtimeVoiceBridgeSessionMock.mock.calls.at(-1)?.[0] as
1395+| {
1396+onToolCall?: (
1397+event: {
1398+itemId: string;
1399+callId: string;
1400+name: string;
1401+args: unknown;
1402+},
1403+session: typeof realtimeSessionMock,
1404+) => void;
1405+onTranscript?: (role: "user" | "assistant", text: string, isFinal: boolean) => void;
1406+}
1407+| undefined;
1408+1409+const ownerTurn = entry.realtime?.beginSpeakerTurn(
1410+{ extraSystemPrompt: undefined, senderIsOwner: true, speakerLabel: "Owner" },
1411+"u-owner",
1412+);
1413+ownerTurn?.sendInputAudio(Buffer.alloc(8));
1414+bridgeParams?.onTranscript?.("user", "late question", true);
1415+1416+await new Promise((resolve) => setTimeout(resolve, 260));
1417+1418+bridgeParams?.onToolCall?.(
1419+{
1420+itemId: "item-late",
1421+callId: "call-late",
1422+name: "openclaw_agent_consult",
1423+args: { question: "late question" },
1424+},
1425+realtimeSessionMock,
1426+);
1427+rejectAgentTurn?.(new Error("agent broke"));
1428+await Promise.resolve();
1429+await Promise.resolve();
1430+await new Promise((resolve) => setTimeout(resolve, 20));
1431+1432+expect(agentCommandMock).toHaveBeenCalledTimes(1);
1433+expect(realtimeSessionMock.sendUserMessage).toHaveBeenCalledWith(
1434+expect.stringContaining("I hit an error while checking that. Please try again."),
1435+);
1436+expect(realtimeSessionMock.submitToolResult).toHaveBeenCalledWith(
1437+"call-late",
1438+{
1439+status: "already_delivered",
1440+message: "OpenClaw already delivered this answer to Discord voice.",
1441+},
1442+{ suppressResponse: true },
1443+);
1444+});
1445+1446+it("does not reuse recent agent-proxy answers over newer speaker audio", async () => {
1447+agentCommandMock
1448+.mockResolvedValueOnce({ payloads: [{ text: "forced answer" }] })
1449+.mockResolvedValueOnce({ payloads: [{ text: "guest answer" }] });
1450+const manager = createManager({
1451+groupPolicy: "open",
1452+voice: {
1453+enabled: true,
1454+mode: "agent-proxy",
1455+realtime: { provider: "openai" },
1456+},
1457+});
1458+1459+await manager.join({ guildId: "g1", channelId: "1001" });
1460+const entry = getSessionEntry(manager) as {
1461+realtime?: {
1462+beginSpeakerTurn: (
1463+context: { extraSystemPrompt?: string; senderIsOwner: boolean; speakerLabel: string },
1464+userId: string,
1465+) => { close: () => void; sendInputAudio: (audio: Buffer) => void };
1466+};
1467+};
1468+const bridgeParams = createRealtimeVoiceBridgeSessionMock.mock.calls.at(-1)?.[0] as
1469+| {
1470+onToolCall?: (
1471+event: {
1472+itemId: string;
1473+callId: string;
1474+name: string;
1475+args: unknown;
1476+},
1477+session: typeof realtimeSessionMock,
1478+) => void;
1479+onTranscript?: (role: "user" | "assistant", text: string, isFinal: boolean) => void;
1480+}
1481+| undefined;
1482+1483+const ownerTurn = entry.realtime?.beginSpeakerTurn(
1484+{ extraSystemPrompt: undefined, senderIsOwner: true, speakerLabel: "Owner" },
1485+"u-owner",
1486+);
1487+ownerTurn?.sendInputAudio(Buffer.alloc(8));
1488+bridgeParams?.onTranscript?.("user", "late question", true);
1489+1490+await new Promise((resolve) => setTimeout(resolve, 260));
1491+1492+const guestTurn = entry.realtime?.beginSpeakerTurn(
1493+{ extraSystemPrompt: undefined, senderIsOwner: false, speakerLabel: "Guest" },
1494+"u-guest",
1495+);
1496+guestTurn?.sendInputAudio(Buffer.alloc(8));
1497+1498+bridgeParams?.onToolCall?.(
1499+{
1500+itemId: "item-late",
1501+callId: "call-late",
1502+name: "openclaw_agent_consult",
1503+args: { question: "late question" },
1504+},
1505+realtimeSessionMock,
1506+);
1507+await Promise.resolve();
1508+await Promise.resolve();
1509+1510+expect(agentCommandMock).toHaveBeenCalledTimes(1);
1511+expect(realtimeSessionMock.sendUserMessage).toHaveBeenCalledWith(
1512+expect.stringContaining("forced answer"),
1513+);
1514+expect(realtimeSessionMock.submitToolResult).toHaveBeenCalledWith("call-late", {
1515+error: "Discord speaker context changed before this realtime consult completed",
1516+});
1517+1518+bridgeParams?.onTranscript?.("user", "guest followup", true);
1519+await new Promise((resolve) => setTimeout(resolve, 260));
1520+1521+expect(agentCommandMock).toHaveBeenCalledTimes(2);
1522+expect(agentCommandMock).toHaveBeenNthCalledWith(
1523+2,
1524+expect.objectContaining({
1525+message: expect.stringContaining("guest followup"),
1526+senderIsOwner: false,
1527+}),
1528+expect.anything(),
1529+);
1530+expect(realtimeSessionMock.sendUserMessage).toHaveBeenCalledWith(
1531+expect.stringContaining("guest answer"),
1532+);
1533+});
1534+1535+it("prefers the newest recent agent-proxy consult for repeated questions", async () => {
1536+agentCommandMock
1537+.mockResolvedValueOnce({ payloads: [{ text: "old direct answer" }] })
1538+.mockResolvedValueOnce({ payloads: [{ text: "new forced answer" }] });
1539+const manager = createManager({
1540+groupPolicy: "open",
1541+voice: {
1542+enabled: true,
1543+mode: "agent-proxy",
1544+realtime: { provider: "openai" },
1545+},
1546+});
1547+1548+await manager.join({ guildId: "g1", channelId: "1001" });
1549+const entry = getSessionEntry(manager) as {
1550+realtime?: {
1551+beginSpeakerTurn: (
1552+context: { extraSystemPrompt?: string; senderIsOwner: boolean; speakerLabel: string },
1553+userId: string,
1554+) => { close: () => void; sendInputAudio: (audio: Buffer) => void };
1555+};
1556+};
1557+const bridgeParams = createRealtimeVoiceBridgeSessionMock.mock.calls.at(-1)?.[0] as
1558+| {
1559+onToolCall?: (
1560+event: {
1561+itemId: string;
1562+callId: string;
1563+name: string;
1564+args: unknown;
1565+},
1566+session: typeof realtimeSessionMock,
1567+) => void;
1568+onTranscript?: (role: "user" | "assistant", text: string, isFinal: boolean) => void;
1569+}
1570+| undefined;
1571+1572+const firstTurn = entry.realtime?.beginSpeakerTurn(
1573+{ extraSystemPrompt: undefined, senderIsOwner: true, speakerLabel: "Owner" },
1574+"u-owner",
1575+);
1576+firstTurn?.sendInputAudio(Buffer.alloc(8));
1577+bridgeParams?.onToolCall?.(
1578+{
1579+itemId: "item-old",
1580+callId: "call-old",
1581+name: "openclaw_agent_consult",
1582+args: { question: "repeat question" },
1583+},
1584+realtimeSessionMock,
1585+);
1586+await Promise.resolve();
1587+await Promise.resolve();
1588+await new Promise((resolve) => setTimeout(resolve, 20));
1589+1590+expect(realtimeSessionMock.submitToolResult).toHaveBeenCalledWith("call-old", {
1591+text: "old direct answer",
1592+});
1593+1594+const secondTurn = entry.realtime?.beginSpeakerTurn(
1595+{ extraSystemPrompt: undefined, senderIsOwner: true, speakerLabel: "Owner" },
1596+"u-owner",
1597+);
1598+secondTurn?.sendInputAudio(Buffer.alloc(8));
1599+bridgeParams?.onTranscript?.("user", "repeat question", true);
1600+await new Promise((resolve) => setTimeout(resolve, 260));
1601+1602+bridgeParams?.onToolCall?.(
1603+{
1604+itemId: "item-new",
1605+callId: "call-new",
1606+name: "openclaw_agent_consult",
1607+args: { question: "repeat question" },
1608+},
1609+realtimeSessionMock,
1610+);
1611+await Promise.resolve();
1612+await Promise.resolve();
1613+1614+expect(agentCommandMock).toHaveBeenCalledTimes(2);
1615+expect(realtimeSessionMock.sendUserMessage).toHaveBeenCalledWith(
1616+expect.stringContaining("new forced answer"),
1617+);
1618+expect(realtimeSessionMock.submitToolResult).toHaveBeenCalledWith(
1619+"call-new",
1620+{
1621+status: "already_delivered",
1622+message: "OpenClaw already delivered this answer to Discord voice.",
1623+},
1624+{ suppressResponse: true },
1625+);
1626+expect(realtimeSessionMock.submitToolResult).not.toHaveBeenCalledWith("call-new", {
1627+text: "old direct answer",
1628+});
1629+});
1630+12991631it("expires closed agent-proxy turns before later speaker audio", async () => {
13001632agentCommandMock.mockResolvedValueOnce({ payloads: [{ text: "guest answer" }] });
13011633const manager = createManager({
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。