Skip to content

Commit eb66def

Browse files
steipeteHCL
andcommitted
fix: scope messaging tool final reply dedupe
Co-authored-by: HCL <[email protected]>
1 parent 5d09b4b commit eb66def

20 files changed

Lines changed: 502 additions & 84 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ Docs: https://docs.openclaw.ai
4444
- Gateway/restart: verify listener PIDs by argv when `lsof` reports only the Node process name, so stale gateway cleanup can find macOS `cnode` listeners. Fixes #70664.
4545
- Gateway/logging: expand leading `~` in `logging.file` before creating the file logger, preventing startup crash loops for home-relative log paths. Fixes #73587.
4646
- Channels/CLI: keep `openclaw channels list --json` usable when provider usage fetching fails, and report per-provider usage errors without aborting the channel list. Refs #67595.
47+
- Agents/messaging: deliver distinct final commentary after same-target `message` tool sends while still deduping text/media already sent by the tool, so short closing remarks are no longer silently dropped. Fixes #76915. Thanks @hclsys.
4748
- Gateway/systemd: preserve operator-added secrets in the Gateway env file across re-stage while clearing OpenClaw-managed keys (such as `OPENCLAW_GATEWAY_TOKEN`) so a fresh staging value is never shadowed by a stale env-file copy; operator secrets are also retained when the state-dir `.env` is empty. Fixes #76860. Thanks @hclsys.
4849
- Plugin updates: do not short-circuit trusted official npm updates as unchanged when the default/latest spec still resolves to an already-installed prerelease that the installer should replace with a stable fallback. Thanks @vincentkoc.
4950
- Plugin tools: keep auth-unavailable optional tools hidden even when another default tool from the same plugin is available and `tools.alsoAllow` names the optional tool. Thanks @vincentkoc.

extensions/codex/src/app-server/dynamic-tools.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,8 @@ describe("createCodexDynamicToolBridge", () => {
179179
provider: "telegram",
180180
to: "chat-1",
181181
threadId: "thread-ts-1",
182+
text: "hello from Codex",
183+
mediaUrls: ["/tmp/reply.png"],
182184
},
183185
],
184186
});

extensions/codex/src/app-server/dynamic-tools.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,13 +231,16 @@ function collectToolTelemetry(params: {
231231
if (text) {
232232
params.telemetry.messagingToolSentTexts.push(text);
233233
}
234-
params.telemetry.messagingToolSentMediaUrls.push(...collectMediaUrls(params.args));
234+
const mediaUrls = collectMediaUrls(params.args);
235+
params.telemetry.messagingToolSentMediaUrls.push(...mediaUrls);
235236
params.telemetry.messagingToolSentTargets.push({
236237
tool: params.toolName,
237238
provider: readFirstString(params.args, ["provider", "channel"]) ?? params.toolName,
238239
accountId: readFirstString(params.args, ["accountId", "account_id"]),
239240
to: readFirstString(params.args, ["to", "target", "recipient"]),
240241
threadId: readFirstString(params.args, ["threadId", "thread_id", "messageThreadId"]),
242+
...(text ? { text } : {}),
243+
...(mediaUrls.length > 0 ? { mediaUrls } : {}),
241244
});
242245
}
243246

extensions/codex/src/app-server/openclaw-owned-tool-runtime-contract.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,8 @@ describe("OpenClaw-owned tool runtime contract — Codex app-server adapter", ()
308308
provider: "telegram",
309309
to: "chat-1",
310310
threadId: "thread-ts-1",
311+
text: "hello from Codex",
312+
mediaUrls: ["/tmp/codex-reply.png"],
311313
},
312314
],
313315
});

