diff --git a/src/cron/isolated-agent/run.payload-fallbacks.test.ts b/src/cron/isolated-agent/run.payload-fallbacks.test.ts index ccccdb122c4c..54a43cee60bf 100644 --- a/src/cron/isolated-agent/run.payload-fallbacks.test.ts +++ b/src/cron/isolated-agent/run.payload-fallbacks.test.ts @@ -39,6 +39,31 @@ function requireModelFallbackRequest(): { describe("runCronIsolatedAgentTurn — payload.fallbacks", () => { setupRunCronIsolatedAgentTurnSuite({ fast: true }); + it("uses the persisted agentTurn payload message when the dispatch message is malformed", async () => { + mockRunCronFallbackPassthrough(); + const dispatchMessage = "SERIALIZATION_PROBE should not be wrapped"; + + const result = await runCronIsolatedAgentTurn( + makeIsolatedAgentTurnParams({ + job: makeIsolatedAgentTurnJob({ + payload: { + kind: "agentTurn", + message: + "SERIALIZATION_PROBE: reply exactly with the marker token you received and nothing else.", + }, + }), + message: { message: dispatchMessage } as unknown as string, + }), + ); + + expect(result.status).toBe("ok"); + expect(runEmbeddedAgentMock).toHaveBeenCalledOnce(); + const request = runEmbeddedAgentMock.mock.calls[0]?.[0] as { prompt?: unknown } | undefined; + expect(request?.prompt).toContain("SERIALIZATION_PROBE: reply exactly"); + expect(request?.prompt).not.toContain(dispatchMessage); + expect(request?.prompt).not.toContain("[object Object]"); + }); + it.each([ { name: "passes payload.fallbacks as fallbacksOverride when defined", diff --git a/src/cron/isolated-agent/run.ts b/src/cron/isolated-agent/run.ts index 99f525df43df..e3d6ce2c8e4d 100644 --- a/src/cron/isolated-agent/run.ts +++ b/src/cron/isolated-agent/run.ts @@ -456,6 +456,13 @@ type RunCronAgentTurnParams = { lane?: string; }; +function resolveCronAgentTurnMessage(input: RunCronAgentTurnParams): string { + if (input.job.payload.kind === "agentTurn") { + return input.job.payload.message; + } + return input.message; +} + type WithRunSession = ( result: Omit, ) => RunCronAgentTurnResult; @@ -765,7 +772,8 @@ async function prepareCronRunContext(params: { }); const { formattedTime, timeLine } = resolveCronStyleNow(input.cfg, now); - const base = `[cron:${input.job.id} ${input.job.name}] ${input.message}`.trim(); + const message = resolveCronAgentTurnMessage(input); + const base = `[cron:${input.job.id} ${input.job.name}] ${message}`.trim(); const isExternalHook = hookExternalContentSource !== undefined || isExternalHookSession(baseSessionKey); const allowUnsafeExternalContent = @@ -776,7 +784,7 @@ async function prepareCronRunContext(params: { if (isExternalHook) { const { detectSuspiciousPatterns } = await loadCronExternalContentRuntime(); - const suspiciousPatterns = detectSuspiciousPatterns(input.message); + const suspiciousPatterns = detectSuspiciousPatterns(message); if (suspiciousPatterns.length > 0) { logWarn( `[security] Suspicious patterns detected in external hook content ` + @@ -789,7 +797,7 @@ async function prepareCronRunContext(params: { const { buildSafeExternalPrompt } = await loadCronExternalContentRuntime(); const hookType = mapHookExternalContentSource(hookExternalContentSource ?? "webhook"); const safeContent = buildSafeExternalPrompt({ - content: input.message, + content: message, source: hookType, jobName: input.job.name, jobId: input.job.id, diff --git a/src/cron/service/ops.regression.test.ts b/src/cron/service/ops.regression.test.ts index 604e557bd429..075a9a50bbed 100644 --- a/src/cron/service/ops.regression.test.ts +++ b/src/cron/service/ops.regression.test.ts @@ -264,6 +264,41 @@ describe("cron service ops regressions", () => { expect((staleExecuted?.state.nextRunAtMs ?? 0) > nowMs).toBe(true); }); + it("passes the rehydrated agentTurn payload message to isolated manual runs", async () => { + const store = opsRegressionFixtures.makeStorePath(); + const nowMs = Date.now(); + const marker = + "SERIALIZATION_PROBE: reply exactly with the marker token you received and nothing else."; + const job = createIsolatedRegressionJob({ + id: "manual-payload-message", + name: "manual payload message", + scheduledAt: nowMs, + schedule: { kind: "at", at: new Date(nowMs + 3_600_000).toISOString() }, + payload: { kind: "agentTurn", message: marker }, + state: { nextRunAtMs: nowMs + 3_600_000 }, + }); + await saveCronStore(store.storePath, { version: 1, jobs: [job] }); + + const runIsolatedAgentJob = vi.fn().mockResolvedValue({ status: "ok", summary: "ok" }); + const state = createCronServiceState({ + cronEnabled: false, + storePath: store.storePath, + log: noopLogger, + enqueueSystemEvent: vi.fn(), + requestHeartbeat: vi.fn(), + runIsolatedAgentJob, + }); + + const runResult = await run(state, job.id, "force"); + + expect(runResult).toEqual({ ok: true, ran: true }); + expect(runIsolatedAgentJob).toHaveBeenCalledOnce(); + const [params] = requireMockCall(runIsolatedAgentJob, 0, "runIsolatedAgentJob") as [ + { message?: unknown }?, + ]; + expect(params?.message).toBe(marker); + }); + it("applies timeoutSeconds to manual cron.run isolated executions", async () => { vi.useFakeTimers(); try {