惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

V
Visual Studio Blog
Engineering at Meta
Engineering at Meta
月光博客
月光博客
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
Tailwind CSS Blog
博客园 - Franky
The GitHub Blog
The GitHub Blog
大猫的无限游戏
大猫的无限游戏
The Cloudflare Blog
B
Blog RSS Feed
云风的 BLOG
云风的 BLOG
小众软件
小众软件
罗磊的独立博客
Microsoft Azure Blog
Microsoft Azure Blog
I
InfoQ
美团技术团队
H
Hackread – Cybersecurity News, Data Breaches, AI and More
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
V
V2EX
C
Check Point Blog
WordPress大学
WordPress大学
博客园 - 【当耐特】
博客园 - 司徒正美
D
Docker

Recent Commits to openclaw:main

test: merge chat side-result checks · openclaw/openclaw@ddd2c2a test: merge cron history checks · openclaw/openclaw@f7eb746 test: merge responsive navigation shell checks · openclaw/openclaw@c2e4b47 docs(changelog): add codex oauth fixes · openclaw/openclaw@628e6cd test: merge navigation routing cases · openclaw/openclaw@5d8cecb Tests: mock channel registry bundled fallback · openclaw/openclaw@2b08233 Secrets: avoid broad web search discovery for single plugin config · openclaw/openclaw@a464f59 test: merge config view browser checks · openclaw/openclaw@20cf511 fix(status): align oauth health with runtime · openclaw/openclaw@eed7116 feat: add macOS screen snapshots for monitor preview (#67954) thanks … · openclaw/openclaw@f377db1 fix: report shared auth scopes in hello-ok (#67810) thanks @BunsDev · openclaw/openclaw@0b6c39b Auto-reply: avoid eager bundled route fallback · openclaw/openclaw@3ea1bf4 Tests: narrow session binding contract setup · openclaw/openclaw@54e4e16 fix(macOS): enable undo/redo in webchat composer text input (#34962) · openclaw/openclaw@00951dc Tests: speed up channel setup promotion · openclaw/openclaw@82b529a Docs: refresh agent instructions · openclaw/openclaw@5775fe2 fix(auth): serialize OAuth refresh across agents to fix #26322 (#67876) · openclaw/openclaw@8e79080 test: allow ollama public surface boundary test · openclaw/openclaw@7d4f1a6 Docs: add test performance guardrails · openclaw/openclaw@89706d3 Tests: restore context-engine usage proof · openclaw/openclaw@e4c4f95 Tests: slim context engine runtime coverage · openclaw/openclaw@74c198f ci: retry failed custom checkouts · openclaw/openclaw@0ee5baf test: trim duplicate provider auth onboarding cases · openclaw/openclaw@1ffc02e matrix: fix sessions_spawn --thread subagent session spawning (#67643) · openclaw/openclaw@1ce2596 test: reduce auth choice fixture churn · openclaw/openclaw@857b9cd test: mock health status config boundaries · openclaw/openclaw@9d5ab4a test: mock onboard config io boundary · openclaw/openclaw@299694d test: mock legacy state plugin boundaries · openclaw/openclaw@2713089 test: mock channel install boundaries · openclaw/openclaw@b945248 test: mock doctor preview channel boundaries · openclaw/openclaw@b1a3ad4
refactor(cron): centralize agent phase watchdog state · o...
steipete · 2026-05-15 · via Recent Commits to openclaw:main

@@ -1,4 +1,5 @@

11

import { resolveFailoverReasonFromError } from "../../agents/failover-error.js";

2+

import { formatEmbeddedAgentExecutionPhase } from "../../agents/pi-embedded-runner/execution-phase.js";

23

import type { CronConfig, CronRetryOn } from "../../config/types.cron.js";

34

import type { HeartbeatRunResult } from "../../infra/heartbeat-wake.js";

45

import {

@@ -109,6 +110,39 @@ type StartupCatchupPlan = {

109110

deferredJobs: StartupDeferredJob[];

110111

};

111112113+

type CronAgentWatchdogState =

114+

| "waiting_for_runner"

115+

| "waiting_for_execution"

116+

| "executing"

117+

| "timed_out"

118+

| "disposed";

119+120+

type CronAgentPhaseWatchdogStage = "pre_execution" | "execution";

121+122+

const CRON_AGENT_PHASE_WATCHDOG_STAGE = {

123+

runner_entered: "pre_execution",

124+

workspace: "pre_execution",

125+

runtime_plugins: "pre_execution",

126+

model_resolution: "pre_execution",

127+

auth: "pre_execution",

128+

context_engine: "pre_execution",

129+

attempt_dispatch: "execution",

130+

context_assembled: "execution",

131+

turn_accepted: "execution",

132+

process_spawned: "execution",

133+

tool_execution_started: "execution",

134+

assistant_output_started: "execution",

135+

model_call_started: "execution",

136+

} as const satisfies Record<CronAgentExecutionPhase, CronAgentPhaseWatchdogStage>;

137+138+

type CronAgentWatchdog = {

139+

start: () => void;

140+

noteRunnerStarted: (info?: CronAgentExecutionStarted) => void;

141+

notePhase: (info: CronAgentExecutionPhaseUpdate) => void;

142+

activeExecution: () => CronAgentExecutionStarted | undefined;

143+

dispose: () => void;

144+

};

145+112146

export async function executeJobCoreWithTimeout(

113147

state: CronServiceState,

114148

job: CronJob,

@@ -119,12 +153,6 @@ export async function executeJobCoreWithTimeout(

119153

}

120154121155

const runAbortController = new AbortController();

122-

let timeoutId: NodeJS.Timeout | undefined;

123-

let setupTimeoutId: NodeJS.Timeout | undefined;

124-

let preExecutionTimeoutId: NodeJS.Timeout | undefined;

125-

let activeExecution: CronAgentExecutionStarted | undefined;

126-

let runnerStarted = false;

127-

let executionStarted = false;

128156

let timeoutReason: string | undefined;

129157

const timeoutMarker = Symbol("cron-timeout");

130158

let resolveTimeout: ((value: typeof timeoutMarker) => void) | undefined;

@@ -142,75 +170,16 @@ export async function executeJobCoreWithTimeout(

142170

runAbortController.abort(reason);

143171

resolveTimeout?.(timeoutMarker);

144172

};

145-

const startTimeout = () => {

146-

if (!timeoutId) {

147-

timeoutId = setTimeout(() => {

148-

triggerTimeout(timeoutErrorMessage(activeExecution));

149-

}, jobTimeoutMs);

150-

}

151-

};

152-

const startSetupTimeout = () => {

153-

if (setupTimeoutId || runnerStarted) {

154-

return;

155-

}

156-

setupTimeoutId = setTimeout(() => {

157-

if (!runnerStarted) {

158-

triggerTimeout(setupTimeoutErrorMessage(activeExecution));

159-

}

160-

}, CRON_AGENT_SETUP_WATCHDOG_MS);

161-

};

162-

const clearSetupTimeout = () => {

163-

if (!setupTimeoutId) {

164-

return;

165-

}

166-

clearTimeout(setupTimeoutId);

167-

setupTimeoutId = undefined;

168-

};

169-

const startPreExecutionTimeout = () => {

170-

if (preExecutionTimeoutId || executionStarted) {

171-

return;

172-

}

173-

preExecutionTimeoutId = setTimeout(() => {

174-

if (!executionStarted) {

175-

triggerTimeout(preExecutionTimeoutErrorMessage(activeExecution));

176-

}

177-

}, resolveCronAgentPreExecutionWatchdogMs(jobTimeoutMs));

178-

};

179-

const clearPreExecutionTimeout = () => {

180-

if (!preExecutionTimeoutId) {

181-

return;

182-

}

183-

clearTimeout(preExecutionTimeoutId);

184-

preExecutionTimeoutId = undefined;

185-

};

186-

const noteExecutionProgress = (info?: CronAgentExecutionStarted) => {

187-

if (info) {

188-

activeExecution = { ...activeExecution, ...info };

189-

if (isCronAgentExecutionStarted(info)) {

190-

executionStarted = true;

191-

clearPreExecutionTimeout();

192-

}

193-

}

194-

};

195-

const onExecutionStarted = (info?: CronAgentExecutionStarted) => {

196-

runnerStarted = true;

197-

noteExecutionProgress(info);

198-

clearSetupTimeout();

199-

startTimeout();

200-

startPreExecutionTimeout();

201-

};

202-

const onExecutionPhase = (info: CronAgentExecutionPhaseUpdate) => {

203-

noteExecutionProgress(info);

204-

};

173+

const watchdog = createCronAgentWatchdog({

174+

deferUntilRunner: deferTimeoutUntilExecutionStart,

175+

jobTimeoutMs,

176+

triggerTimeout,

177+

});

205178

const corePromise = executeJobCore(state, job, runAbortController.signal, {

206-

onExecutionStarted: deferTimeoutUntilExecutionStart ? onExecutionStarted : undefined,

207-

onExecutionPhase: deferTimeoutUntilExecutionStart ? onExecutionPhase : undefined,

179+

onExecutionStarted: deferTimeoutUntilExecutionStart ? watchdog.noteRunnerStarted : undefined,

180+

onExecutionPhase: deferTimeoutUntilExecutionStart ? watchdog.notePhase : undefined,

208181

});

209-

if (!deferTimeoutUntilExecutionStart) {

210-

startTimeout();

211-

} else {

212-

startSetupTimeout();

213-

}

182+

watchdog.start();

214183

void corePromise.catch((err) => {

215184

if (runAbortController.signal.aborted) {

216185

state.deps.log.warn(

@@ -224,6 +193,7 @@ export async function executeJobCoreWithTimeout(

224193

if (first !== timeoutMarker) {

225194

return first;

226195

}

196+

const activeExecution = watchdog.activeExecution();

227197

await cleanupTimedOutCronAgentRun(state, job, jobTimeoutMs, activeExecution);

228198

const error = timeoutReason ?? timeoutErrorMessage(activeExecution);

229199

return {

@@ -234,14 +204,113 @@ export async function executeJobCoreWithTimeout(

234204

}),

235205

};

236206

} finally {

237-

if (timeoutId) {

238-

clearTimeout(timeoutId);

239-

}

240-

clearSetupTimeout();

241-

clearPreExecutionTimeout();

207+

watchdog.dispose();

242208

}

243209

}

244210211+

function createCronAgentWatchdog(params: {

212+

deferUntilRunner: boolean;

213+

jobTimeoutMs: number;

214+

triggerTimeout: (reason: string) => void;

215+

}): CronAgentWatchdog {

216+

let state: CronAgentWatchdogState = params.deferUntilRunner ? "waiting_for_runner" : "executing";

217+

let timeoutId: NodeJS.Timeout | undefined;

218+

let setupTimeoutId: NodeJS.Timeout | undefined;

219+

let preExecutionTimeoutId: NodeJS.Timeout | undefined;

220+

let activeExecution: CronAgentExecutionStarted | undefined;

221+222+

const setTimedOut = (reason: string) => {

223+

if (state === "timed_out" || state === "disposed") {

224+

return;

225+

}

226+

state = "timed_out";

227+

params.triggerTimeout(reason);

228+

};

229+

const startTimeout = () => {

230+

if (timeoutId || state === "disposed") {

231+

return;

232+

}

233+

timeoutId = setTimeout(() => {

234+

setTimedOut(timeoutErrorMessage(activeExecution));

235+

}, params.jobTimeoutMs);

236+

};

237+

const clearSetupTimeout = () => {

238+

if (!setupTimeoutId) {

239+

return;

240+

}

241+

clearTimeout(setupTimeoutId);

242+

setupTimeoutId = undefined;

243+

};

244+

const clearPreExecutionTimeout = () => {

245+

if (!preExecutionTimeoutId) {

246+

return;

247+

}

248+

clearTimeout(preExecutionTimeoutId);

249+

preExecutionTimeoutId = undefined;

250+

};

251+

const startPreExecutionTimeout = () => {

252+

if (preExecutionTimeoutId || state !== "waiting_for_execution") {

253+

return;

254+

}

255+

preExecutionTimeoutId = setTimeout(() => {

256+

if (state === "waiting_for_execution") {

257+

setTimedOut(preExecutionTimeoutErrorMessage(activeExecution));

258+

}

259+

}, resolveCronAgentPreExecutionWatchdogMs(params.jobTimeoutMs));

260+

};

261+

const noteExecutionProgress = (info?: CronAgentExecutionStarted) => {

262+

if (!info) {

263+

return;

264+

}

265+

activeExecution = { ...activeExecution, ...info };

266+

if (isCronAgentExecutionStarted(info)) {

267+

state = "executing";

268+

clearPreExecutionTimeout();

269+

}

270+

};

271+272+

return {

273+

start: () => {

274+

if (params.deferUntilRunner) {

275+

setupTimeoutId = setTimeout(() => {

276+

if (state === "waiting_for_runner") {

277+

setTimedOut(setupTimeoutErrorMessage(activeExecution));

278+

}

279+

}, CRON_AGENT_SETUP_WATCHDOG_MS);

280+

return;

281+

}

282+

startTimeout();

283+

},

284+

noteRunnerStarted: (info?: CronAgentExecutionStarted) => {

285+

if (state === "disposed" || state === "timed_out") {

286+

return;

287+

}

288+

clearSetupTimeout();

289+

startTimeout();

290+

if (state !== "executing") {

291+

state = "waiting_for_execution";

292+

}

293+

noteExecutionProgress(info);

294+

startPreExecutionTimeout();

295+

},

296+

notePhase: (info: CronAgentExecutionPhaseUpdate) => {

297+

if (state === "disposed" || state === "timed_out") {

298+

return;

299+

}

300+

noteExecutionProgress(info);

301+

},

302+

activeExecution: () => activeExecution,

303+

dispose: () => {

304+

state = "disposed";

305+

if (timeoutId) {

306+

clearTimeout(timeoutId);

307+

}

308+

clearSetupTimeout();

309+

clearPreExecutionTimeout();

310+

},

311+

};

312+

}

313+245314

async function cleanupTimedOutCronAgentRun(

246315

state: CronServiceState,

247316

job: CronJob,

@@ -302,30 +371,14 @@ function preExecutionTimeoutErrorMessage(execution?: CronAgentExecutionStarted):

302371

}

303372304373

function formatCronAgentExecutionPhase(execution?: CronAgentExecutionStarted): string | undefined {

305-

return execution?.phase?.replaceAll("_", "-");

374+

return formatEmbeddedAgentExecutionPhase(execution?.phase);

306375

}

307376308-

const CRON_AGENT_EXECUTION_STARTED_BY_PHASE = {

309-

runner_entered: false,

310-

workspace: false,

311-

runtime_plugins: false,

312-

model_resolution: false,

313-

auth: false,

314-

context_engine: false,

315-

attempt_dispatch: true,

316-

context_assembled: true,

317-

turn_accepted: true,

318-

process_spawned: true,

319-

tool_execution_started: true,

320-

assistant_output_started: true,

321-

model_call_started: true,

322-

} as const satisfies Record<CronAgentExecutionPhase, boolean>;

323-324377

function isCronAgentExecutionStarted(info: CronAgentExecutionStarted): boolean {

325378

if (info.firstModelCallStarted) {

326379

return true;

327380

}

328-

return info.phase ? CRON_AGENT_EXECUTION_STARTED_BY_PHASE[info.phase] : false;

381+

return info.phase ? CRON_AGENT_PHASE_WATCHDOG_STAGE[info.phase] === "execution" : false;

329382

}

330383331384

function resolveCronAgentPreExecutionWatchdogMs(jobTimeoutMs: number): number {