Skip to content

Commit d509a81

Browse files
SidSid-QinTakhoffman
authored
fix(cron): treat transient tool error payloads as recoverable (#29527) thanks @Sid-Qin
Verified: - pnpm install --frozen-lockfile - pnpm check - pnpm test -- --run src/cron/isolated-agent.uses-last-non-empty-agent-text-as.test.ts Co-authored-by: Sid-Qin <[email protected]> Co-authored-by: Tak Hoffman <[email protected]>
1 parent 635c78a commit d509a81

File tree

2 files changed

+60
-4
lines changed

2 files changed

+60
-4
lines changed

src/cron/isolated-agent.uses-last-non-empty-agent-text-as.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,50 @@ describe("runCronIsolatedAgentTurn", () => {
197197
});
198198
});
199199

200+
it("treats transient error payloads as non-fatal when a later success payload exists", async () => {
201+
await withTempHome(async (home) => {
202+
mockEmbeddedPayloads([
203+
{
204+
text: "⚠️ ✍️ Write: failed",
205+
isError: true,
206+
},
207+
{
208+
text: "Write completed successfully.",
209+
isError: false,
210+
},
211+
]);
212+
const { res } = await runCronTurn(home, {
213+
jobPayload: DEFAULT_AGENT_TURN_PAYLOAD,
214+
mockTexts: null,
215+
});
216+
217+
expect(res.status).toBe("ok");
218+
expect(res.summary).toBe("Write completed successfully.");
219+
});
220+
});
221+
222+
it("keeps error status when run-level error accompanies post-error text", async () => {
223+
await withTempHome(async (home) => {
224+
vi.mocked(runEmbeddedPiAgent).mockResolvedValue({
225+
payloads: [
226+
{ text: "Model context overflow", isError: true },
227+
{ text: "Partial assistant text before error" },
228+
],
229+
meta: {
230+
durationMs: 5,
231+
agentMeta: { sessionId: "s", provider: "p", model: "m" },
232+
error: { kind: "context_overflow", message: "exceeded context window" },
233+
},
234+
});
235+
const { res } = await runCronTurn(home, {
236+
jobPayload: DEFAULT_AGENT_TURN_PAYLOAD,
237+
mockTexts: null,
238+
});
239+
240+
expect(res.status).toBe("error");
241+
});
242+
});
243+
200244
it("passes resolved agentDir to runEmbeddedPiAgent", async () => {
201245
await withTempHome(async (home) => {
202246
const { res } = await runCronTurn(home, {

src/cron/isolated-agent/run.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -570,17 +570,29 @@ export async function runCronIsolatedAgentTurn(params: {
570570
Object.keys(deliveryPayload?.channelData ?? {}).length > 0;
571571
const deliveryBestEffort = resolveCronDeliveryBestEffort(params.job);
572572
const hasErrorPayload = payloads.some((payload) => payload?.isError === true);
573+
const runLevelError = runResult.meta?.error;
574+
const lastErrorPayloadIndex = payloads.findLastIndex((payload) => payload?.isError === true);
575+
const hasSuccessfulPayloadAfterLastError =
576+
!runLevelError &&
577+
lastErrorPayloadIndex >= 0 &&
578+
payloads
579+
.slice(lastErrorPayloadIndex + 1)
580+
.some((payload) => payload?.isError !== true && Boolean(payload?.text?.trim()));
581+
// Tool wrappers can emit transient/false-positive error payloads before a valid final
582+
// assistant payload. Only treat payload errors as recoverable when (a) the run itself
583+
// did not report a model/context-level error and (b) a non-error payload follows.
584+
const hasFatalErrorPayload = hasErrorPayload && !hasSuccessfulPayloadAfterLastError;
573585
const lastErrorPayloadText = [...payloads]
574586
.toReversed()
575587
.find((payload) => payload?.isError === true && Boolean(payload?.text?.trim()))
576588
?.text?.trim();
577-
const embeddedRunError = hasErrorPayload
589+
const embeddedRunError = hasFatalErrorPayload
578590
? (lastErrorPayloadText ?? "cron isolated run returned an error payload")
579591
: undefined;
580592
const resolveRunOutcome = (params?: { delivered?: boolean; deliveryAttempted?: boolean }) =>
581593
withRunSession({
582-
status: hasErrorPayload ? "error" : "ok",
583-
...(hasErrorPayload
594+
status: hasFatalErrorPayload ? "error" : "ok",
595+
...(hasFatalErrorPayload
584596
? { error: embeddedRunError ?? "cron isolated run returned an error payload" }
585597
: {}),
586598
summary,
@@ -637,7 +649,7 @@ export async function runCronIsolatedAgentTurn(params: {
637649
deliveryAttempted:
638650
deliveryResult.result.deliveryAttempted ?? deliveryResult.deliveryAttempted,
639651
};
640-
if (!hasErrorPayload || deliveryResult.result.status !== "ok") {
652+
if (!hasFatalErrorPayload || deliveryResult.result.status !== "ok") {
641653
return resultWithDeliveryMeta;
642654
}
643655
return resolveRunOutcome({

0 commit comments

Comments
 (0)