Skip to content

Commit 60b3a59

Browse files
committed
fix(cron): preserve intentional terminal progress
1 parent 485726e commit 60b3a59

2 files changed

Lines changed: 87 additions & 1 deletion

File tree

src/cron/isolated-agent/run.meta-error-status.test.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,85 @@ describe("runCronIsolatedAgentTurn - meta.error status propagation", () => {
170170
expect(result.delivered).toBe(false);
171171
});
172172

173+
it("keeps explicit silent replies as successful cron completions", async () => {
174+
runWithModelFallbackMock.mockResolvedValueOnce({
175+
result: {
176+
payloads: [],
177+
meta: {
178+
finalAssistantRawText: "NO_REPLY",
179+
finalAssistantVisibleText: "NO_REPLY",
180+
agentMeta: { usage: { input: 10, output: 1 } },
181+
},
182+
},
183+
provider: "anthropic",
184+
model: "claude-opus-4-8",
185+
attempts: [],
186+
});
187+
resolveCronDeliveryPlanMock.mockReturnValue({
188+
requested: true,
189+
mode: "announce",
190+
channel: "messagechat",
191+
to: "test-target",
192+
});
193+
resolveCronPayloadOutcomeMock.mockReturnValue({
194+
summary: undefined,
195+
outputText: undefined,
196+
synthesizedText: undefined,
197+
deliveryPayload: undefined,
198+
deliveryPayloads: [],
199+
deliveryPayloadHasStructuredContent: false,
200+
hasFatalErrorPayload: false,
201+
hasFatalStructuredErrorPayload: false,
202+
embeddedRunError: undefined,
203+
});
204+
205+
const result = await runCronIsolatedAgentTurn(makeIsolatedAgentParamsFixture());
206+
207+
expect(dispatchCronDeliveryMock).toHaveBeenCalled();
208+
expect(result.status).toBe("ok");
209+
expect(result.error).toBeUndefined();
210+
});
211+
212+
it("keeps committed message-tool deliveries as successful cron completions", async () => {
213+
runWithModelFallbackMock.mockResolvedValueOnce({
214+
result: {
215+
payloads: [],
216+
didSendViaMessagingTool: true,
217+
messagingToolSentTexts: ["Delivered to an intentional recipient"],
218+
messagingToolSentTargets: [],
219+
meta: {
220+
agentMeta: { usage: { input: 10, output: 0 } },
221+
},
222+
},
223+
provider: "anthropic",
224+
model: "claude-opus-4-8",
225+
attempts: [],
226+
});
227+
resolveCronDeliveryPlanMock.mockReturnValue({
228+
requested: true,
229+
mode: "announce",
230+
channel: "messagechat",
231+
to: "test-target",
232+
});
233+
resolveCronPayloadOutcomeMock.mockReturnValue({
234+
summary: undefined,
235+
outputText: undefined,
236+
synthesizedText: undefined,
237+
deliveryPayload: undefined,
238+
deliveryPayloads: [],
239+
deliveryPayloadHasStructuredContent: false,
240+
hasFatalErrorPayload: false,
241+
hasFatalStructuredErrorPayload: false,
242+
embeddedRunError: undefined,
243+
});
244+
245+
const result = await runCronIsolatedAgentTurn(makeIsolatedAgentParamsFixture());
246+
247+
expect(dispatchCronDeliveryMock).toHaveBeenCalled();
248+
expect(result.status).toBe("ok");
249+
expect(result.error).toBeUndefined();
250+
});
251+
173252
it("does not mark empty deterministic approval prompts as cron errors", async () => {
174253
runWithModelFallbackMock.mockResolvedValueOnce({
175254
result: {

src/cron/isolated-agent/run.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@ import { normalizeOptionalString } from "@openclaw/normalization-core/string-coe
44
import { hasAcceptedSessionSpawn } from "../../agents/accepted-session-spawn.js";
55
import { retireSessionMcpRuntime } from "../../agents/agent-bundle-mcp-tools.js";
66
import { hasAnyAuthProfileStoreSource } from "../../agents/auth-profiles/source-check.js";
7+
import { hasCommittedMessagingToolDeliveryEvidence } from "../../agents/embedded-agent-runner/delivery-evidence.js";
78
import { resolveAgentHarnessPolicy } from "../../agents/harness/policy.js";
89
import { findModelInCatalog } from "../../agents/model-catalog-lookup.js";
910
import { listOpenAIAuthProfileProvidersForAgentRuntime } from "../../agents/openai-routing.js";
1011
import { createAgentRunRestartAbortError } from "../../agents/run-termination.js";
1112
import { expandToolGroups, normalizeToolName } from "../../agents/tool-policy.js";
1213
import { deriveContextPromptTokens } from "../../agents/usage.js";
1314
import type { ThinkLevel } from "../../auto-reply/thinking.js";
15+
import { isSilentReplyPayloadText } from "../../auto-reply/tokens.js";
1416
import type { CliDeps } from "../../cli/outbound-send-deps.js";
1517
import {
1618
getRuntimeConfigSnapshot,
@@ -1345,15 +1347,20 @@ async function finalizeCronRun(params: {
13451347
});
13461348
}
13471349
const hasCommittedTerminalProgress =
1350+
hasCommittedMessagingToolDeliveryEvidence(finalRunResult) ||
13481351
finalRunResult.didSendDeterministicApprovalPrompt === true ||
13491352
hasAcceptedSessionSpawn(finalRunResult.acceptedSessionSpawns) ||
13501353
(finalRunResult.successfulCronAdds ?? 0) > 0;
1354+
const hasIntentionalSilentReply =
1355+
finalRunResult.meta?.terminalReplyKind === "silent-empty" ||
1356+
isSilentReplyPayloadText(finalRunResult.meta?.finalAssistantRawText) ||
1357+
isSilentReplyPayloadText(finalRunResult.meta?.finalAssistantVisibleText);
13511358
if (
13521359
prepared.deliveryRequested &&
13531360
!hasFatalErrorPayload &&
13541361
!sourceDeliveryOutcome.satisfiesSourceDelivery &&
13551362
!hasCommittedTerminalProgress &&
1356-
finalRunResult.meta?.terminalReplyKind !== "silent-empty" &&
1363+
!hasIntentionalSilentReply &&
13571364
deliveryPayloads.length === 0 &&
13581365
normalizeOptionalString(synthesizedText) === undefined
13591366
) {

0 commit comments

Comments
 (0)