feat: recover stale running agent runs

This commit is contained in:
Your Name
2026-07-02 12:12:41 +08:00
parent af5396c127
commit c27bf04a71
7 changed files with 334 additions and 15 deletions
+81
View File
@@ -493,6 +493,17 @@ export function createAgentRunGateway({
for (const row of rows) {
statusCounts[row.status] = Number(row.count ?? 0);
}
const [oldestRunningRows] = await pool.query(
`SELECT MIN(started_at) AS oldest_started_at
FROM h5_agent_runs
WHERE status = 'running'`,
);
const oldestRunningStartedAt = oldestRunningRows[0]?.oldest_started_at == null
? null
: Number(oldestRunningRows[0].oldest_started_at);
const oldestRunningAgeMs = oldestRunningStartedAt == null
? 0
: Math.max(0, nowMs() - oldestRunningStartedAt);
return {
autoDispatch,
maxConcurrentRuns,
@@ -500,18 +511,86 @@ export function createAgentRunGateway({
inFlight: inFlight.size,
pendingDispatches: queuedDispatches.length,
statusCounts,
oldestRunningStartedAt,
oldestRunningAgeMs,
terminalStatuses: [...TERMINAL_STATUSES],
toolGateway: toolGateway?.getStatus ? toolGateway.getStatus() : null,
};
}
async function recoverStaleRunningRuns({
staleMs = runTimeoutMs,
limit = maxConcurrentRuns,
dryRun = true,
reason = 'stale_running_timeout',
} = {}) {
const normalizedStaleMs = positiveInteger(staleMs, runTimeoutMs);
const normalizedLimit = positiveInteger(limit, maxConcurrentRuns);
const cutoff = nowMs() - normalizedStaleMs;
const [rows] = await pool.query(
`SELECT id, request_id, started_at, updated_at, attempts
FROM h5_agent_runs
WHERE status = 'running' AND started_at IS NOT NULL AND started_at <= ?
ORDER BY started_at ASC
LIMIT ?`,
[cutoff, normalizedLimit],
);
const recovered = [];
for (const row of rows) {
const ageMs = Math.max(0, nowMs() - Number(row.started_at ?? 0));
const item = {
id: row.id,
requestId: row.request_id,
startedAt: Number(row.started_at ?? 0),
updatedAt: Number(row.updated_at ?? 0),
attempts: Number(row.attempts ?? 0),
ageMs,
reason,
};
if (!dryRun) {
const message = `agent run recovered from stale running state after ${ageMs}ms`;
const completedAt = nowMs();
const [update] = await pool.query(
`UPDATE h5_agent_runs
SET status = 'failed', error_message = ?, updated_at = ?, completed_at = ?
WHERE id = ? AND status = 'running' AND started_at <= ?`,
[message, completedAt, completedAt, row.id, cutoff],
);
if (Number(update?.affectedRows ?? 0) === 0) continue;
await appendEvent(row.id, 'stale_recovered', {
reason,
staleMs: normalizedStaleMs,
ageMs,
status: 'failed',
error: message,
});
}
recovered.push(item);
}
return {
dryRun,
staleMs: normalizedStaleMs,
cutoff,
considered: rows.length,
recovered: dryRun ? 0 : recovered.length,
runs: recovered,
};
}
async function dispatchQueuedRuns({ limit = maxConcurrentRuns } = {}) {
const staleRecovery = await recoverStaleRunningRuns({
staleMs: runTimeoutMs,
limit: Math.max(maxConcurrentRuns, Number(limit) || maxConcurrentRuns),
dryRun: false,
reason: 'worker_dispatch_stale_recovery',
});
const dispatchLimit = Math.max(1, Number(limit) || maxConcurrentRuns);
const available = Math.max(0, maxConcurrentRuns - inFlight.size - queuedDispatches.length);
if (available <= 0) {
return {
considered: 0,
dispatched: 0,
staleRecovery,
queue: await getQueueStatus(),
};
}
@@ -530,6 +609,7 @@ export function createAgentRunGateway({
return {
considered: rows.length,
dispatched: rows.length,
staleRecovery,
queue: await getQueueStatus(),
};
}
@@ -540,5 +620,6 @@ export function createAgentRunGateway({
dispatchRun,
getQueueStatus,
dispatchQueuedRuns,
recoverStaleRunningRuns,
};
}