src/agents/openclaw-owned-tool-runtime-contract.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,8 @@ describe("OpenClaw-owned tool runtime contract — Pi adapter", () => {
291291
tool: "message",
292292
provider: "telegram",
293293
to: "chat-1",
294+
text: "hello from Pi",
295+
mediaUrls: ["/tmp/pi-reply.png"],
294296
}),
295297
]);
296298
await vi.waitFor(() => {

src/agents/pi-embedded-helpers.sanitizeuserfacingtext.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -798,6 +798,13 @@ describe("isMessagingToolDuplicate", () => {
798798
sentTexts: ['I sent the message: "Hello, this is a test message!"'],
799799
expected: true,
800800
},
801+
{
802+
input: "v2ex hot topics delivered to telegram",
803+
sentTexts: [
804+
"1. some article title\n2. another title\nv2ex hot topics delivered to telegram\n3. yet another",
805+
],
806+
expected: false,
807+
},
801808
{
802809
input: "This is completely different content.",
803810
sentTexts: ["Hello, this is a test message!"],

src/agents/pi-embedded-helpers/messaging-dedupe.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
22

33
const MIN_DUPLICATE_TEXT_LENGTH = 10;
4+
const MIN_REVERSE_SUBSTRING_DUPLICATE_RATIO = 0.5;
45

56
/**
67
* Normalize text for duplicate comparison.
@@ -30,7 +31,13 @@ export function isMessagingToolDuplicateNormalized(
3031
if (!normalizedSent || normalizedSent.length < MIN_DUPLICATE_TEXT_LENGTH) {
3132
return false;
3233
}
33-
return normalized.includes(normalizedSent) || normalizedSent.includes(normalized);
34+
if (normalized.includes(normalizedSent)) {
35+
return true;
36+
}
37+
return (
38+
normalizedSent.includes(normalized) &&
39+
normalized.length >= normalizedSent.length * MIN_REVERSE_SUBSTRING_DUPLICATE_RATIO
40+
);
3441
});
3542
}
3643

src/agents/pi-embedded-messaging.types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,6 @@ export type MessagingToolSend = {
44
accountId?: string;
55
to?: string;
66
threadId?: string;
7+
text?: string;
8+
mediaUrls?: string[];
79
};

src/agents/pi-embedded-subscribe.handlers.tools.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -847,6 +847,13 @@ describe("messaging tool media URL tracking", () => {
847847
await handleToolExecutionEnd(ctx, endEvt);
848848

849849
expect(ctx.state.messagingToolSentMediaUrls).toContain("file:///img.jpg");
850+
expect(ctx.state.messagingToolSentTargets).toEqual([
851+
expect.objectContaining({
852+
to: "channel:123",
853+
text: "hi",
854+
mediaUrls: ["file:///img.jpg"],
855+
}),
856+
]);
850857
expect(ctx.state.pendingMessagingMediaUrls.has("tool-m2")).toBe(false);
851858
});
852859

@@ -883,6 +890,13 @@ describe("messaging tool media URL tracking", () => {
883890
"file:///img-a.jpg",
884891
"file:///img-b.jpg",
885892
]);
893+
expect(ctx.state.messagingToolSentTargets).toEqual([
894+
expect.objectContaining({
895+
to: "channel:123",
896+
text: "hi",
897+
mediaUrls: ["file:///img-a.jpg", "file:///img-b.jpg"],
898+
}),
899+
]);
886900
});
887901

888902
it("trims messagingToolSentMediaUrls to 200 on commit (FIFO)", async () => {

src/agents/pi-embedded-subscribe.handlers.tools.ts

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -867,9 +867,21 @@ export async function handleToolExecutionEnd(
867867
});
868868
}
869869

870-
// Commit messaging tool text on success, discard on error.
870+
// Commit messaging tool evidence on success, discard on error.
871871
const pendingText = ctx.state.pendingMessagingTexts.get(toolCallId);
872872
const pendingTarget = ctx.state.pendingMessagingTargets.get(toolCallId);
873+
const pendingMediaUrls = ctx.state.pendingMessagingMediaUrls.get(toolCallId) ?? [];
874+
const startArgs =
875+
startData?.args && typeof startData.args === "object"
876+
? (startData.args as Record<string, unknown>)
877+
: {};
878+
const isMessagingSend =
879+
pendingMediaUrls.length > 0 ||
880+
(isMessagingTool(toolName) && isMessagingToolSendAction(toolName, startArgs));
881+
const committedMediaUrls =
882+
!isToolError && isMessagingSend
883+
? [...pendingMediaUrls, ...collectMessagingMediaUrlsFromToolResult(result)]
884+
: [];
873885
if (pendingText) {
874886
ctx.state.pendingMessagingTexts.delete(toolCallId);
875887
if (!isToolError) {
@@ -882,24 +894,16 @@ export async function handleToolExecutionEnd(
882894
if (pendingTarget) {
883895
ctx.state.pendingMessagingTargets.delete(toolCallId);
884896
if (!isToolError) {
885-
ctx.state.messagingToolSentTargets.push(pendingTarget);
897+
ctx.state.messagingToolSentTargets.push({
898+
...pendingTarget,
899+
...(pendingText ? { text: pendingText } : {}),
900+
...(committedMediaUrls.length > 0 ? { mediaUrls: committedMediaUrls.slice() } : {}),
901+
});
886902
ctx.trimMessagingToolSent();
887903
}
888904
}
889-
const pendingMediaUrls = ctx.state.pendingMessagingMediaUrls.get(toolCallId) ?? [];
890905
ctx.state.pendingMessagingMediaUrls.delete(toolCallId);
891-
const startArgs =
892-
startData?.args && typeof startData.args === "object"
893-
? (startData.args as Record<string, unknown>)
894-
: {};
895-
const isMessagingSend =
896-
pendingMediaUrls.length > 0 ||
897-
(isMessagingTool(toolName) && isMessagingToolSendAction(toolName, startArgs));
898906
if (!isToolError && isMessagingSend) {
899-
const committedMediaUrls = [
900-
...pendingMediaUrls,
901-
...collectMessagingMediaUrlsFromToolResult(result),
902-
];
903907
if (committedMediaUrls.length > 0) {
904908
ctx.state.messagingToolSentMediaUrls.push(...committedMediaUrls);
905909
ctx.trimMessagingToolSent();

0 commit comments

Comments
 (0)