Skip to content

Commit ddd0388

Browse files
fsdwenclaude
andcommitted
fix(cron): always emit before_agent_reply phase for cron triggers (#93530)
When no before_agent_reply hook is registered, the phase notification is gated behind hookRunner.hasHooks(), so the cron pre-execution watchdog never receives the 'execution' stage signal and fires prematurely. Move notifyExecutionPhase('before_agent_reply') outside the hasHooks guard so the watchdog always learns the agent entered execution for cron triggers. Hook execution remains guarded. Also fix the same pattern in the CLI runner. Co-Authored-By: Claude <[email protected]>
1 parent 67b3787 commit ddd0388

3 files changed

Lines changed: 37 additions & 33 deletions

File tree

src/agents/cli-runner.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,14 @@ async function runCliAgentInternal(params: RunCliAgentParams): Promise<EmbeddedA
378378
if (params.trigger === "cron") {
379379
const startedAt = Date.now();
380380
const hookRunner = getGlobalHookRunner();
381+
// Always notify the watchdog that the CLI agent reached the execution
382+
// phase, even when no before_agent_reply hooks are registered, so the
383+
// pre-execution timeout is always cleared for cron triggers.
384+
params.onExecutionPhase?.({
385+
phase: "before_agent_reply",
386+
provider: params.provider,
387+
model: params.model ?? "",
388+
});
381389
if (hookRunner?.hasHooks("before_agent_reply")) {
382390
const hookContext = {
383391
runId: params.runId,
@@ -389,11 +397,6 @@ async function runCliAgentInternal(params: RunCliAgentParams): Promise<EmbeddedA
389397
trigger: params.trigger,
390398
...buildAgentHookContextChannelFields(params),
391399
} as const;
392-
params.onExecutionPhase?.({
393-
phase: "before_agent_reply",
394-
provider: params.provider,
395-
model: params.model ?? "",
396-
});
397400
const hookResult = await hookRunner.runBeforeAgentReply(
398401
{ cleanedBody: params.prompt },
399402
hookContext,
@@ -417,12 +420,12 @@ async function runCliAgentInternal(params: RunCliAgentParams): Promise<EmbeddedA
417420
},
418421
};
419422
}
420-
params.onExecutionPhase?.({
421-
phase: "runtime_plugins",
422-
provider: params.provider,
423-
model: params.model ?? "",
424-
});
425423
}
424+
params.onExecutionPhase?.({
425+
phase: "runtime_plugins",
426+
provider: params.provider,
427+
model: params.model ?? "",
428+
});
426429
}
427430
const { prepareCliRunContext } = await import("./cli-runner/prepare.runtime.js");
428431
const context = await prepareCliRunContext(params);

src/agents/embedded-agent-runner/run.ts

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -834,26 +834,31 @@ async function runEmbeddedAgentInternal(
834834
trigger: params.trigger,
835835
...buildAgentHookContextChannelFields(params),
836836
};
837-
if (params.trigger === "cron" && hookRunner?.hasHooks("before_agent_reply")) {
837+
if (params.trigger === "cron") {
838+
// Notify the watchdog that the agent reached the execution phase
839+
// even when no before_agent_reply hooks are registered, so the
840+
// pre-execution timeout is always cleared for cron triggers.
838841
notifyExecutionPhase("before_agent_reply", { provider, model: modelId });
839-
const hookResult = await hookRunner.runBeforeAgentReply(
840-
{ cleanedBody: params.prompt },
841-
hookCtx,
842-
);
843-
if (hookResult?.handled) {
844-
return {
845-
payloads: buildHandledReplyPayloads(hookResult.reply),
846-
meta: {
847-
durationMs: Date.now() - started,
848-
agentMeta: {
849-
sessionId: params.sessionId,
850-
provider,
851-
model: modelId,
842+
if (hookRunner?.hasHooks("before_agent_reply")) {
843+
const hookResult = await hookRunner.runBeforeAgentReply(
844+
{ cleanedBody: params.prompt },
845+
hookCtx,
846+
);
847+
if (hookResult?.handled) {
848+
return {
849+
payloads: buildHandledReplyPayloads(hookResult.reply),
850+
meta: {
851+
durationMs: Date.now() - started,
852+
agentMeta: {
853+
sessionId: params.sessionId,
854+
provider,
855+
model: modelId,
856+
},
857+
finalAssistantVisibleText: hookResult.reply?.text ?? SILENT_REPLY_TOKEN,
858+
finalAssistantRawText: hookResult.reply?.text ?? SILENT_REPLY_TOKEN,
852859
},
853-
finalAssistantVisibleText: hookResult.reply?.text ?? SILENT_REPLY_TOKEN,
854-
finalAssistantRawText: hookResult.reply?.text ?? SILENT_REPLY_TOKEN,
855-
},
856-
};
860+
};
861+
}
857862
}
858863
notifyExecutionPhase("runtime_plugins", { provider, model: modelId });
859864
}

src/cron/service/agent-watchdog.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -129,11 +129,7 @@ export function createCronAgentWatchdog(params: {
129129
startPreExecutionTimeout();
130130
return;
131131
}
132-
// Any phase notification proves the runner is alive and making progress.
133-
// Clear the pre-execution timeout even for pre_execution stages so that
134-
// conditionally-emitted phases (e.g. before_agent_reply gated behind
135-
// hookRunner.hasHooks) do not cause false timeouts.
136-
if (stage !== undefined || info.firstModelCallStarted) {
132+
if (stage === "execution" || info.firstModelCallStarted) {
137133
state = "executing";
138134
clearPreExecutionTimeout();
139135
}

0 commit comments

Comments
 (0)