mirror of
https://fastgit.cc/github.com/openclaw/openclaw
synced 2026-04-30 14:02:56 +08:00
fix(cron): suppress trailing NO_REPLY in announce delivery path [AI-assisted] (#65004)
Merged via squash.
Prepared head SHA: b7f1996d60
Co-authored-by: neo1027144-creator <267440006+neo1027144-creator@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
This commit is contained in:
@@ -10,6 +10,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
- Docker/build: verify `@matrix-org/matrix-sdk-crypto-nodejs` native bindings with `find` under `node_modules` instead of a hardcoded `.pnpm/...` path so pnpm v10+ virtual-store layouts no longer fail the image build. (#67143) thanks @ly85206559.
|
||||
- Matrix/E2EE: keep startup bootstrap conservative for passwordless token-auth bots, still attempt the guarded repair pass without requiring `channels.matrix.password`, and document the remaining password-UIA limitation. (#66228) Thanks @SARAMALI15792.
|
||||
- Cron/announce delivery: suppress mixed-content isolated cron announce replies that end with `NO_REPLY` so trailing silent sentinels no longer leak summary text to the target channel. (#65004) thanks @neo1027144-creator.
|
||||
|
||||
## 2026.4.15-beta.1
|
||||
|
||||
|
||||
@@ -940,4 +940,50 @@ describe("dispatchCronDelivery — double-announce guard", () => {
|
||||
timeoutMs: 10_000,
|
||||
});
|
||||
});
|
||||
|
||||
it("suppresses trailing NO_REPLY after summary text in direct delivery (#64976)", async () => {
|
||||
vi.mocked(countActiveDescendantRuns).mockReturnValue(0);
|
||||
vi.mocked(isLikelyInterimCronMessage).mockReturnValue(false);
|
||||
|
||||
const params = makeBaseParams({
|
||||
synthesizedText: "All 3 items already processed.\n\nNO_REPLY",
|
||||
});
|
||||
(params as Record<string, unknown>).deliveryPayloadHasStructuredContent = true;
|
||||
const state = await dispatchCronDelivery(params);
|
||||
|
||||
expect(deliverOutboundPayloads).not.toHaveBeenCalled();
|
||||
expect(state.result).toEqual(
|
||||
expect.objectContaining({ status: "ok", delivered: false, deliveryAttempted: true }),
|
||||
);
|
||||
});
|
||||
|
||||
it("suppresses trailing NO_REPLY after summary text in text delivery (#64976)", async () => {
|
||||
vi.mocked(countActiveDescendantRuns).mockReturnValue(0);
|
||||
vi.mocked(isLikelyInterimCronMessage).mockReturnValue(false);
|
||||
|
||||
const params = makeBaseParams({
|
||||
synthesizedText: "Nothing actionable found today.\n\nNO_REPLY",
|
||||
});
|
||||
const state = await dispatchCronDelivery(params);
|
||||
|
||||
expect(deliverOutboundPayloads).not.toHaveBeenCalled();
|
||||
expect(state.result).toEqual(
|
||||
expect.objectContaining({ status: "ok", delivered: false, deliveryAttempted: true }),
|
||||
);
|
||||
});
|
||||
|
||||
it("suppresses mixed-case trailing No_Reply after summary text (#64976)", async () => {
|
||||
vi.mocked(countActiveDescendantRuns).mockReturnValue(0);
|
||||
vi.mocked(isLikelyInterimCronMessage).mockReturnValue(false);
|
||||
|
||||
const params = makeBaseParams({
|
||||
synthesizedText: "All done, nothing to report.\n\nNo_Reply",
|
||||
});
|
||||
const state = await dispatchCronDelivery(params);
|
||||
|
||||
expect(deliverOutboundPayloads).not.toHaveBeenCalled();
|
||||
expect(state.result).toEqual(
|
||||
expect.objectContaining({ status: "ok", delivered: false, deliveryAttempted: true }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ReplyPayload } from "../../auto-reply/reply-payload.js";
|
||||
import { isSilentReplyText, SILENT_REPLY_TOKEN } from "../../auto-reply/tokens.js";
|
||||
import { isSilentReplyText, stripSilentToken, SILENT_REPLY_TOKEN } from "../../auto-reply/tokens.js";
|
||||
import type { CliDeps } from "../../cli/outbound-send-deps.js";
|
||||
import {
|
||||
resolveAgentMainSessionKey,
|
||||
@@ -463,9 +463,20 @@ export async function dispatchCronDelivery(
|
||||
? [{ text: synthesizedText }]
|
||||
: [];
|
||||
// Suppress NO_REPLY sentinel so it never leaks to external channels.
|
||||
const payloadsForDelivery = rawPayloads.filter(
|
||||
(p) => !isSilentReplyText(p.text, SILENT_REPLY_TOKEN),
|
||||
);
|
||||
// Also suppress payloads where the agent appended a trailing NO_REPLY
|
||||
// after other text (e.g. "summary...\n\nNO_REPLY") — the token signals
|
||||
// "do not deliver" regardless of preceding content.
|
||||
const payloadsForDelivery = rawPayloads.filter((p) => {
|
||||
const text = p.text ?? "";
|
||||
if (isSilentReplyText(text, SILENT_REPLY_TOKEN)) {
|
||||
return false;
|
||||
}
|
||||
// Case-insensitive trailing check: uppercase before stripping since
|
||||
// stripSilentToken's regex is case-sensitive.
|
||||
const upper = text.toUpperCase();
|
||||
const stripped = stripSilentToken(upper, SILENT_REPLY_TOKEN);
|
||||
return stripped === upper.trim();
|
||||
});
|
||||
if (payloadsForDelivery.length === 0) {
|
||||
return await finishSilentReplyDelivery();
|
||||
}
|
||||
@@ -689,9 +700,17 @@ export async function dispatchCronDelivery(
|
||||
...params.telemetry,
|
||||
});
|
||||
}
|
||||
// Suppress delivery when synthesizedText is (or ends with) NO_REPLY.
|
||||
// isSilentReplyText handles case-insensitive exact matches (e.g. "No_Reply");
|
||||
// stripSilentToken catches trailing tokens after other text.
|
||||
if (isSilentReplyText(synthesizedText, SILENT_REPLY_TOKEN)) {
|
||||
return await finishSilentReplyDelivery();
|
||||
}
|
||||
const upperSynthesized = synthesizedText.toUpperCase();
|
||||
const strippedSynthesized = stripSilentToken(upperSynthesized, SILENT_REPLY_TOKEN);
|
||||
if (strippedSynthesized !== upperSynthesized.trim()) {
|
||||
return await finishSilentReplyDelivery();
|
||||
}
|
||||
if (params.isAborted()) {
|
||||
return params.withRunSession({
|
||||
status: "error",
|
||||
|
||||
Reference in New Issue
Block a user