@@ -31,6 +31,7 @@ import {
3131 type CodexDynamicToolSpec ,
3232 type JsonValue ,
3333} from "./protocol.js" ;
34+ import { settleCodexSourceReplyFinality } from "./source-reply-finality.js" ;
3435
3536const CODEX_OPENCLAW_DYNAMIC_TOOL_NAMESPACE = "openclaw" ;
3637
@@ -1388,7 +1389,7 @@ describe("createCodexDynamicToolBridge", () => {
13881389 ] ) ;
13891390 } ) ;
13901391
1391- it ( "marks delivered message-tool-only source replies as terminal when final is omitted " , async ( ) => {
1392+ it ( "keeps omitted source-reply finality non- terminal until a successful attempt settles " , async ( ) => {
13921393 const bridge = createBridgeWithToolResult (
13931394 "message" ,
13941395 textToolResult ( "Sent." , { messageId : "imessage-6264" } ) ,
@@ -1401,15 +1402,97 @@ describe("createCodexDynamicToolBridge", () => {
14011402 } ) ;
14021403
14031404 expect ( result ) . toEqual ( expectInputText ( "Sent." ) ) ;
1404- expect ( result . terminate ) . toBe ( true ) ;
1405+ expect ( result . terminate ) . toBeUndefined ( ) ;
14051406 expect ( bridge . telemetry . didDeliverSourceReplyViaMessageTool ) . toBe ( true ) ;
1407+ expect ( bridge . telemetry . messagingToolSentTargets . at ( - 1 ) ) . not . toHaveProperty ( "sourceReplyFinal" ) ;
1408+
1409+ expect ( settleCodexSourceReplyFinality ( bridge . telemetry , true ) ) . toBe ( true ) ;
1410+
14061411 expect ( bridge . telemetry . messagingToolSentTargets . at ( - 1 ) ) . toMatchObject ( {
14071412 sourceReplyFinal : true ,
14081413 } ) ;
14091414 expect ( Object . keys ( result ) ) . not . toContain ( "terminate" ) ;
14101415 } ) ;
14111416
1412- it ( "requires explicit final=false to keep a delivered message-tool-only source reply non-terminal" , async ( ) => {
1417+ it ( "settles omitted source-reply finality as progress when the attempt fails" , async ( ) => {
1418+ const bridge = createBridgeWithToolResult (
1419+ "message" ,
1420+ {
1421+ ...textToolResult ( "Sent." , { messageId : "imessage-6264" } ) ,
1422+ terminate : true ,
1423+ } ,
1424+ { sourceReplyDeliveryMode : "message_tool_only" } ,
1425+ ) ;
1426+
1427+ const result = await handleMessageToolCall ( bridge , {
1428+ action : "send" ,
1429+ message : "visible reply" ,
1430+ } ) ;
1431+ expect ( result . terminate ) . toBeUndefined ( ) ;
1432+ expect ( settleCodexSourceReplyFinality ( bridge . telemetry , false ) ) . toBe ( false ) ;
1433+
1434+ expect ( bridge . telemetry . messagingToolSentTargets . at ( - 1 ) ) . toMatchObject ( {
1435+ sourceReplyFinal : false ,
1436+ } ) ;
1437+ } ) ;
1438+
1439+ it ( "settles only the latest omitted source reply as final after success" , async ( ) => {
1440+ const bridge = createBridgeWithToolResult (
1441+ "message" ,
1442+ textToolResult ( "Sent." , { messageId : "imessage-6264" } ) ,
1443+ { sourceReplyDeliveryMode : "message_tool_only" } ,
1444+ ) ;
1445+
1446+ await handleMessageToolCall ( bridge , { action : "send" , message : "first update" } ) ;
1447+ await handleMessageToolCall ( bridge , { action : "send" , message : "second update" } ) ;
1448+ settleCodexSourceReplyFinality ( bridge . telemetry , true ) ;
1449+
1450+ expect (
1451+ bridge . telemetry . messagingToolSentTargets . map ( ( target ) => target . sourceReplyFinal ) ,
1452+ ) . toEqual ( [ false , true ] ) ;
1453+ } ) ;
1454+
1455+ it ( "does not promote an omitted reply past a later explicit progress reply" , async ( ) => {
1456+ const bridge = createBridgeWithToolResult (
1457+ "message" ,
1458+ textToolResult ( "Sent." , { messageId : "imessage-6264" } ) ,
1459+ { sourceReplyDeliveryMode : "message_tool_only" } ,
1460+ ) ;
1461+
1462+ await handleMessageToolCall ( bridge , { action : "send" , message : "first update" } ) ;
1463+ await handleMessageToolCall ( bridge , {
1464+ action : "send" ,
1465+ message : "still working" ,
1466+ final : false ,
1467+ } ) ;
1468+ settleCodexSourceReplyFinality ( bridge . telemetry , true ) ;
1469+
1470+ expect (
1471+ bridge . telemetry . messagingToolSentTargets . map ( ( target ) => target . sourceReplyFinal ) ,
1472+ ) . toEqual ( [ false , false ] ) ;
1473+ } ) ;
1474+
1475+ it ( "keeps a later explicit final reply authoritative over an omitted reply" , async ( ) => {
1476+ const bridge = createBridgeWithToolResult (
1477+ "message" ,
1478+ textToolResult ( "Sent." , { messageId : "imessage-6264" } ) ,
1479+ { sourceReplyDeliveryMode : "message_tool_only" } ,
1480+ ) ;
1481+
1482+ await handleMessageToolCall ( bridge , { action : "send" , message : "first update" } ) ;
1483+ await handleMessageToolCall ( bridge , {
1484+ action : "send" ,
1485+ message : "finished" ,
1486+ final : true ,
1487+ } ) ;
1488+ settleCodexSourceReplyFinality ( bridge . telemetry , true ) ;
1489+
1490+ expect (
1491+ bridge . telemetry . messagingToolSentTargets . map ( ( target ) => target . sourceReplyFinal ) ,
1492+ ) . toEqual ( [ false , true ] ) ;
1493+ } ) ;
1494+
1495+ it ( "honors explicit finality for delivered message-tool-only source replies" , async ( ) => {
14131496 const bridge = createBridgeWithToolResult (
14141497 "message" ,
14151498 textToolResult ( "Sent." , { messageId : "imessage-6264" } ) ,
@@ -1474,6 +1557,7 @@ describe("createCodexDynamicToolBridge", () => {
14741557 const result = await handleMessageToolCall ( bridge , {
14751558 action : "send" ,
14761559 message : "visible reply" ,
1560+ final : true ,
14771561 } ) ;
14781562
14791563 expect ( result ) . toEqual ( expectInputText ( "Sent." ) ) ;
@@ -1525,6 +1609,7 @@ describe("createCodexDynamicToolBridge", () => {
15251609 messageId : "853" ,
15261610 message : "visible reply" ,
15271611 buttons : [ ] ,
1612+ final : true ,
15281613 } ) ;
15291614
15301615 expect ( result ) . toEqual ( expectInputText ( "Sent." ) ) ;
@@ -1571,6 +1656,7 @@ describe("createCodexDynamicToolBridge", () => {
15711656 target : "+1 (206) 910-6512" ,
15721657 messageId : "853" ,
15731658 message : "visible reply" ,
1659+ final : true ,
15741660 } ) ;
15751661
15761662 expect ( result ) . toEqual ( expectInputText ( "Sent." ) ) ;
@@ -1610,6 +1696,7 @@ describe("createCodexDynamicToolBridge", () => {
16101696 messageId : "857" ,
16111697 message : "visible reply" ,
16121698 buttons : [ ] ,
1699+ final : true ,
16131700 } ) ;
16141701
16151702 expect ( result ) . toEqual ( expectInputText ( "Sent." ) ) ;
@@ -1646,6 +1733,7 @@ describe("createCodexDynamicToolBridge", () => {
16461733 messageId : "861" ,
16471734 message : "visible reply" ,
16481735 buttons : [ ] ,
1736+ final : true ,
16491737 } ) ;
16501738
16511739 expect ( result ) . toEqual ( expectInputText ( receiptText ) ) ;
@@ -1726,6 +1814,7 @@ describe("createCodexDynamicToolBridge", () => {
17261814 messageId : "863" ,
17271815 message : "visible reply" ,
17281816 buttons : [ ] ,
1817+ final : true ,
17291818 } ) ;
17301819
17311820 expect ( result ) . toEqual ( expectInputText ( "Sent." ) ) ;
@@ -1747,6 +1836,7 @@ describe("createCodexDynamicToolBridge", () => {
17471836 messageId : "865" ,
17481837 message : "visible reply" ,
17491838 buttons : [ ] ,
1839+ final : true ,
17501840 } ) ;
17511841
17521842 expect ( result ) . toEqual ( expectInputText ( "Sent." ) ) ;
@@ -1755,7 +1845,7 @@ describe("createCodexDynamicToolBridge", () => {
17551845 expect ( Object . keys ( result ) ) . not . toContain ( "terminate" ) ;
17561846 } ) ;
17571847
1758- it ( "records message-tool-owned terminal replies as delivered source replies " , async ( ) => {
1848+ it ( "defers omitted finality even when the message tool returns legacy termination " , async ( ) => {
17591849 const bridge = createBridgeWithToolResult (
17601850 "message" ,
17611851 {
@@ -1775,8 +1865,12 @@ describe("createCodexDynamicToolBridge", () => {
17751865 } ) ;
17761866
17771867 expect ( result ) . toEqual ( expectInputText ( "Sent." ) ) ;
1778- expect ( result . terminate ) . toBe ( true ) ;
1868+ expect ( result . terminate ) . toBeUndefined ( ) ;
17791869 expect ( bridge . telemetry . didDeliverSourceReplyViaMessageTool ) . toBe ( true ) ;
1870+ expect ( bridge . telemetry . messagingToolSentTargets . at ( - 1 ) ) . not . toHaveProperty ( "sourceReplyFinal" ) ;
1871+
1872+ settleCodexSourceReplyFinality ( bridge . telemetry , true ) ;
1873+
17801874 expect ( bridge . telemetry . messagingToolSentTargets . at ( - 1 ) ) . toMatchObject ( {
17811875 sourceReplyFinal : true ,
17821876 } ) ;
@@ -1850,7 +1944,7 @@ describe("createCodexDynamicToolBridge", () => {
18501944 arguments : { action : "inspect" } ,
18511945 } ) ;
18521946
1853- expect ( firstResult . terminate ) . toBe ( true ) ;
1947+ expect ( firstResult . terminate ) . toBeUndefined ( ) ;
18541948 expect ( bridge . telemetry . didSendViaMessagingTool ) . toBe ( true ) ;
18551949 expect ( secondResult ) . toEqual ( expectInputText ( "No message sent." ) ) ;
18561950 expect ( secondResult . terminate ) . toBeUndefined ( ) ;
0 commit comments