





























@@ -20,6 +20,7 @@ import {
2020} from "../agents/agent-scope.js";
2121import { appendCronStyleCurrentTimeLine } from "../agents/current-time.js";
2222import { resolveEmbeddedSessionLane } from "../agents/embedded-agent-runner/lanes.js";
23+import { listActiveEmbeddedRunSessionKeys } from "../agents/embedded-agent-runner/run-state.js";
2324import { formatReasoningMessage } from "../agents/embedded-agent-utils.js";
2425import { resolveAgentHarnessPolicy } from "../agents/harness/policy.js";
2526import { resolveModelRefFromString, type ModelRef } from "../agents/model-selection.js";
@@ -43,6 +44,10 @@ import {
4344} from "../auto-reply/heartbeat.js";
4445import { replaceGenericExternalRunFailureText } from "../auto-reply/reply/agent-runner-failure-copy.js";
4546import { 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";
4651import {
4752listActiveReplyRunSessionKeys,
4853replyRunRegistry,
@@ -155,6 +160,7 @@ export type HeartbeatDeps = OutboundSendDeps &
155160getCommandLaneSnapshots?: () => readonly CommandLaneSnapshot[];
156161isReplyRunActive?: (sessionKey: string) => boolean;
157162listActiveReplyRunSessionKeys?: () => readonly string[];
163+listActiveEmbeddedRunSessionKeys?: () => readonly string[];
158164nowMs?: () => number;
159165};
160166@@ -229,17 +235,22 @@ function hasAgentOptInBusyLaneWork(
229235return 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 {
236239const normalizedAgentId = normalizeAgentId(agentId);
237240return listSessionKeys().some((sessionKey) => {
238241const parsed = parseAgentSessionKey(sessionKey);
239242return 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+243254function resolveHeartbeatChannelPlugin(channel: string): ChannelPlugin | undefined {
244255const activePlugin = getActivePluginChannelRegistry()?.channels.find(
245256(entry) => entry.plugin.id === channel,
@@ -1358,10 +1369,16 @@ export async function runHeartbeatOnce(opts: {
13581369const shouldHonorActiveReplyRuns = opts.intent !== "immediate" && opts.intent !== "manual";
13591370const listActiveReplyRuns =
13601371opts.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+) {
13651382emitHeartbeatEvent({
13661383status: "skipped",
13671384reason: HEARTBEAT_SKIP_REQUESTS_IN_FLIGHT,
@@ -1417,7 +1434,7 @@ export async function runHeartbeatOnce(opts: {
14171434const { entry, sessionKey, storePath, suppressOriginatingContext } = preflight.session;
14181435const isReplyRunActive =
14191436opts.deps?.isReplyRunActive ?? ((key: string) => replyRunRegistry.isActive(key));
1420-if (isReplyRunActive(sessionKey)) {
1437+if (isReplyRunActive(sessionKey) || hasActiveRunForSession(sessionKey, listActiveEmbeddedRuns)) {
14211438emitHeartbeatEvent({
14221439status: "skipped",
14231440reason: 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+) {
15801600emitHeartbeatEvent({
15811601status: "skipped",
15821602reason: HEARTBEAT_SKIP_REQUESTS_IN_FLIGHT,
@@ -1781,8 +1801,10 @@ export async function runHeartbeatOnce(opts: {
17811801const timeoutOverrideSeconds = resolveHeartbeatTimeoutOverrideSeconds(cfg, heartbeat);
17821802const bootstrapContextMode: "lightweight" | undefined =
17831803heartbeat?.lightContext === true ? "lightweight" : undefined;
1804+const replyOperationRunState: ReplyOperationRunState = {};
17841805const replyOpts = {
17851806isHeartbeat: 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: {
18001822const replyResult = await getReplyFromConfig(ctx, replyOpts, cfg);
18011823const heartbeatToolResponse = resolveHeartbeatToolResponseFromReplyResult(replyResult);
18021824const 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+}
18031838const includeReasoning = heartbeat?.includeReasoning === true;
18041839const reasoningPayloads = includeReasoning
18051840 ? resolveHeartbeatReasoningPayloads(replyResult).filter((payload) => payload !== replyPayload)
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。