Skip to content

Commit bea4f0d

Browse files
authored
fix(gateway): defer heartbeats during active replies
* fix(gateway): defer heartbeats during active replies * fix(gateway): bind heartbeat reply run fallback
1 parent 77ca3dc commit bea4f0d

3 files changed

Lines changed: 35 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ Docs: https://docs.openclaw.ai
3737
- CLI/setup: order the model/auth provider picker as OpenAI, Anthropic, xAI, Google, then the remaining providers alphabetically.
3838
- Gateway/diagnostics: add opt-in critical memory pressure stability snapshots with gateway logs, V8 heap, cgroup, active-resource, and redacted large session-file evidence. Fixes #82518.
3939
- Doctor/Gateway: avoid treating unrelated macOS LaunchAgents as legacy gateways just because their environment values mention old checkout paths.
40+
- Gateway/heartbeat: defer heartbeat runs while the target reply operation is queued or active, preventing heartbeat prompts from interleaving with WebChat responses before the streaming lane starts. Fixes #82722. Thanks @Andy-Xie-1145.
4041
- CLI/setup: collapse raw gateway config keys in existing-config summaries into friendly `Model` and `Gateway` rows.
4142
- CLI/config: show concise human config-write output with an indented backup path instead of printing checksum-heavy overwrite audit details by default.
4243
- CLI/docs: call the canonical lowercase docs MCP search tool and surface MCP errors instead of returning empty search results. Fixes #82702. (#82704) Thanks @hclsys.

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,28 @@ describe("heartbeat runner skips when target session lane is busy", () => {
228228
});
229229
});
230230

231+
it("returns requests-in-flight when the target session has an active reply run", async () => {
232+
await withTempHeartbeatSandbox(async ({ storePath, replySpy }) => {
233+
const cfg = createHeartbeatTelegramConfig();
234+
const sessionKey = await seedHeartbeatTelegramSession(storePath, cfg);
235+
const isReplyRunActive = vi.fn((key: string) => key === sessionKey);
236+
237+
const result = await runHeartbeatOnce({
238+
cfg,
239+
deps: {
240+
getQueueSize: vi.fn((_lane?: string) => 0),
241+
isReplyRunActive,
242+
nowMs: () => Date.now(),
243+
getReplyFromConfig: replySpy,
244+
} as HeartbeatDeps,
245+
});
246+
247+
expect(result).toEqual({ status: "skipped", reason: HEARTBEAT_SKIP_REQUESTS_IN_FLIGHT });
248+
expect(isReplyRunActive).toHaveBeenCalledWith(sessionKey);
249+
expect(replySpy).not.toHaveBeenCalled();
250+
});
251+
});
252+
231253
it("does not defer on a recent heartbeat ack pending final delivery", async () => {
232254
await withTempHeartbeatSandbox(async ({ storePath, replySpy }) => {
233255
const cfg = createHeartbeatTelegramConfig();

src/infra/heartbeat-runner.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
type HeartbeatTask,
3535
} from "../auto-reply/heartbeat.js";
3636
import { resolveDefaultModel } from "../auto-reply/reply/directive-handling.defaults.js";
37+
import { replyRunRegistry } from "../auto-reply/reply/reply-run-registry.js";
3738
import { resolveResponsePrefixTemplate } from "../auto-reply/reply/response-prefix-template.js";
3839
import { HEARTBEAT_TOKEN } from "../auto-reply/tokens.js";
3940
import type { ReplyPayload } from "../auto-reply/types.js";
@@ -142,6 +143,7 @@ export type HeartbeatDeps = OutboundSendDeps &
142143
runtime?: RuntimeEnv;
143144
getQueueSize?: (lane?: string) => number;
144145
getCommandLaneSnapshots?: () => readonly CommandLaneSnapshot[];
146+
isReplyRunActive?: (sessionKey: string) => boolean;
145147
nowMs?: () => number;
146148
};
147149

@@ -1350,6 +1352,16 @@ export async function runHeartbeatOnce(opts: {
13501352
return { status: "skipped", reason: preflight.skipReason };
13511353
}
13521354
const { entry, sessionKey, storePath, suppressOriginatingContext } = preflight.session;
1355+
const isReplyRunActive =
1356+
opts.deps?.isReplyRunActive ?? ((key: string) => replyRunRegistry.isActive(key));
1357+
if (isReplyRunActive(sessionKey)) {
1358+
emitHeartbeatEvent({
1359+
status: "skipped",
1360+
reason: HEARTBEAT_SKIP_REQUESTS_IN_FLIGHT,
1361+
durationMs: Date.now() - startedAt,
1362+
});
1363+
return { status: "skipped", reason: HEARTBEAT_SKIP_REQUESTS_IN_FLIGHT };
1364+
}
13531365

13541366
// Check the resolved session lane — if it is busy, skip to avoid interrupting
13551367
// an active streaming turn. The wake-layer retry (heartbeat-wake.ts) will

0 commit comments

Comments
 (0)