Skip to content

Commit e1d6f32

Browse files
committed
fix(heartbeat): stop pending final replay
1 parent b2a620d commit e1d6f32

5 files changed

Lines changed: 10 additions & 92 deletions

File tree

src/auto-reply/get-reply-options.types.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,6 @@ export type GetReplyOptions = {
6464
onTypingCleanup?: () => void;
6565
onTypingController?: (typing: TypingController) => void;
6666
isHeartbeat?: boolean;
67-
/** If true, heartbeat turns do not replay durable pending final text directly. */
68-
suppressPendingFinalDeliveryReplay?: boolean;
6967
/** Policy-level typing control for run classes (user/system/internal/heartbeat). */
7068
typingPolicy?: TypingPolicy;
7169
/** Force-disable typing indicators for this run (system/internal/cross-channel routes). */

src/auto-reply/reply/get-reply.fast-path.test.ts

Lines changed: 10 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,6 @@ import fs from "node:fs/promises";
22
import os from "node:os";
33
import path from "node:path";
44
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
5-
import {
6-
INTERNAL_RUNTIME_CONTEXT_BEGIN,
7-
INTERNAL_RUNTIME_CONTEXT_END,
8-
} from "../../agents/internal-runtime-context.js";
95
import type { OpenClawConfig } from "../../config/config.js";
106
import {
117
buildFastReplyCommandContext,
@@ -203,7 +199,7 @@ describe("getReplyFromConfig fast test bootstrap", () => {
203199
expect(vi.mocked(runPreparedReplyMock)).toHaveBeenCalledOnce();
204200
});
205201

206-
it("clears stale ack-only heartbeat pending delivery before replay", async () => {
202+
it("clears stale ack-only heartbeat pending delivery before running heartbeat", async () => {
207203
const home = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-heartbeat-pending-clear-"));
208204
const storePath = path.join(home, "sessions.json");
209205
const sessionKey = "agent:main:telegram:123";
@@ -243,7 +239,7 @@ describe("getReplyFromConfig fast test bootstrap", () => {
243239
expect(stored.pendingFinalDeliveryAttemptCount).toBeUndefined();
244240
});
245241

246-
it("uses ackMaxChars when replaying stale heartbeat pending delivery", async () => {
242+
it("keeps non-ack heartbeat pending delivery without direct replay", async () => {
247243
const home = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-heartbeat-pending-replay-"));
248244
const storePath = path.join(home, "sessions.json");
249245
const sessionKey = "agent:main:telegram:123";
@@ -272,57 +268,15 @@ describe("getReplyFromConfig fast test bootstrap", () => {
272268

273269
await expect(
274270
getReplyFromConfig(buildGetReplyCtx(), { isHeartbeat: true }, cfg),
275-
).resolves.toEqual({ text: "short" });
271+
).resolves.toEqual({ text: "ok" });
276272

277273
const stored = JSON.parse(await fs.readFile(storePath, "utf8"))[sessionKey];
278274
expect(stored.pendingFinalDelivery).toBe(true);
279-
expect(stored.pendingFinalDeliveryText).toBe("short");
280-
expect(stored.pendingFinalDeliveryAttemptCount).toBe(1);
281-
});
282-
283-
it("sanitizes stale heartbeat pending delivery before replay", async () => {
284-
const home = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-heartbeat-pending-sanitize-"));
285-
const storePath = path.join(home, "sessions.json");
286-
const sessionKey = "agent:main:telegram:123";
287-
await fs.writeFile(
288-
storePath,
289-
JSON.stringify({
290-
[sessionKey]: {
291-
sessionId: "pending-dirty-remainder",
292-
updatedAt: Date.now(),
293-
pendingFinalDelivery: true,
294-
pendingFinalDeliveryText: [
295-
"HEARTBEAT_OK",
296-
INTERNAL_RUNTIME_CONTEXT_BEGIN,
297-
"internal recovery detail",
298-
INTERNAL_RUNTIME_CONTEXT_END,
299-
"notify the user",
300-
].join("\n"),
301-
},
302-
}),
303-
"utf8",
304-
);
305-
const cfg = withFastReplyConfig({
306-
agents: {
307-
defaults: {
308-
model: "openai/gpt-5.5",
309-
workspace: home,
310-
heartbeat: { ackMaxChars: 0 },
311-
},
312-
},
313-
session: { store: storePath },
314-
} as OpenClawConfig);
315-
316-
await expect(
317-
getReplyFromConfig(buildGetReplyCtx(), { isHeartbeat: true }, cfg),
318-
).resolves.toEqual({ text: "notify the user" });
319-
320-
const stored = JSON.parse(await fs.readFile(storePath, "utf8"))[sessionKey];
321-
expect(stored.pendingFinalDeliveryText).toBe("notify the user");
322-
expect(stored.pendingFinalDeliveryAttemptCount).toBe(1);
275+
expect(stored.pendingFinalDeliveryText).toBe("HEARTBEAT_OK short");
276+
expect(stored.pendingFinalDeliveryAttemptCount).toBeUndefined();
323277
});
324278

325-
it("does not replay stale heartbeat pending delivery when suppression is requested", async () => {
279+
it("does not replay stale heartbeat pending delivery", async () => {
326280
const home = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-heartbeat-pending-suppress-"));
327281
const storePath = path.join(home, "sessions.json");
328282
const sessionKey = "agent:main:telegram:123";
@@ -351,12 +305,10 @@ describe("getReplyFromConfig fast test bootstrap", () => {
351305
} as OpenClawConfig);
352306

353307
await expect(
354-
getReplyFromConfig(
355-
buildGetReplyCtx(),
356-
{ isHeartbeat: true, suppressPendingFinalDeliveryReplay: true },
357-
cfg,
358-
),
359-
).resolves.toEqual({ text: "ok" });
308+
getReplyFromConfig(buildGetReplyCtx(), { isHeartbeat: true }, cfg),
309+
).resolves.toEqual({
310+
text: "ok",
311+
});
360312

361313
const stored = JSON.parse(await fs.readFile(storePath, "utf8"))[sessionKey];
362314
expect(stored.pendingFinalDelivery).toBe(true);

src/auto-reply/reply/get-reply.ts

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -532,36 +532,6 @@ export async function getReplyFromConfig(
532532
},
533533
});
534534
}
535-
} else if (opts.suppressPendingFinalDeliveryReplay !== true) {
536-
const updatedAt = Date.now();
537-
const attemptCount = (sessionEntry.pendingFinalDeliveryAttemptCount ?? 0) + 1;
538-
sessionEntry.pendingFinalDeliveryLastAttemptAt = updatedAt;
539-
sessionEntry.pendingFinalDeliveryAttemptCount = attemptCount;
540-
sessionEntry.pendingFinalDeliveryLastError = null;
541-
const replayText = sanitizePendingFinalDeliveryText(heartbeatPending.replayText);
542-
sessionEntry.pendingFinalDeliveryText = replayText;
543-
sessionEntry.updatedAt = updatedAt;
544-
if (sessionKey && sessionStore) {
545-
sessionStore[sessionKey] = sessionEntry;
546-
}
547-
if (sessionKey && storePath) {
548-
const { applySessionStoreEntryPatch } = await import("../../config/sessions.js");
549-
await applySessionStoreEntryPatch({
550-
storePath,
551-
sessionKey,
552-
skipMaintenance: true,
553-
takeCacheOwnership: true,
554-
patch: {
555-
pendingFinalDeliveryText: replayText,
556-
pendingFinalDeliveryLastAttemptAt: updatedAt,
557-
pendingFinalDeliveryAttemptCount: attemptCount,
558-
pendingFinalDeliveryLastError: null,
559-
updatedAt,
560-
},
561-
});
562-
}
563-
logResolverTiming("completed", "pending_final_delivery_replay");
564-
return { text: replayText };
565535
}
566536
}
567537
}

src/infra/heartbeat-runner.skips-busy-session-lane.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -484,7 +484,6 @@ describe("heartbeat runner skips when target session lane is busy", () => {
484484
expect(replySpy).toHaveBeenCalledOnce();
485485
expect(replySpy.mock.calls[0]?.[1]).toMatchObject({
486486
isHeartbeat: true,
487-
suppressPendingFinalDeliveryReplay: true,
488487
});
489488
expect(sendTelegram).not.toHaveBeenCalled();
490489
});

src/infra/heartbeat-runner.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1758,7 +1758,6 @@ export async function runHeartbeatOnce(opts: {
17581758
heartbeat?.lightContext === true ? "lightweight" : undefined;
17591759
const replyOpts = {
17601760
isHeartbeat: true,
1761-
suppressPendingFinalDeliveryReplay: true,
17621761
...(heartbeatModelOverride ? { heartbeatModelOverride } : {}),
17631762
suppressToolErrorWarnings,
17641763
...(usesHeartbeatResponseTool ? { enableHeartbeatTool: true, forceHeartbeatTool: true } : {}),

0 commit comments

Comments
 (0)