
























@@ -2171,6 +2171,20 @@ export function startHeartbeatRunner(opts: {
21712171agent.nextDueMs = seekActiveSlotForAgent(agent, rawDueMs);
21722172};
217321732174+const advanceStaleScheduleAfterDeferral = (
2175+agent: HeartbeatAgentState,
2176+now: number,
2177+reason?: string,
2178+decision?: DeferDecision,
2179+) => {
2180+if (!decision?.defer || decision.reason === "not-due" || agent.nextDueMs > now) {
2181+return;
2182+}
2183+// Deferrals that do not have wake-layer retry ownership still need to move
2184+// the due slot forward; otherwise scheduleNext() will keep rearming at 0ms.
2185+advanceAgentSchedule(agent, now, reason);
2186+};
2187+21742188// Centralized cooldown gate. Both targeted and broadcast dispatch branches
21752189// call this before invoking `runOnce`. Manual wakes are never deferred.
21762190// Everything else respects `nextDueMs`, the min-spacing floor, and the flood
@@ -2366,6 +2380,7 @@ export function startHeartbeatRunner(opts: {
23662380}
23672381const deferral = evaluateWakeDeferral(targetAgent, now, reason, intent);
23682382if (deferral.defer) {
2383+advanceStaleScheduleAfterDeferral(targetAgent, now, reason, deferral);
23692384return { status: "skipped", reason: deferral.reason };
23702385}
23712386try {
@@ -2395,11 +2410,10 @@ export function startHeartbeatRunner(opts: {
23952410return res;
23962411}
23972412// Non-retryable outcome (ran, disabled, failed-but-not-busy). Record
2398-// bookkeeping so subsequent wakes within the cooldown window defer.
2413+// bookkeeping and move the due slot so scheduleNext() cannot hot-loop
2414+// on a stale past-due agent.
23992415recordRunBookkeeping(targetAgent, now);
2400-if (res.status !== "skipped" || res.reason !== "disabled") {
2401-advanceAgentSchedule(targetAgent, now, reason);
2402-}
2416+advanceAgentSchedule(targetAgent, now, reason);
24032417return res.status === "ran" ? { status: "ran", durationMs: Date.now() - startedAt } : res;
24042418} catch (err) {
24052419const errMsg = formatErrorMessage(err);
@@ -2428,6 +2442,7 @@ export function startHeartbeatRunner(opts: {
24282442const runOneAgent = async (agent: HeartbeatAgentState): Promise<AgentWakeOutcome> => {
24292443const deferral = evaluateWakeDeferral(agent, now, reason, intent);
24302444if (deferral.defer) {
2445+advanceStaleScheduleAfterDeferral(agent, now, reason, deferral);
24312446return { ran: false };
24322447}
24332448@@ -2462,9 +2477,7 @@ export function startHeartbeatRunner(opts: {
24622477}
24632478// Non-retryable outcome — record bookkeeping for cooldown gates.
24642479recordRunBookkeeping(agent, now);
2465-if (res.status !== "skipped" || res.reason !== "disabled") {
2466-advanceAgentSchedule(agent, now, reason);
2467-}
2480+advanceAgentSchedule(agent, now, reason);
24682481let agentRan = res.status === "ran";
2469248224702483const defaultSessionKey = resolveHeartbeatSession(
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。