From 8c8374defa4d670e62236ba2a161ff009462b1f8 Mon Sep 17 00:00:00 2001 From: chilu18 Date: Mon, 23 Feb 2026 19:16:57 +0000 Subject: [PATCH] fix(cron): treat embedded error payloads as run failures (cherry picked from commit 50fd31c070e8b466db6d81c70b285fd631df1c05) --- ....uses-last-non-empty-agent-text-as.test.ts | 27 +++++++++++++++++-- src/cron/isolated-agent/run.ts | 26 ++++++++++++++++-- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/src/cron/isolated-agent.uses-last-non-empty-agent-text-as.test.ts b/src/cron/isolated-agent.uses-last-non-empty-agent-text-as.test.ts index abb27177a54..353d92e1b85 100644 --- a/src/cron/isolated-agent.uses-last-non-empty-agent-text-as.test.ts +++ b/src/cron/isolated-agent.uses-last-non-empty-agent-text-as.test.ts @@ -27,9 +27,9 @@ function makeDeps(): CliDeps { }; } -function mockEmbeddedTexts(texts: string[]) { +function mockEmbeddedPayloads(payloads: Array<{ text?: string; isError?: boolean }>) { vi.mocked(runEmbeddedPiAgent).mockResolvedValue({ - payloads: texts.map((text) => ({ text })), + payloads, meta: { durationMs: 5, agentMeta: { sessionId: "s", provider: "p", model: "m" }, @@ -37,6 +37,10 @@ function mockEmbeddedTexts(texts: string[]) { }); } +function mockEmbeddedTexts(texts: string[]) { + mockEmbeddedPayloads(texts.map((text) => ({ text }))); +} + function mockEmbeddedOk() { mockEmbeddedTexts(["ok"]); } @@ -174,6 +178,25 @@ describe("runCronIsolatedAgentTurn", () => { }); }); + it("returns error when embedded run payload is marked as error", async () => { + await withTempHome(async (home) => { + mockEmbeddedPayloads([ + { + text: "⚠️ 🛠️ Exec failed: /bin/bash: line 1: python: command not found", + isError: true, + }, + ]); + const { res } = await runCronTurn(home, { + jobPayload: DEFAULT_AGENT_TURN_PAYLOAD, + mockTexts: null, + }); + + expect(res.status).toBe("error"); + expect(res.error).toContain("command not found"); + expect(res.summary).toContain("Exec failed"); + }); + }); + it("passes resolved agentDir to runEmbeddedPiAgent", async () => { await withTempHome(async (home) => { const { res } = await runCronTurn(home, { diff --git a/src/cron/isolated-agent/run.ts b/src/cron/isolated-agent/run.ts index ea6c819e253..bfc37d48249 100644 --- a/src/cron/isolated-agent/run.ts +++ b/src/cron/isolated-agent/run.ts @@ -543,6 +543,25 @@ export async function runCronIsolatedAgentTurn(params: { (deliveryPayload?.mediaUrls?.length ?? 0) > 0 || Object.keys(deliveryPayload?.channelData ?? {}).length > 0; const deliveryBestEffort = resolveCronDeliveryBestEffort(params.job); + const hasErrorPayload = payloads.some((payload) => payload?.isError === true); + const lastErrorPayloadText = [...payloads] + .toReversed() + .find((payload) => payload?.isError === true && Boolean(payload?.text?.trim())) + ?.text?.trim(); + const embeddedRunError = hasErrorPayload + ? (lastErrorPayloadText ?? "cron isolated run returned an error payload") + : undefined; + const resolveRunOutcome = (params?: { delivered?: boolean }) => + withRunSession({ + status: hasErrorPayload ? "error" : "ok", + ...(hasErrorPayload + ? { error: embeddedRunError ?? "cron isolated run returned an error payload" } + : {}), + summary, + outputText, + delivered: params?.delivered, + ...telemetry, + }); // Skip delivery for heartbeat-only responses (HEARTBEAT_OK with no real content). const ackMaxChars = resolveHeartbeatAckMaxChars(agentCfg); @@ -586,11 +605,14 @@ export async function runCronIsolatedAgentTurn(params: { withRunSession, }); if (deliveryResult.result) { - return deliveryResult.result; + if (!hasErrorPayload || deliveryResult.result.status !== "ok") { + return deliveryResult.result; + } + return resolveRunOutcome({ delivered: deliveryResult.result.delivered }); } const delivered = deliveryResult.delivered; summary = deliveryResult.summary; outputText = deliveryResult.outputText; - return withRunSession({ status: "ok", summary, outputText, delivered, ...telemetry }); + return resolveRunOutcome({ delivered }); }