@@ -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" ;
@@ -155,6 +156,7 @@ export type HeartbeatDeps = OutboundSendDeps &
155156 getCommandLaneSnapshots ?: ( ) => readonly CommandLaneSnapshot [ ] ;
156157 isReplyRunActive ?: ( sessionKey : string ) => boolean ;
157158 listActiveReplyRunSessionKeys ?: ( ) => readonly string [ ] ;
159+ listActiveEmbeddedRunSessionKeys ?: ( ) => readonly string [ ] ;
158160 nowMs ?: ( ) => number ;
159161 } ;
160162
@@ -229,17 +231,22 @@ function hasAgentOptInBusyLaneWork(
229231 return hasQueuedWorkInLaneSnapshots ( getSnapshots ( ) , ( lane ) => laneBelongsToAgent ( lane , agentId ) ) ;
230232}
231233
232- function hasActiveReplyRunForAgent (
233- agentId : string ,
234- listSessionKeys : ( ) => readonly string [ ] ,
235- ) : boolean {
234+ function hasActiveRunForAgent ( agentId : string , listSessionKeys : ( ) => readonly string [ ] ) : boolean {
236235 const normalizedAgentId = normalizeAgentId ( agentId ) ;
237236 return listSessionKeys ( ) . some ( ( sessionKey ) => {
238237 const parsed = parseAgentSessionKey ( sessionKey ) ;
239238 return parsed ? normalizeAgentId ( parsed . agentId ) === normalizedAgentId : false ;
240239 } ) ;
241240}
242241
242+ function hasActiveRunForSession (
243+ sessionKey : string ,
244+ listSessionKeys : ( ) => readonly string [ ] ,
245+ ) : boolean {
246+ const normalizedSessionKey = sessionKey . trim ( ) ;
247+ return Boolean ( normalizedSessionKey ) && listSessionKeys ( ) . includes ( normalizedSessionKey ) ;
248+ }
249+
243250function resolveHeartbeatChannelPlugin ( channel : string ) : ChannelPlugin | undefined {
244251 const activePlugin = getActivePluginChannelRegistry ( ) ?. channels . find (
245252 ( entry ) => entry . plugin . id === channel ,
@@ -1358,10 +1365,16 @@ export async function runHeartbeatOnce(opts: {
13581365 const shouldHonorActiveReplyRuns = opts . intent !== "immediate" && opts . intent !== "manual" ;
13591366 const listActiveReplyRuns =
13601367 opts . deps ?. listActiveReplyRunSessionKeys ?? listActiveReplyRunSessionKeys ;
1368+ const listActiveEmbeddedRuns =
1369+ opts . deps ?. listActiveEmbeddedRunSessionKeys ?? listActiveEmbeddedRunSessionKeys ;
13611370 // Scheduled heartbeats are background work, so defer them when any session on
13621371 // the same agent is already replying; immediate/manual wakes keep their
13631372 // existing semantics for explicit user/system actions.
1364- if ( shouldHonorActiveReplyRuns && hasActiveReplyRunForAgent ( agentId , listActiveReplyRuns ) ) {
1373+ if (
1374+ shouldHonorActiveReplyRuns &&
1375+ ( hasActiveRunForAgent ( agentId , listActiveReplyRuns ) ||
1376+ hasActiveRunForAgent ( agentId , listActiveEmbeddedRuns ) )
1377+ ) {
13651378 emitHeartbeatEvent ( {
13661379 status : "skipped" ,
13671380 reason : HEARTBEAT_SKIP_REQUESTS_IN_FLIGHT ,
@@ -1417,7 +1430,7 @@ export async function runHeartbeatOnce(opts: {
14171430 const { entry, sessionKey, storePath, suppressOriginatingContext } = preflight . session ;
14181431 const isReplyRunActive =
14191432 opts . deps ?. isReplyRunActive ?? ( ( key : string ) => replyRunRegistry . isActive ( key ) ) ;
1420- if ( isReplyRunActive ( sessionKey ) ) {
1433+ if ( isReplyRunActive ( sessionKey ) || hasActiveRunForSession ( sessionKey , listActiveEmbeddedRuns ) ) {
14211434 emitHeartbeatEvent ( {
14221435 status : "skipped" ,
14231436 reason : HEARTBEAT_SKIP_REQUESTS_IN_FLIGHT ,
@@ -1576,7 +1589,10 @@ export async function runHeartbeatOnce(opts: {
15761589 isolatedSessionKey,
15771590 isolatedBaseSessionKey,
15781591 } ) ;
1579- if ( isReplyRunActive ( isolatedSessionKey ) ) {
1592+ if (
1593+ isReplyRunActive ( isolatedSessionKey ) ||
1594+ hasActiveRunForSession ( isolatedSessionKey , listActiveEmbeddedRuns )
1595+ ) {
15801596 emitHeartbeatEvent ( {
15811597 status : "skipped" ,
15821598 reason : HEARTBEAT_SKIP_REQUESTS_IN_FLIGHT ,
@@ -1800,6 +1816,19 @@ export async function runHeartbeatOnce(opts: {
18001816 const replyResult = await getReplyFromConfig ( ctx , replyOpts , cfg ) ;
18011817 const heartbeatToolResponse = resolveHeartbeatToolResponseFromReplyResult ( replyResult ) ;
18021818 const replyPayload = resolveHeartbeatReplyPayload ( replyResult ) ;
1819+ if (
1820+ ! heartbeatToolResponse &&
1821+ ( ! replyPayload || ! hasOutboundReplyContent ( replyPayload ) ) &&
1822+ ( isReplyRunActive ( runSessionKey ) ||
1823+ hasActiveRunForSession ( runSessionKey , listActiveEmbeddedRuns ) )
1824+ ) {
1825+ emitHeartbeatEvent ( {
1826+ status : "skipped" ,
1827+ reason : HEARTBEAT_SKIP_REQUESTS_IN_FLIGHT ,
1828+ durationMs : Date . now ( ) - startedAt ,
1829+ } ) ;
1830+ return { status : "skipped" , reason : HEARTBEAT_SKIP_REQUESTS_IN_FLIGHT } ;
1831+ }
18031832 const includeReasoning = heartbeat ?. includeReasoning === true ;
18041833 const reasoningPayloads = includeReasoning
18051834 ? resolveHeartbeatReasoningPayloads ( replyResult ) . filter ( ( payload ) => payload !== replyPayload )
0 commit comments