@@ -26,6 +26,7 @@ import { formatReasoningMessage } from "../agents/embedded-agent-utils.js";
2626import { resolveAgentHarnessPolicy } from "../agents/harness/policy.js" ;
2727import { resolveModelRefFromString , type ModelRef } from "../agents/model-selection.js" ;
2828import { resolvePersistedSessionRuntimeId } from "../agents/session-runtime-compat.js" ;
29+ import { STREAM_ERROR_FALLBACK_TEXT } from "../agents/stream-message-shared.js" ;
2930import { DEFAULT_HEARTBEAT_FILENAME } from "../agents/workspace.js" ;
3031import { resolveHeartbeatReplyPayload } from "../auto-reply/heartbeat-reply-payload.js" ;
3132import {
@@ -839,41 +840,67 @@ function stripLeadingHeartbeatResponsePrefix(
839840 return text . replace ( prefixPattern , "" ) ;
840841}
841842
843+ type NormalizedHeartbeatDelivery = {
844+ shouldSkip : boolean ;
845+ text : string ;
846+ hasMedia : boolean ;
847+ isInternalPlaceholderOnly : boolean ;
848+ } ;
849+
850+ function isStreamErrorFallbackPlaceholderOnly ( text : string ) : boolean {
851+ let remaining = text . trim ( ) ;
852+ if ( ! remaining ) {
853+ return false ;
854+ }
855+
856+ while ( remaining . startsWith ( STREAM_ERROR_FALLBACK_TEXT ) ) {
857+ remaining = remaining . slice ( STREAM_ERROR_FALLBACK_TEXT . length ) . trimStart ( ) ;
858+ }
859+ return remaining . length === 0 ;
860+ }
861+
842862function normalizeHeartbeatReply (
843863 payload : ReplyPayload ,
844864 responsePrefix : string | undefined ,
845865 ackMaxChars : number ,
846- ) {
866+ ) : NormalizedHeartbeatDelivery {
847867 const rawText = typeof payload . text === "string" ? payload . text : "" ;
848868 const textForStrip = stripLeadingHeartbeatResponsePrefix ( rawText , responsePrefix ) ;
849869 const stripped = stripHeartbeatToken ( textForStrip , {
850870 mode : "heartbeat" ,
851871 maxAckChars : ackMaxChars ,
852872 } ) ;
853873 const hasMedia = resolveSendableOutboundReplyParts ( payload ) . hasMedia ;
854- if ( stripped . shouldSkip && ! hasMedia ) {
874+ const isInternalPlaceholderOnly = isStreamErrorFallbackPlaceholderOnly ( stripped . text ) ;
875+ if ( ( stripped . shouldSkip || isInternalPlaceholderOnly ) && ! hasMedia ) {
855876 return {
856877 shouldSkip : true ,
857878 text : "" ,
858879 hasMedia,
880+ isInternalPlaceholderOnly,
859881 } ;
860882 }
861- let finalText = stripped . text ;
883+ let finalText = isInternalPlaceholderOnly ? "" : stripped . text ;
862884 if ( responsePrefix && finalText && ! finalText . startsWith ( responsePrefix ) ) {
863885 finalText = `${ responsePrefix } ${ finalText } ` ;
864886 }
865- return { shouldSkip : false , text : finalText , hasMedia } ;
887+ return { shouldSkip : false , text : finalText , hasMedia, isInternalPlaceholderOnly } ;
866888}
867889
868890function normalizeHeartbeatToolNotification (
869891 response : HeartbeatToolResponse ,
870892 responsePrefix : string | undefined ,
871- ) {
893+ ) : NormalizedHeartbeatDelivery {
872894 let finalText = getHeartbeatToolNotificationText ( response ) ;
873895 if ( responsePrefix && finalText && ! finalText . startsWith ( responsePrefix ) ) {
874896 finalText = `${ responsePrefix } ${ finalText } ` ;
875897 }
876- return { shouldSkip : false , text : finalText , hasMedia : false } ;
898+ return {
899+ shouldSkip : false ,
900+ text : finalText ,
901+ hasMedia : false ,
902+ isInternalPlaceholderOnly : false ,
903+ } ;
877904}
878905
879906type HeartbeatWakePayloadFlags = {
@@ -1927,7 +1954,12 @@ export async function runHeartbeatOnce(opts: {
19271954 ? normalizeHeartbeatToolNotification ( heartbeatToolResponse , responsePrefix )
19281955 : replyPayload
19291956 ? normalizeHeartbeatReply ( replyPayload , responsePrefix , ackMaxChars )
1930- : { shouldSkip : true , text : "" , hasMedia : false } ;
1957+ : {
1958+ shouldSkip : true ,
1959+ text : "" ,
1960+ hasMedia : false ,
1961+ isInternalPlaceholderOnly : false ,
1962+ } ;
19311963 // For exec completion events, don't skip even if the response looks like HEARTBEAT_OK.
19321964 // The model should be responding with exec results, not ack tokens.
19331965 // Also, if normalized.text is empty due to token stripping but we have exec completion,
@@ -1936,6 +1968,7 @@ export async function runHeartbeatOnce(opts: {
19361968 ! heartbeatToolResponse &&
19371969 hasRelayableExecCompletion &&
19381970 ! normalized . text . trim ( ) &&
1971+ ! normalized . isInternalPlaceholderOnly &&
19391972 replyPayload ?. text ?. trim ( )
19401973 ? replyPayload . text . trim ( )
19411974 : null ;
@@ -1952,7 +1985,9 @@ export async function runHeartbeatOnce(opts: {
19521985 normalized . shouldSkip = false ;
19531986 }
19541987 const shouldSkipMain =
1955- normalized . shouldSkip && ! normalized . hasMedia && ! hasRelayableExecCompletion ;
1988+ normalized . shouldSkip &&
1989+ ! normalized . hasMedia &&
1990+ ( ! hasRelayableExecCompletion || normalized . isInternalPlaceholderOnly ) ;
19561991 if ( shouldSkipMain && reasoningPayloads . length === 0 ) {
19571992 await restoreHeartbeatUpdatedAt ( {
19581993 storePath,
0 commit comments