Skip to content

Commit 4780546

Browse files
849261680clawsweeper[bot]Takhoffman
authored
fix(cron): preserve isolated agent turn payload message (#91230)
Summary: - The PR changes isolated cron agent prompt construction to read agentTurn text from `job.payload.message` and adds regression coverage for malformed dispatch messages plus SQLite-rehydrated manual runs. - PR surface: Source +8, Tests +60. Total +68 across 3 files. - Reproducibility: yes. source-level: current main interpolates `input.message` into the isolated cron prompt, ... release report supplies operator repro evidence; I did not run it locally because this review is read-only. Automerge notes: - PR branch already contained follow-up commit before automerge: fix(cron): preserve isolated agent turn payload message Validation: - ClawSweeper review passed for head 4d33607. - Required merge gates passed before the squash merge. Prepared head SHA: 4d33607 Review: #91230 (comment) Co-authored-by: 宇宙熊Yzx <53250620+849261680@users.noreply.github.com> Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com> Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com> Approved-by: takhoffman Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
1 parent 766c5b3 commit 4780546

3 files changed

Lines changed: 71 additions & 3 deletions

File tree

src/cron/isolated-agent/run.payload-fallbacks.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,31 @@ function requireModelFallbackRequest(): {
3939
describe("runCronIsolatedAgentTurn — payload.fallbacks", () => {
4040
setupRunCronIsolatedAgentTurnSuite({ fast: true });
4141

42+
it("uses the persisted agentTurn payload message when the dispatch message is malformed", async () => {
43+
mockRunCronFallbackPassthrough();
44+
const dispatchMessage = "SERIALIZATION_PROBE should not be wrapped";
45+
46+
const result = await runCronIsolatedAgentTurn(
47+
makeIsolatedAgentTurnParams({
48+
job: makeIsolatedAgentTurnJob({
49+
payload: {
50+
kind: "agentTurn",
51+
message:
52+
"SERIALIZATION_PROBE: reply exactly with the marker token you received and nothing else.",
53+
},
54+
}),
55+
message: { message: dispatchMessage } as unknown as string,
56+
}),
57+
);
58+
59+
expect(result.status).toBe("ok");
60+
expect(runEmbeddedAgentMock).toHaveBeenCalledOnce();
61+
const request = runEmbeddedAgentMock.mock.calls[0]?.[0] as { prompt?: unknown } | undefined;
62+
expect(request?.prompt).toContain("SERIALIZATION_PROBE: reply exactly");
63+
expect(request?.prompt).not.toContain(dispatchMessage);
64+
expect(request?.prompt).not.toContain("[object Object]");
65+
});
66+
4267
it.each([
4368
{
4469
name: "passes payload.fallbacks as fallbacksOverride when defined",

src/cron/isolated-agent/run.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,13 @@ type RunCronAgentTurnParams = {
456456
lane?: string;
457457
};
458458

459+
function resolveCronAgentTurnMessage(input: RunCronAgentTurnParams): string {
460+
if (input.job.payload.kind === "agentTurn") {
461+
return input.job.payload.message;
462+
}
463+
return input.message;
464+
}
465+
459466
type WithRunSession = (
460467
result: Omit<RunCronAgentTurnResult, "sessionId" | "sessionKey">,
461468
) => RunCronAgentTurnResult;
@@ -765,7 +772,8 @@ async function prepareCronRunContext(params: {
765772
});
766773

767774
const { formattedTime, timeLine } = resolveCronStyleNow(input.cfg, now);
768-
const base = `[cron:${input.job.id} ${input.job.name}] ${input.message}`.trim();
775+
const message = resolveCronAgentTurnMessage(input);
776+
const base = `[cron:${input.job.id} ${input.job.name}] ${message}`.trim();
769777
const isExternalHook =
770778
hookExternalContentSource !== undefined || isExternalHookSession(baseSessionKey);
771779
const allowUnsafeExternalContent =
@@ -776,7 +784,7 @@ async function prepareCronRunContext(params: {
776784

777785
if (isExternalHook) {
778786
const { detectSuspiciousPatterns } = await loadCronExternalContentRuntime();
779-
const suspiciousPatterns = detectSuspiciousPatterns(input.message);
787+
const suspiciousPatterns = detectSuspiciousPatterns(message);
780788
if (suspiciousPatterns.length > 0) {
781789
logWarn(
782790
`[security] Suspicious patterns detected in external hook content ` +
@@ -789,7 +797,7 @@ async function prepareCronRunContext(params: {
789797
const { buildSafeExternalPrompt } = await loadCronExternalContentRuntime();
790798
const hookType = mapHookExternalContentSource(hookExternalContentSource ?? "webhook");
791799
const safeContent = buildSafeExternalPrompt({
792-
content: input.message,
800+
content: message,
793801
source: hookType,
794802
jobName: input.job.name,
795803
jobId: input.job.id,

src/cron/service/ops.regression.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,41 @@ describe("cron service ops regressions", () => {
264264
expect((staleExecuted?.state.nextRunAtMs ?? 0) > nowMs).toBe(true);
265265
});
266266

267+
it("passes the rehydrated agentTurn payload message to isolated manual runs", async () => {
268+
const store = opsRegressionFixtures.makeStorePath();
269+
const nowMs = Date.now();
270+
const marker =
271+
"SERIALIZATION_PROBE: reply exactly with the marker token you received and nothing else.";
272+
const job = createIsolatedRegressionJob({
273+
id: "manual-payload-message",
274+
name: "manual payload message",
275+
scheduledAt: nowMs,
276+
schedule: { kind: "at", at: new Date(nowMs + 3_600_000).toISOString() },
277+
payload: { kind: "agentTurn", message: marker },
278+
state: { nextRunAtMs: nowMs + 3_600_000 },
279+
});
280+
await saveCronStore(store.storePath, { version: 1, jobs: [job] });
281+
282+
const runIsolatedAgentJob = vi.fn().mockResolvedValue({ status: "ok", summary: "ok" });
283+
const state = createCronServiceState({
284+
cronEnabled: false,
285+
storePath: store.storePath,
286+
log: noopLogger,
287+
enqueueSystemEvent: vi.fn(),
288+
requestHeartbeat: vi.fn(),
289+
runIsolatedAgentJob,
290+
});
291+
292+
const runResult = await run(state, job.id, "force");
293+
294+
expect(runResult).toEqual({ ok: true, ran: true });
295+
expect(runIsolatedAgentJob).toHaveBeenCalledOnce();
296+
const [params] = requireMockCall(runIsolatedAgentJob, 0, "runIsolatedAgentJob") as [
297+
{ message?: unknown }?,
298+
];
299+
expect(params?.message).toBe(marker);
300+
});
301+
267302
it("applies timeoutSeconds to manual cron.run isolated executions", async () => {
268303
vi.useFakeTimers();
269304
try {

0 commit comments

Comments
 (0)