Skip to content

Commit 009db0b

Browse files
author
OpenClaw Assistant
committed
fix(discord): tighten longwork checkpoint evidence
1 parent e13cde3 commit 009db0b

2 files changed

Lines changed: 136 additions & 10 deletions

File tree

src/auto-reply/reply/agent-runner.runreplyagent.e2e.test.ts

Lines changed: 81 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1558,22 +1558,98 @@ describe("runReplyAgent typing (heartbeat)", () => {
15581558
await expect(run()).resolves.toBeUndefined();
15591559
});
15601560

1561-
it("does not surface an acknowledged-longwork checkpoint for a final-looking message followed by cleanup tools", async () => {
1561+
it.each(["Done — tests passed and the patch is ready.", "I am done checking; tests passed."])(
1562+
"does not surface an acknowledged-longwork checkpoint for final-looking message %j followed by cleanup tools",
1563+
async (messageText) => {
1564+
state.runEmbeddedPiAgentMock.mockResolvedValueOnce({
1565+
payloads: [],
1566+
messagingToolSentTexts: [messageText],
1567+
messagingToolSentTargets: [
1568+
{
1569+
tool: "message",
1570+
provider: "discord",
1571+
to: "channel:C1",
1572+
text: messageText,
1573+
},
1574+
],
1575+
toolActivityAfterMessagingToolDelivery: true,
1576+
meta: {
1577+
stopReason: "stop",
1578+
toolSummary: { calls: 4, tools: ["exec", "message", "read"] },
1579+
},
1580+
});
1581+
1582+
const { run } = createMinimalRun({
1583+
runOverrides: {
1584+
messageProvider: "discord",
1585+
allowEmptyAssistantReplyAsSilent: true,
1586+
},
1587+
sessionCtx: {
1588+
Provider: "discord",
1589+
OriginatingChannel: "discord",
1590+
OriginatingTo: "channel:C1",
1591+
ChatType: "channel",
1592+
MessageSid: "1506849058013184022",
1593+
},
1594+
});
1595+
1596+
await expect(run()).resolves.toBeUndefined();
1597+
},
1598+
);
1599+
1600+
it("does not surface an acknowledged-longwork checkpoint for an ACK sent to a different Discord route", async () => {
15621601
state.runEmbeddedPiAgentMock.mockResolvedValueOnce({
15631602
payloads: [],
1564-
messagingToolSentTexts: ["Done — tests passed and the patch is ready."],
1603+
messagingToolSentTexts: ["I will handle the remaining checks."],
1604+
messagingToolSentTargets: [
1605+
{
1606+
tool: "message",
1607+
provider: "discord",
1608+
to: "channel:C2",
1609+
text: "I will handle the remaining checks.",
1610+
},
1611+
],
1612+
toolActivityAfterMessagingToolDelivery: true,
1613+
meta: {
1614+
stopReason: "stop",
1615+
toolSummary: { calls: 4, tools: ["message", "exec", "read"] },
1616+
},
1617+
});
1618+
1619+
const { run } = createMinimalRun({
1620+
runOverrides: {
1621+
messageProvider: "discord",
1622+
allowEmptyAssistantReplyAsSilent: true,
1623+
},
1624+
sessionCtx: {
1625+
Provider: "discord",
1626+
OriginatingChannel: "discord",
1627+
OriginatingTo: "channel:C1",
1628+
ChatType: "channel",
1629+
MessageSid: "1506849058013184022",
1630+
},
1631+
});
1632+
1633+
await expect(run()).resolves.toBeUndefined();
1634+
});
1635+
1636+
it("does not surface an acknowledged-longwork checkpoint for an ACK sent to a different Discord thread", async () => {
1637+
state.runEmbeddedPiAgentMock.mockResolvedValueOnce({
1638+
payloads: [],
1639+
messagingToolSentTexts: ["I will handle the remaining checks."],
15651640
messagingToolSentTargets: [
15661641
{
15671642
tool: "message",
15681643
provider: "discord",
15691644
to: "channel:C1",
1570-
text: "Done — tests passed and the patch is ready.",
1645+
threadId: "T2",
1646+
text: "I will handle the remaining checks.",
15711647
},
15721648
],
15731649
toolActivityAfterMessagingToolDelivery: true,
15741650
meta: {
15751651
stopReason: "stop",
1576-
toolSummary: { calls: 4, tools: ["exec", "message", "read"] },
1652+
toolSummary: { calls: 4, tools: ["message", "exec", "read"] },
15771653
},
15781654
});
15791655

@@ -1586,6 +1662,7 @@ describe("runReplyAgent typing (heartbeat)", () => {
15861662
Provider: "discord",
15871663
OriginatingChannel: "discord",
15881664
OriginatingTo: "channel:C1",
1665+
MessageThreadId: "T1",
15891666
ChatType: "channel",
15901667
MessageSid: "1506849058013184022",
15911668
},

src/auto-reply/reply/agent-runner.ts

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { resolveContextTokensForModel } from "../../agents/context.js";
99
import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js";
1010
import { resolveModelAuthMode } from "../../agents/model-auth.js";
1111
import { isCliProvider } from "../../agents/model-selection.js";
12+
import type { MessagingToolSend } from "../../agents/pi-embedded-messaging.types.js";
1213
import {
1314
formatEmbeddedPiQueueFailureSummary,
1415
queueEmbeddedPiMessageWithOutcomeAsync,
@@ -37,7 +38,10 @@ import { enqueueSystemEvent } from "../../infra/system-events.js";
3738
import { CommandLaneClearedError, GatewayDrainingError } from "../../process/command-queue.js";
3839
import { shouldPreserveUserFacingSessionStateForInputProvenance } from "../../sessions/input-provenance.js";
3940
import { resolveSendPolicy } from "../../sessions/send-policy.js";
40-
import { normalizeOptionalString } from "../../shared/string-coerce.js";
41+
import {
42+
normalizeOptionalString,
43+
normalizeOptionalStringifiedId,
44+
} from "../../shared/string-coerce.js";
4145
import {
4246
estimateUsageCost,
4347
formatTokenCount,
@@ -95,6 +99,7 @@ import {
9599
type QueueSettings,
96100
} from "./queue.js";
97101
import { createReplyMediaContext } from "./reply-media-paths.js";
102+
import { getMatchingMessagingToolReplyTargets } from "./reply-payloads-dedupe.js";
98103
import { replyRunRegistry, type ReplyOperation } from "./reply-run-registry.js";
99104
import { createReplyToModeFilterForChannel, resolveReplyToMode } from "./reply-threading.js";
100105
import { admitReplyTurn, resolveReplyTurnKind } from "./reply-turn-admission.js";
@@ -170,8 +175,18 @@ function isAcknowledgedLongworkMessagingText(value: string): boolean {
170175
}
171176
// Keep this guard conservative: a generic checkpoint is only helpful when the
172177
// earlier message looks like an ACK/progress commitment, not a final result.
178+
if (
179+
/\b(?:done|finished|completed|complete|ready|resolved|fixed|passed|success(?:fully)?|sent|delivered)\b/.test(
180+
normalized,
181+
) &&
182+
/\b(?:i(?:'|)?m|i am|i(?:'|)?ve|i have|tests?|checks?|patch|result|summary|report|final|reply|message)\b/.test(
183+
normalized,
184+
)
185+
) {
186+
return false;
187+
}
173188
return (
174-
/\b(i(?:'|)?ll|i will|i am|i'm|working|starting|checking|investigating|continue|handle|look into|going to|detaching)\b/.test(
189+
/\b(i(?:'|)?ll|i will|working|starting|checking|investigating|continu(?:e|ing)|handling|handle|look into|going to|detaching)\b/.test(
175190
normalized,
176191
) || /|||||||||.*[]/.test(value)
177192
);
@@ -201,12 +216,42 @@ function hasAcknowledgedLongworkMessagingText(params: {
201216
});
202217
}
203218

219+
function resolveAcknowledgedLongworkRouteTexts(params: {
220+
followupRun: FollowupRun;
221+
sessionCtx: TemplateContext;
222+
messagingToolSentTargets?: MessagingToolSend[];
223+
}): string[] {
224+
const sentTargets = params.messagingToolSentTargets ?? [];
225+
const matchingTargets = getMatchingMessagingToolReplyTargets({
226+
messageProvider: resolveOriginMessageProvider({
227+
originatingChannel: params.sessionCtx.OriginatingChannel,
228+
provider: params.followupRun.run.messageProvider,
229+
}),
230+
messagingToolSentTargets: sentTargets,
231+
originatingTo: resolveOriginMessageTo({
232+
originatingTo: params.sessionCtx.OriginatingTo,
233+
to: params.sessionCtx.To,
234+
}),
235+
accountId: params.sessionCtx.AccountId,
236+
}).filter((target) => {
237+
const originThreadId = normalizeOptionalStringifiedId(params.sessionCtx.MessageThreadId);
238+
const targetThreadId = normalizeOptionalStringifiedId(target.threadId);
239+
if (!originThreadId) {
240+
return !targetThreadId;
241+
}
242+
return targetThreadId === originThreadId;
243+
});
244+
return matchingTargets.flatMap((target) =>
245+
typeof target.text === "string" && target.text.trim() ? [target.text] : [],
246+
);
247+
}
248+
204249
function hasSuccessfulSideEffectDelivery(params: {
205250
blockReplyPipeline: { didStream: () => boolean; isAborted: () => boolean } | null;
206251
directlySentBlockKeys?: Set<string>;
207252
messagingToolSentTexts?: string[];
208253
messagingToolSentMediaUrls?: string[];
209-
messagingToolSentTargets?: unknown[];
254+
messagingToolSentTargets?: MessagingToolSend[];
210255
successfulCronAdds?: number;
211256
didSendDeterministicApprovalPrompt?: boolean;
212257
}): boolean {
@@ -248,7 +293,7 @@ function shouldSurfaceAcknowledgedLongworkCheckpoint(params: {
248293
toolActivityAfterMessagingToolDelivery?: boolean;
249294
messagingToolSentTexts?: string[];
250295
messagingToolSentMediaUrls?: string[];
251-
messagingToolSentTargets?: unknown[];
296+
messagingToolSentTargets?: MessagingToolSend[];
252297
successfulCronAdds?: number;
253298
didSendDeterministicApprovalPrompt?: boolean;
254299
}): boolean {
@@ -263,10 +308,14 @@ function shouldSurfaceAcknowledgedLongworkCheckpoint(params: {
263308
) {
264309
return false;
265310
}
266-
return hasAcknowledgedLongworkMessagingText({
267-
messagingToolSentTexts: params.messagingToolSentTexts,
311+
const routeSentTexts = resolveAcknowledgedLongworkRouteTexts({
312+
followupRun: params.followupRun,
313+
sessionCtx: params.sessionCtx,
268314
messagingToolSentTargets: params.messagingToolSentTargets,
269315
});
316+
return hasAcknowledgedLongworkMessagingText({
317+
messagingToolSentTexts: routeSentTexts,
318+
});
270319
}
271320

272321
function resolveConfiguredFallbackModel(params: {

0 commit comments

Comments
 (0)