Skip to content

Commit e331da8

Browse files
fix heartbeat admission busy retry
1 parent ef2952c commit e331da8

2 files changed

Lines changed: 46 additions & 0 deletions

File tree

src/infra/heartbeat-runner.skips-busy-session-lane.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,39 @@ describe("heartbeat runner skips when target session lane is busy", () => {
360360
});
361361
});
362362

363+
it("returns requests-in-flight when reply admission races after the preflight checks", async () => {
364+
await withTempHeartbeatSandbox(async ({ storePath }) => {
365+
const cfg = createHeartbeatTelegramConfig();
366+
const sessionKey = await seedHeartbeatTelegramSession(storePath, cfg);
367+
let operation: ReturnType<typeof createReplyOperation> | undefined;
368+
const replySpy = vi.fn(async () => {
369+
operation = createReplyOperation({
370+
sessionKey,
371+
sessionId: "racing-visible-session",
372+
resetTriggered: false,
373+
});
374+
operation.setPhase("running");
375+
return undefined;
376+
});
377+
378+
try {
379+
const result = await runHeartbeatOnce({
380+
cfg,
381+
deps: {
382+
getQueueSize: vi.fn((_lane?: string) => 0),
383+
nowMs: () => Date.now(),
384+
getReplyFromConfig: replySpy,
385+
} as HeartbeatDeps,
386+
});
387+
388+
expect(result).toEqual({ status: "skipped", reason: HEARTBEAT_SKIP_REQUESTS_IN_FLIGHT });
389+
expect(replySpy).toHaveBeenCalledOnce();
390+
} finally {
391+
operation?.complete();
392+
}
393+
});
394+
});
395+
363396
it("returns requests-in-flight when an isolated heartbeat reply run is still active", async () => {
364397
await withTempHeartbeatSandbox(async ({ storePath, replySpy }) => {
365398
const cfg = createHeartbeatTelegramConfig();

src/infra/heartbeat-runner.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1813,6 +1813,19 @@ export async function runHeartbeatOnce(opts: {
18131813
const replyResult = await getReplyFromConfig(ctx, replyOpts, cfg);
18141814
const heartbeatToolResponse = resolveHeartbeatToolResponseFromReplyResult(replyResult);
18151815
const replyPayload = resolveHeartbeatReplyPayload(replyResult);
1816+
if (
1817+
!heartbeatToolResponse &&
1818+
(!replyPayload || !hasOutboundReplyContent(replyPayload)) &&
1819+
(isReplyRunActive(runSessionKey) ||
1820+
hasActiveRunForSession(runSessionKey, listActiveEmbeddedRuns))
1821+
) {
1822+
emitHeartbeatEvent({
1823+
status: "skipped",
1824+
reason: HEARTBEAT_SKIP_REQUESTS_IN_FLIGHT,
1825+
durationMs: Date.now() - startedAt,
1826+
});
1827+
return { status: "skipped", reason: HEARTBEAT_SKIP_REQUESTS_IN_FLIGHT };
1828+
}
18161829
const includeReasoning = heartbeat?.includeReasoning === true;
18171830
const reasoningPayloads = includeReasoning
18181831
? resolveHeartbeatReasoningPayloads(replyResult).filter((payload) => payload !== replyPayload)

0 commit comments

Comments
 (0)