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

推荐订阅源

小众软件
小众软件
WordPress大学
WordPress大学
IT之家
IT之家
G
Google Developers Blog
Vercel News
Vercel News
阮一峰的网络日志
阮一峰的网络日志
博客园 - 三生石上(FineUI控件)
Engineering at Meta
Engineering at Meta
Martin Fowler
Martin Fowler
V
V2EX
爱范儿
爱范儿
Hugging Face - Blog
Hugging Face - Blog
Apple Machine Learning Research
Apple Machine Learning Research
B
Blog
V
Visual Studio Blog
有赞技术团队
有赞技术团队
I
InfoQ
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
月光博客
月光博客
J
Java Code Geeks
Stack Overflow Blog
Stack Overflow Blog
P
Proofpoint News Feed
云风的 BLOG
云风的 BLOG
雷峰网
雷峰网

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
fix #85871: [Bug]: Heartbeat scheduler silently fails to ...
zhangguiping · 2026-06-16 · via Recent Commits to openclaw:main

@@ -20,6 +20,7 @@ import {

2020

} from "../agents/agent-scope.js";

2121

import { appendCronStyleCurrentTimeLine } from "../agents/current-time.js";

2222

import { resolveEmbeddedSessionLane } from "../agents/embedded-agent-runner/lanes.js";

23+

import { listActiveEmbeddedRunSessionKeys } from "../agents/embedded-agent-runner/run-state.js";

2324

import { formatReasoningMessage } from "../agents/embedded-agent-utils.js";

2425

import { resolveAgentHarnessPolicy } from "../agents/harness/policy.js";

2526

import { resolveModelRefFromString, type ModelRef } from "../agents/model-selection.js";

@@ -43,6 +44,10 @@ import {

4344

} from "../auto-reply/heartbeat.js";

4445

import { replaceGenericExternalRunFailureText } from "../auto-reply/reply/agent-runner-failure-copy.js";

4546

import { resolveDefaultModel } from "../auto-reply/reply/directive-handling.defaults.js";

47+

import {

48+

REPLY_OPERATION_RUN_STATE,

49+

type ReplyOperationRunState,

50+

} from "../auto-reply/reply/reply-operation-run-state.js";

4651

import {

4752

listActiveReplyRunSessionKeys,

4853

replyRunRegistry,

@@ -155,6 +160,7 @@ export type HeartbeatDeps = OutboundSendDeps &

155160

getCommandLaneSnapshots?: () => readonly CommandLaneSnapshot[];

156161

isReplyRunActive?: (sessionKey: string) => boolean;

157162

listActiveReplyRunSessionKeys?: () => readonly string[];

163+

listActiveEmbeddedRunSessionKeys?: () => readonly string[];

158164

nowMs?: () => number;

159165

};

160166

@@ -229,17 +235,22 @@ function hasAgentOptInBusyLaneWork(

229235

return hasQueuedWorkInLaneSnapshots(getSnapshots(), (lane) => laneBelongsToAgent(lane, agentId));

230236

}

231237232-

