mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-19 18:39:20 -05:00
fix(heartbeat): bound responsePrefix strip for ack detection
This commit is contained in:
@@ -50,6 +50,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Security/Sessions: create new session transcript JSONL files with user-only (`0o600`) permissions and extend `openclaw security audit --fix` to remediate existing transcript file permissions.
|
||||
- Infra/Fetch: ensure foreign abort-signal listener cleanup never masks original fetch successes/failures, while still preventing detached-finally unhandled rejection noise in `wrapFetchWithAbortSignal`. Thanks @Jackten.
|
||||
- Heartbeat: allow suppressing tool error warning payloads during heartbeat runs via a new heartbeat config flag. (#18497) Thanks @thewilloftheshadow.
|
||||
- Heartbeat/Telegram: strip configured `responsePrefix` before heartbeat ack detection (with boundary-safe matching) so prefixed `HEARTBEAT_OK` replies are correctly suppressed instead of leaking into DMs. (#18602)
|
||||
|
||||
## 2026.2.15
|
||||
|
||||
|
||||
@@ -201,7 +201,7 @@ describe("resolveHeartbeatIntervalMs", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("strips responsePrefix before detecting HEARTBEAT_OK and skips telegram delivery", async () => {
|
||||
it("strips responsePrefix before HEARTBEAT_OK detection and suppresses short ack text", async () => {
|
||||
await withTempTelegramHeartbeatSandbox(async ({ tmpDir, storePath, replySpy }) => {
|
||||
const cfg = createHeartbeatConfig({
|
||||
tmpDir,
|
||||
@@ -226,7 +226,7 @@ describe("resolveHeartbeatIntervalMs", () => {
|
||||
lastTo: "12345",
|
||||
});
|
||||
|
||||
replySpy.mockResolvedValue({ text: "[openclaw] HEARTBEAT_OK" });
|
||||
replySpy.mockResolvedValue({ text: "[openclaw] HEARTBEAT_OK all good" });
|
||||
const sendTelegram = vi.fn().mockResolvedValue({
|
||||
messageId: "m1",
|
||||
toJid: "jid",
|
||||
@@ -241,6 +241,51 @@ describe("resolveHeartbeatIntervalMs", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("does not strip alphanumeric responsePrefix from larger words", async () => {
|
||||
await withTempTelegramHeartbeatSandbox(async ({ tmpDir, storePath, replySpy }) => {
|
||||
const cfg = createHeartbeatConfig({
|
||||
tmpDir,
|
||||
storePath,
|
||||
heartbeat: {
|
||||
every: "5m",
|
||||
target: "telegram",
|
||||
},
|
||||
channels: {
|
||||
telegram: {
|
||||
token: "test-token",
|
||||
allowFrom: ["*"],
|
||||
heartbeat: { showOk: false },
|
||||
},
|
||||
},
|
||||
messages: { responsePrefix: "Hi" },
|
||||
});
|
||||
|
||||
await seedMainSession(storePath, cfg, {
|
||||
lastChannel: "telegram",
|
||||
lastProvider: "telegram",
|
||||
lastTo: "12345",
|
||||
});
|
||||
|
||||
replySpy.mockResolvedValue({ text: "History check complete" });
|
||||
const sendTelegram = vi.fn().mockResolvedValue({
|
||||
messageId: "m1",
|
||||
toJid: "jid",
|
||||
});
|
||||
|
||||
await runHeartbeatOnce({
|
||||
cfg,
|
||||
deps: makeTelegramDeps({ sendTelegram }),
|
||||
});
|
||||
|
||||
expect(sendTelegram).toHaveBeenCalledTimes(1);
|
||||
expect(sendTelegram).toHaveBeenCalledWith(
|
||||
"12345",
|
||||
"History check complete",
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("skips heartbeat LLM calls when visibility disables all output", async () => {
|
||||
await withTempHeartbeatSandbox(async ({ tmpDir, storePath, replySpy }) => {
|
||||
const cfg = createHeartbeatConfig({
|
||||
|
||||
@@ -430,17 +430,31 @@ async function captureTranscriptState(params: {
|
||||
}
|
||||
}
|
||||
|
||||
function stripLeadingHeartbeatResponsePrefix(
|
||||
text: string,
|
||||
responsePrefix: string | undefined,
|
||||
): string {
|
||||
const normalizedPrefix = responsePrefix?.trim();
|
||||
if (!normalizedPrefix) {
|
||||
return text;
|
||||
}
|
||||
|
||||
// Require a boundary after the configured prefix so short prefixes like "Hi"
|
||||
// do not strip the beginning of normal words like "History".
|
||||
const prefixPattern = new RegExp(
|
||||
`^${escapeRegExp(normalizedPrefix)}(?=$|\\s|[\\p{P}\\p{S}])\\s*`,
|
||||
"iu",
|
||||
);
|
||||
return text.replace(prefixPattern, "");
|
||||
}
|
||||
|
||||
function normalizeHeartbeatReply(
|
||||
payload: ReplyPayload,
|
||||
responsePrefix: string | undefined,
|
||||
ackMaxChars: number,
|
||||
) {
|
||||
const rawText = typeof payload.text === "string" ? payload.text : "";
|
||||
|
||||
const prefixPattern = responsePrefix?.trim()
|
||||
? new RegExp(`^${escapeRegExp(responsePrefix.trim())}\\s*`, "i")
|
||||
: null;
|
||||
const textForStrip = prefixPattern ? rawText.replace(prefixPattern, "") : rawText;
|
||||
const textForStrip = stripLeadingHeartbeatResponsePrefix(rawText, responsePrefix);
|
||||
const stripped = stripHeartbeatToken(textForStrip, {
|
||||
mode: "heartbeat",
|
||||
maxAckChars: ackMaxChars,
|
||||
|
||||
Reference in New Issue
Block a user