@@ -120,8 +120,8 @@ import {
120120 type FollowupRun ,
121121 type QueueSettings ,
122122} from "./queue.js" ;
123- import { createReplyMediaContext } from "./reply-media-paths.js" ;
124123import { normalizeReplyPayloadDirectives } from "./reply-delivery.js" ;
124+ import { createReplyMediaContext } from "./reply-media-paths.js" ;
125125import { resolveReplyOperationRunState } from "./reply-operation-run-state.js" ;
126126import {
127127 replyRunRegistry ,
@@ -136,7 +136,7 @@ import { incrementRunCompactionCount, persistRunSessionUsage } from "./session-r
136136import { resolveSourceReplyVisibilityPolicy } from "./source-reply-delivery-mode.js" ;
137137import {
138138 buildStrandedReplyDeliveryFailurePayload ,
139- STRANDED_REPLY_RETRY_MARKER ,
139+ buildStrandedReplyRetryFollowupRun ,
140140} from "./stranded-reply-recovery.js" ;
141141import { createTypingSignaler } from "./typing-mode.js" ;
142142import type { TypingController } from "./typing.js" ;
@@ -2051,10 +2051,7 @@ export async function runReplyAgent(params: {
20512051 }
20522052 return buildStrandedReplyDeliveryFailurePayload ( ) ;
20532053 } ;
2054- if (
2055- opts ?. sourceReplyDeliveryMode === "message_tool_only" &&
2056- committedSourceReplyDelivery
2057- ) {
2054+ if ( opts ?. sourceReplyDeliveryMode === "message_tool_only" && committedSourceReplyDelivery ) {
20582055 await opts . onObservedReplyDelivery ?.( ) ;
20592056 }
20602057 const currentMessageId = sessionCtx . MessageSidFull ?? sessionCtx . MessageSid ;
@@ -2634,7 +2631,10 @@ export async function runReplyAgent(params: {
26342631 : ( rawAssistantText ?? "" ) ,
26352632 ) ;
26362633 const isRoomEvent = sessionCtx . InboundEventKind === "room_event" ;
2634+ // Heartbeats already deliver fallback finals via sendDurableMessageBatch;
2635+ // recovering here would duplicate that message.
26372636 const isStrandedReply =
2637+ ! isHeartbeat &&
26382638 ! isRoomEvent &&
26392639 shouldWarnAboutPrivateMessageToolFinal ( {
26402640 sourceReplyDeliveryMode : sourceReplyPolicy . sourceReplyDeliveryMode ,
@@ -2644,6 +2644,7 @@ export async function runReplyAgent(params: {
26442644 } ) ;
26452645 const retryMissingSourceDelivery =
26462646 isStrandedReplyRetryRun &&
2647+ ! isHeartbeat &&
26472648 ! isRoomEvent &&
26482649 sourceReplyPolicy . sourceReplyDeliveryMode === "message_tool_only" &&
26492650 ! sourceReplyPolicy . sendPolicyDenied &&
@@ -2663,30 +2664,12 @@ export async function runReplyAgent(params: {
26632664 if ( isStrandedReplyRetryRun ) {
26642665 finalPayloads = [ ...finalPayloads , buildStrandedReplyDeliveryFailurePayload ( ) ] ;
26652666 } else {
2666- const retryDeliveryText = assistantFinalText ;
2667- const retryPrompt =
2668- `[System] Your previous reply was not delivered to the conversation because ` +
2669- `you did not call message(action=send). Your reply text was:\n\n` +
2670- `"${ retryDeliveryText } "\n\n` +
2671- `Please deliver this reply now by calling message(action=send). ` +
2672- `Do not add any extra commentary; just deliver the original reply.` ;
26732667 const retryEnqueued = enqueueFollowupRun (
26742668 queueKey ,
2675- {
2676- ...followupRun ,
2677- prompt : retryPrompt ,
2678- summaryLine : STRANDED_REPLY_RETRY_MARKER ,
2679- strandedReplyRetry : true ,
2680- disableCollectBatching : true ,
2681- transcriptPrompt : undefined ,
2682- userTurnTranscriptRecorder : undefined ,
2683- currentInboundContext : undefined ,
2684- run : {
2685- ...followupRun . run ,
2686- sourceReplyDeliveryMode : sourceReplyPolicy . sourceReplyDeliveryMode ,
2687- suppressNextUserMessagePersistence : true ,
2688- } ,
2689- } ,
2669+ buildStrandedReplyRetryFollowupRun ( followupRun , {
2670+ finalText : assistantFinalText ,
2671+ sourceReplyDeliveryMode : sourceReplyPolicy . sourceReplyDeliveryMode ,
2672+ } ) ,
26902673 resolvedQueue ,
26912674 "none" ,
26922675 runFollowupTurn ,
0 commit comments