function hasActiveReplyRunForAgent(

233-

agentId: string,

234-

listSessionKeys: () => readonly string[],

235-

): boolean {

238+

function hasActiveRunForAgent(agentId: string, listSessionKeys: () => readonly string[]): boolean {

236239

const normalizedAgentId = normalizeAgentId(agentId);

237240

return listSessionKeys().some((sessionKey) => {

238241

const parsed = parseAgentSessionKey(sessionKey);

239242

return parsed ? normalizeAgentId(parsed.agentId) === normalizedAgentId : false;

240243

});

241244

}

242245246+

function hasActiveRunForSession(

247+

sessionKey: string,

248+

listSessionKeys: () => readonly string[],

249+

): boolean {

250+

const normalizedSessionKey = sessionKey.trim();

251+

return Boolean(normalizedSessionKey) && listSessionKeys().includes(normalizedSessionKey);

252+

}

253+243254

function resolveHeartbeatChannelPlugin(channel: string): ChannelPlugin | undefined {

244255

const activePlugin = getActivePluginChannelRegistry()?.channels.find(

245256

(entry) => entry.plugin.id === channel,

@@ -1358,10 +1369,16 @@ export async function runHeartbeatOnce(opts: {

13581369

const shouldHonorActiveReplyRuns = opts.intent !== "immediate" && opts.intent !== "manual";

13591370

const listActiveReplyRuns =

13601371

opts.deps?.listActiveReplyRunSessionKeys ?? listActiveReplyRunSessionKeys;

1372+

const listActiveEmbeddedRuns =

1373+

opts.deps?.listActiveEmbeddedRunSessionKeys ?? listActiveEmbeddedRunSessionKeys;

13611374

// Scheduled heartbeats are background work, so defer them when any session on

13621375

// the same agent is already replying; immediate/manual wakes keep their

13631376

// existing semantics for explicit user/system actions.

1364-

if (shouldHonorActiveReplyRuns && hasActiveReplyRunForAgent(agentId, listActiveReplyRuns)) {

1377+

if (

1378+

shouldHonorActiveReplyRuns &&

1379+

(hasActiveRunForAgent(agentId, listActiveReplyRuns) ||

1380+

hasActiveRunForAgent(agentId, listActiveEmbeddedRuns))

1381+

) {

13651382

emitHeartbeatEvent({

13661383

status: "skipped",

13671384

reason: HEARTBEAT_SKIP_REQUESTS_IN_FLIGHT,

@@ -1417,7 +1434,7 @@ export async function runHeartbeatOnce(opts: {

14171434

const { entry, sessionKey, storePath, suppressOriginatingContext } = preflight.session;

14181435

const isReplyRunActive =

14191436

opts.deps?.isReplyRunActive ?? ((key: string) => replyRunRegistry.isActive(key));

1420-

if (isReplyRunActive(sessionKey)) {

1437+

if (isReplyRunActive(sessionKey) || hasActiveRunForSession(sessionKey, listActiveEmbeddedRuns)) {

14211438

emitHeartbeatEvent({

14221439

status: "skipped",

14231440

reason: HEARTBEAT_SKIP_REQUESTS_IN_FLIGHT,

@@ -1576,7 +1593,10 @@ export async function runHeartbeatOnce(opts: {

15761593

isolatedSessionKey,

15771594

isolatedBaseSessionKey,

15781595

});

1579-

if (isReplyRunActive(isolatedSessionKey)) {

1596+

if (

1597+

isReplyRunActive(isolatedSessionKey) ||

1598+

hasActiveRunForSession(isolatedSessionKey, listActiveEmbeddedRuns)

1599+

) {

15801600

emitHeartbeatEvent({

15811601

status: "skipped",

15821602

reason: HEARTBEAT_SKIP_REQUESTS_IN_FLIGHT,

@@ -1781,8 +1801,10 @@ export async function runHeartbeatOnce(opts: {

17811801

const timeoutOverrideSeconds = resolveHeartbeatTimeoutOverrideSeconds(cfg, heartbeat);

17821802

const bootstrapContextMode: "lightweight" | undefined =

17831803

heartbeat?.lightContext === true ? "lightweight" : undefined;

1804+

const replyOperationRunState: ReplyOperationRunState = {};

17841805

const replyOpts = {

17851806

isHeartbeat: true,

1807+

[REPLY_OPERATION_RUN_STATE]: replyOperationRunState,

17861808

...(heartbeatModelOverride ? { heartbeatModelOverride } : {}),

17871809

suppressToolErrorWarnings,

17881810

...(usesHeartbeatResponseTool ? { enableHeartbeatTool: true, forceHeartbeatTool: true } : {}),

@@ -1800,6 +1822,19 @@ export async function runHeartbeatOnce(opts: {

18001822

const replyResult = await getReplyFromConfig(ctx, replyOpts, cfg);

18011823

const heartbeatToolResponse = resolveHeartbeatToolResponseFromReplyResult(replyResult);

18021824

const replyPayload = resolveHeartbeatReplyPayload(replyResult);

1825+

if (

1826+

!heartbeatToolResponse &&

1827+

(!replyPayload || !hasOutboundReplyContent(replyPayload)) &&

1828+

replyOperationRunState.admission?.status === "skipped" &&

1829+

replyOperationRunState.admission.reason === "active-run"

1830+

) {

1831+

emitHeartbeatEvent({

1832+

status: "skipped",

1833+

reason: HEARTBEAT_SKIP_REQUESTS_IN_FLIGHT,

1834+

durationMs: Date.now() - startedAt,

1835+

});

1836+

return { status: "skipped", reason: HEARTBEAT_SKIP_REQUESTS_IN_FLIGHT };

1837+

}

18031838

const includeReasoning = heartbeat?.includeReasoning === true;

18041839

const reasoningPayloads = includeReasoning

18051840

? resolveHeartbeatReasoningPayloads(replyResult).filter((payload) => payload !== replyPayload)