11import fs from "node:fs/promises" ;
22import {
3+ resolveAgentConfig ,
34 resolveAgentDir ,
45 resolveAgentWorkspaceDir ,
56 resolveSessionAgentId ,
@@ -19,7 +20,7 @@ import { createLazyImportLoader } from "../../shared/lazy-promise.js";
1920import { normalizeOptionalString } from "../../shared/string-coerce.js" ;
2021import { normalizeStringEntries } from "../../shared/string-normalization.js" ;
2122import type { GetReplyOptions } from "../get-reply-options.types.js" ;
22- import { stripHeartbeatToken } from "../heartbeat.js" ;
23+ import { DEFAULT_HEARTBEAT_ACK_MAX_CHARS , stripHeartbeatToken } from "../heartbeat.js" ;
2324import type { ReplyPayload } from "../reply-payload.js" ;
2425import type { MsgContext } from "../templating.js" ;
2526import { normalizeVerboseLevel } from "../thinking.js" ;
@@ -52,8 +53,25 @@ import { createTypingController } from "./typing.js";
5253
5354type ResetCommandAction = "new" | "reset" ;
5455
55- function isHeartbeatPendingFinalDeliveryEffectivelyEmpty ( text : string ) : boolean {
56- return stripHeartbeatToken ( text , { mode : "heartbeat" } ) . shouldSkip ;
56+ function classifyHeartbeatPendingFinalDelivery ( text : string , ackMaxChars : number ) {
57+ const stripped = stripHeartbeatToken ( text , {
58+ mode : "heartbeat" ,
59+ maxAckChars : ackMaxChars ,
60+ } ) ;
61+ return {
62+ shouldClear : stripped . shouldSkip ,
63+ replayText : stripped . didStrip && stripped . text ? stripped . text : text ,
64+ } ;
65+ }
66+
67+ function resolveHeartbeatAckMaxChars ( cfg : OpenClawConfig , agentId : string ) : number {
68+ const agentHeartbeat = resolveAgentConfig ( cfg , agentId ) ?. heartbeat ;
69+ return Math . max (
70+ 0 ,
71+ agentHeartbeat ?. ackMaxChars ??
72+ cfg . agents ?. defaults ?. heartbeat ?. ackMaxChars ??
73+ DEFAULT_HEARTBEAT_ACK_MAX_CHARS ,
74+ ) ;
5775}
5876
5977const sessionResetModelRuntimeLoader = createLazyImportLoader (
@@ -376,7 +394,11 @@ export async function getReplyFromConfig(
376394 // If it's a user message, we deliver the lost reply first, then continue.
377395 // For now, let's just return the lost reply if it's a heartbeat.
378396 if ( opts ?. isHeartbeat ) {
379- if ( isHeartbeatPendingFinalDeliveryEffectivelyEmpty ( text ) ) {
397+ const heartbeatPending = classifyHeartbeatPendingFinalDelivery (
398+ text ,
399+ resolveHeartbeatAckMaxChars ( cfg , agentId ) ,
400+ ) ;
401+ if ( heartbeatPending . shouldClear ) {
380402 sessionEntry . pendingFinalDelivery = undefined ;
381403 sessionEntry . pendingFinalDeliveryText = undefined ;
382404 sessionEntry . pendingFinalDeliveryCreatedAt = undefined ;
@@ -409,6 +431,7 @@ export async function getReplyFromConfig(
409431 sessionEntry . pendingFinalDeliveryLastAttemptAt = updatedAt ;
410432 sessionEntry . pendingFinalDeliveryAttemptCount = attemptCount ;
411433 sessionEntry . pendingFinalDeliveryLastError = null ;
434+ sessionEntry . pendingFinalDeliveryText = heartbeatPending . replayText ;
412435 sessionEntry . updatedAt = updatedAt ;
413436 if ( sessionKey && sessionStore ) {
414437 sessionStore [ sessionKey ] = sessionEntry ;
@@ -419,14 +442,15 @@ export async function getReplyFromConfig(
419442 storePath,
420443 sessionKey,
421444 update : async ( ) => ( {
445+ pendingFinalDeliveryText : heartbeatPending . replayText ,
422446 pendingFinalDeliveryLastAttemptAt : updatedAt ,
423447 pendingFinalDeliveryAttemptCount : attemptCount ,
424448 pendingFinalDeliveryLastError : null ,
425449 updatedAt,
426450 } ) ,
427451 } ) ;
428452 }
429- return { text } ;
453+ return { text : heartbeatPending . replayText } ;
430454 }
431455 }
432456 }
0 commit comments