Skip to content

Commit b80d0bc

Browse files
committed
[AI] fix(codex): continue after source-reply delivery instead of terminating turn
Delivered and receipt-confirmed source replies in message_tool_only mode no longer set terminate=true on the dynamic tool response, matching the generic embedded-runner behavior established by PR #92343. - Remove deliveredSourceReply/receiptConfirmedSourceReply from withDynamicToolTermination conditions - Only final=true + is actual source reply explicitly terminates; final omitted no longer degrades to terminate-on-delivery - toolConfirmedSourceReply (tool-returned terminate:true) still terminates unless overridden by final=false - sourceReplyFinal telemetry: still true when final omitted (preserves completed marker for stranded-reply recovery); false for final=false; true for final=true or toolConfirmed Related to #106961
1 parent 242cdca commit b80d0bc

2 files changed

Lines changed: 60 additions & 27 deletions

File tree

extensions/codex/src/app-server/dynamic-tools.test.ts

Lines changed: 53 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1317,7 +1317,7 @@ describe("createCodexDynamicToolBridge", () => {
13171317
]);
13181318
});
13191319

1320-
it("marks delivered message-tool-only source replies as terminal when final is omitted", async () => {
1320+
it("does not mark delivered message-tool-only source replies as terminal when final is omitted", async () => {
13211321
const bridge = createBridgeWithToolResult(
13221322
"message",
13231323
textToolResult("Sent.", { messageId: "imessage-6264" }),
@@ -1330,15 +1330,15 @@ describe("createCodexDynamicToolBridge", () => {
13301330
});
13311331

13321332
expect(result).toEqual(expectInputText("Sent."));
1333-
expect(result.terminate).toBe(true);
1333+
expect(result.terminate).toBeUndefined();
13341334
expect(bridge.telemetry.didDeliverSourceReplyViaMessageTool).toBe(true);
13351335
expect(bridge.telemetry.messagingToolSentTargets.at(-1)).toMatchObject({
13361336
sourceReplyFinal: true,
13371337
});
13381338
expect(Object.keys(result)).not.toContain("terminate");
13391339
});
13401340

1341-
it("requires explicit final=false to keep a delivered message-tool-only source reply non-terminal", async () => {
1341+
it("requires explicit final=true to make a delivered message-tool-only source reply terminal", async () => {
13421342
const bridge = createBridgeWithToolResult(
13431343
"message",
13441344
textToolResult("Sent.", { messageId: "imessage-6264" }),
@@ -1373,7 +1373,7 @@ describe("createCodexDynamicToolBridge", () => {
13731373
expect(Object.keys(result)).not.toContain("terminate");
13741374
});
13751375

1376-
it("keeps message-tool-only source replies terminal when middleware redacts receipt details", async () => {
1376+
it("does not mark delivered message-tool-only source replies as terminal when middleware redacts receipt details", async () => {
13771377
const registry = createEmptyPluginRegistry();
13781378
registry.agentToolResultMiddlewares.push({
13791379
pluginId: "receipt-redactor",
@@ -1406,7 +1406,8 @@ describe("createCodexDynamicToolBridge", () => {
14061406
});
14071407

14081408
expect(result).toEqual(expectInputText("Sent."));
1409-
expect(result.terminate).toBe(true);
1409+
expect(result.terminate).toBeUndefined();
1410+
expect(bridge.telemetry.didDeliverSourceReplyViaMessageTool).toBe(true);
14101411
expect(Object.keys(result)).not.toContain("terminate");
14111412
});
14121413

@@ -1435,7 +1436,7 @@ describe("createCodexDynamicToolBridge", () => {
14351436
expect(bridge.telemetry.didDeliverSourceReplyViaMessageTool).toBe(false);
14361437
});
14371438

1438-
it("keeps message-tool-only source replies terminal for explicit current source routes", async () => {
1439+
it("does not mark delivered explicit current source routes as terminal without final=true", async () => {
14391440
const bridge = createBridgeWithToolResult(
14401441
"message",
14411442
textToolResult("Sent.", { ok: true, messageId: "imessage-853" }),
@@ -1457,15 +1458,15 @@ describe("createCodexDynamicToolBridge", () => {
14571458
});
14581459

14591460
expect(result).toEqual(expectInputText("Sent."));
1460-
expect(result.terminate).toBe(true);
1461+
expect(result.terminate).toBeUndefined();
14611462
expect(bridge.telemetry.didDeliverSourceReplyViaMessageTool).toBe(true);
14621463
expect(bridge.telemetry.messagingToolSentTargets.at(-1)).toMatchObject({
14631464
sourceReplyFinal: true,
14641465
});
14651466
expect(Object.keys(result)).not.toContain("terminate");
14661467
});
14671468

1468-
it("keeps normalized explicit source routes terminal", async () => {
1469+
it("does not mark normalized explicit source routes as terminal without final=true", async () => {
14691470
setActivePluginRegistry(
14701471
createTestRegistry([
14711472
{
@@ -1511,12 +1512,12 @@ describe("createCodexDynamicToolBridge", () => {
15111512
text: "visible reply",
15121513
}),
15131514
]);
1514-
expect(result.terminate).toBe(true);
1515+
expect(result.terminate).toBeUndefined();
15151516
expect(bridge.telemetry.didDeliverSourceReplyViaMessageTool).toBe(true);
15161517
expect(Object.keys(result)).not.toContain("terminate");
15171518
});
15181519

1519-
it("keeps message-tool-only source replies terminal when the reply receipt matches the current message id", async () => {
1520+
it("does not mark delivered message-tool-only source replies as terminal when the reply receipt matches the current message id", async () => {
15201521
const bridge = createBridgeWithToolResult(
15211522
"message",
15221523
textToolResult("Sent.", {
@@ -1550,12 +1551,12 @@ describe("createCodexDynamicToolBridge", () => {
15501551
text: "visible reply",
15511552
}),
15521553
]);
1553-
expect(result.terminate).toBe(true);
1554+
expect(result.terminate).toBeUndefined();
15541555
expect(bridge.telemetry.didDeliverSourceReplyViaMessageTool).toBe(true);
15551556
expect(Object.keys(result)).not.toContain("terminate");
15561557
});
15571558

1558-
it("keeps message-tool-only source replies terminal when a text receipt matches the current message id", async () => {
1559+
it("does not mark delivered message-tool-only source replies as terminal when a text receipt matches the current message id", async () => {
15591560
const receiptText = JSON.stringify({
15601561
ok: true,
15611562
messageId: "provider-message-1",
@@ -1578,7 +1579,7 @@ describe("createCodexDynamicToolBridge", () => {
15781579
});
15791580

15801581
expect(result).toEqual(expectInputText(receiptText));
1581-
expect(result.terminate).toBe(true);
1582+
expect(result.terminate).toBeUndefined();
15821583
expect(bridge.telemetry.didDeliverSourceReplyViaMessageTool).toBe(true);
15831584
expect(Object.keys(result)).not.toContain("terminate");
15841585
});
@@ -1641,7 +1642,7 @@ describe("createCodexDynamicToolBridge", () => {
16411642
expect(bridge.telemetry.didDeliverSourceReplyViaMessageTool).toBe(false);
16421643
});
16431644

1644-
it("keeps message-tool-only source replies terminal for explicit native target segments", async () => {
1645+
it("does not mark delivered explicit native target segments as terminal without final=true", async () => {
16451646
const bridge = createBridgeWithToolResult("message", textToolResult("Sent.", { ok: true }), {
16461647
sourceReplyDeliveryMode: "message_tool_only",
16471648
currentChannelProvider: "imessage",
@@ -1658,12 +1659,12 @@ describe("createCodexDynamicToolBridge", () => {
16581659
});
16591660

16601661
expect(result).toEqual(expectInputText("Sent."));
1661-
expect(result.terminate).toBe(true);
1662+
expect(result.terminate).toBeUndefined();
16621663
expect(bridge.telemetry.didDeliverSourceReplyViaMessageTool).toBe(true);
16631664
expect(Object.keys(result)).not.toContain("terminate");
16641665
});
16651666

1666-
it("keeps message-tool-only source replies terminal when the provider is only in the current channel id", async () => {
1667+
it("does not mark delivered source replies as terminal when the provider is only in the current channel id", async () => {
16671668
const bridge = createBridgeWithToolResult("message", textToolResult("Sent.", { ok: true }), {
16681669
sourceReplyDeliveryMode: "message_tool_only",
16691670
currentChannelId: "imessage:any;-;+12069106512",
@@ -1679,7 +1680,7 @@ describe("createCodexDynamicToolBridge", () => {
16791680
});
16801681

16811682
expect(result).toEqual(expectInputText("Sent."));
1682-
expect(result.terminate).toBe(true);
1683+
expect(result.terminate).toBeUndefined();
16831684
expect(bridge.telemetry.didDeliverSourceReplyViaMessageTool).toBe(true);
16841685
expect(Object.keys(result)).not.toContain("terminate");
16851686
});
@@ -1779,12 +1780,45 @@ describe("createCodexDynamicToolBridge", () => {
17791780
arguments: { action: "inspect" },
17801781
});
17811782

1782-
expect(firstResult.terminate).toBe(true);
1783-
expect(bridge.telemetry.didSendViaMessagingTool).toBe(true);
1783+
expect(firstResult.terminate).toBeUndefined();
1784+
expect(bridge.telemetry.didDeliverSourceReplyViaMessageTool).toBe(true);
17841785
expect(secondResult).toEqual(expectInputText("No message sent."));
17851786
expect(secondResult.terminate).toBeUndefined();
17861787
});
17871788

1789+
it("continues after an intermediate message-tool-only source reply without final", async () => {
1790+
const execute = vi
1791+
.fn()
1792+
.mockResolvedValueOnce(
1793+
textToolResult("One moment.", { messageId: "source-reply-intermediate" }),
1794+
)
1795+
.mockResolvedValueOnce(textToolResult("Command output.", { exitCode: 0 }));
1796+
const bridge = createCodexDynamicToolBridge({
1797+
tools: [createTool({ name: "message", execute }), createTool({ name: "exec", execute })],
1798+
signal: new AbortController().signal,
1799+
hookContext: { sourceReplyDeliveryMode: "message_tool_only" },
1800+
});
1801+
1802+
const firstResult = await handleMessageToolCall(bridge, {
1803+
action: "send",
1804+
message: "One moment.",
1805+
});
1806+
expect(firstResult).toEqual(expectInputText("One moment."));
1807+
expect(firstResult.terminate).toBeUndefined();
1808+
expect(bridge.telemetry.didDeliverSourceReplyViaMessageTool).toBe(true);
1809+
1810+
const secondResult = await bridge.handleToolCall({
1811+
threadId: "thread-1",
1812+
turnId: "turn-1",
1813+
callId: "call-2",
1814+
namespace: null,
1815+
tool: "exec",
1816+
arguments: { command: "ls" },
1817+
});
1818+
expect(secondResult.success).toBe(true);
1819+
expect(secondResult.terminate).toBeUndefined();
1820+
});
1821+
17881822
it("does not mark explicit message-tool sends as terminal source replies", async () => {
17891823
const bridge = createBridgeWithToolResult(
17901824
"message",

extensions/codex/src/app-server/dynamic-tools.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -686,17 +686,13 @@ export function createCodexDynamicToolBridge(params: {
686686
toolName === "message" &&
687687
!resultIsError &&
688688
(rawResult.terminate === true || result.terminate === true);
689-
const hasExplicitFinalControl = typeof executedArgs.final === "boolean";
690-
// Omitted final on a confirmed source reply must degrade to legacy
691-
// terminate-on-delivery (completed marker), never progress; otherwise
692-
// stranded-reply recovery re-delivers a duplicate of that reply.
693689
const sourceReplyFinal =
694690
params.hookContext?.sourceReplyDeliveryMode === "message_tool_only" &&
695691
toolName === "message" &&
696692
(toolConfirmedSourceReply || deliveredSourceReply || receiptConfirmedSourceReply)
697-
? hasExplicitFinalControl
698-
? executedArgs.final === true
699-
: true
693+
? typeof executedArgs.final !== "boolean"
694+
? true
695+
: executedArgs.final
700696
: undefined;
701697
collectToolTelemetry({
702698
toolName,
@@ -721,7 +717,10 @@ export function createCodexDynamicToolBridge(params: {
721717
)) ||
722718
isToolResultYield(rawResult) ||
723719
isToolResultYield(result) ||
724-
((deliveredSourceReply || receiptConfirmedSourceReply) && executedArgs.final !== false),
720+
(params.hookContext?.sourceReplyDeliveryMode === "message_tool_only" &&
721+
toolName === "message" &&
722+
executedArgs.final === true &&
723+
(deliveredSourceReply || receiptConfirmedSourceReply)),
725724
);
726725
const asyncStarted =
727726
isAsyncStartedToolResult(rawResult) || isAsyncStartedToolResult(result);

0 commit comments

Comments
 (0)