refactor: lazy load heartbeat reply runtime

This commit is contained in:
Shakker
2026-04-02 00:24:10 +01:00
committed by Peter Steinberger
parent bf3d1f85b8
commit a78dba4396
9 changed files with 118 additions and 70 deletions

View File

@@ -1,6 +1,5 @@
import fs from "node:fs/promises";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import * as replyModule from "../auto-reply/reply.js";
import type { OpenClawConfig } from "../config/config.js";
import { resolveMainSessionKey } from "../config/sessions.js";
import { runHeartbeatOnce } from "./heartbeat-runner.js";
@@ -27,9 +26,7 @@ describe("Ghost reminder bug (issue #13317)", () => {
messageId: "m1",
chatId: "155462274",
});
const getReplySpy = vi
.spyOn(replyModule, "getReplyFromConfig")
.mockResolvedValue({ text: replyText });
const getReplySpy = vi.fn().mockResolvedValue({ text: replyText });
return { sendTelegram, getReplySpy };
};
@@ -117,6 +114,7 @@ describe("Ghost reminder bug (issue #13317)", () => {
agentId: "main",
reason: params.reason,
deps: {
getReplyFromConfig: getReplySpy,
telegram: sendTelegram,
},
});
@@ -278,6 +276,7 @@ describe("Ghost reminder bug (issue #13317)", () => {
agentId: "main",
reason: "wake",
deps: {
getReplyFromConfig: replySpy,
telegram: sendTelegram,
},
});
@@ -340,6 +339,7 @@ describe("Ghost reminder bug (issue #13317)", () => {
agentId: "main",
reason: "wake",
deps: {
getReplyFromConfig: replySpy,
telegram: sendTelegram,
},
});

View File

@@ -1,5 +1,4 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import * as replyModule from "../auto-reply/reply.js";
import type { OpenClawConfig } from "../config/config.js";
import { resolveAgentMainSessionKey, resolveMainSessionKey } from "../config/sessions.js";
import { runHeartbeatOnce } from "./heartbeat-runner.js";
@@ -19,11 +18,12 @@ async function withHeartbeatFixture(
run: (ctx: {
tmpDir: string;
storePath: string;
replySpy: ReturnType<typeof vi.fn>;
seedSession: (sessionKey: string, input: SeedSessionInput) => Promise<void>;
}) => Promise<unknown>,
): Promise<unknown> {
return withTempHeartbeatSandbox(
async ({ tmpDir, storePath }) => {
async ({ tmpDir, storePath, replySpy }) => {
const seedSession = async (sessionKey: string, input: SeedSessionInput) => {
await seedSessionStore(storePath, sessionKey, {
updatedAt: input.updatedAt,
@@ -32,7 +32,7 @@ async function withHeartbeatFixture(
lastTo: input.lastTo,
});
};
return run({ tmpDir, storePath, seedSession });
return run({ tmpDir, storePath, replySpy, seedSession });
},
{ prefix: "openclaw-hb-model-" },
);
@@ -47,27 +47,28 @@ describe("runHeartbeatOnce heartbeat model override", () => {
seedSession: (sessionKey: string, input: SeedSessionInput) => Promise<void>;
cfg: OpenClawConfig;
sessionKey: string;
replySpy: ReturnType<typeof vi.fn>;
agentId?: string;
}) {
await params.seedSession(params.sessionKey, { lastChannel: "whatsapp", lastTo: "+1555" });
const replySpy = vi.spyOn(replyModule, "getReplyFromConfig");
replySpy.mockResolvedValue({ text: "HEARTBEAT_OK" });
params.replySpy.mockResolvedValue({ text: "HEARTBEAT_OK" });
await runHeartbeatOnce({
cfg: params.cfg,
agentId: params.agentId,
deps: {
getReplyFromConfig: params.replySpy,
getQueueSize: () => 0,
nowMs: () => 0,
},
});
expect(replySpy).toHaveBeenCalledTimes(1);
expect(params.replySpy).toHaveBeenCalledTimes(1);
return {
ctx: replySpy.mock.calls[0]?.[0],
opts: replySpy.mock.calls[0]?.[1],
replySpy,
ctx: params.replySpy.mock.calls[0]?.[0],
opts: params.replySpy.mock.calls[0]?.[1],
replySpy: params.replySpy,
};
}
@@ -77,7 +78,7 @@ describe("runHeartbeatOnce heartbeat model override", () => {
lightContext?: boolean;
isolatedSession?: boolean;
}) {
return withHeartbeatFixture(async ({ tmpDir, storePath, seedSession }) => {
return withHeartbeatFixture(async ({ tmpDir, storePath, replySpy, seedSession }) => {
const cfg: OpenClawConfig = {
agents: {
defaults: {
@@ -100,6 +101,7 @@ describe("runHeartbeatOnce heartbeat model override", () => {
seedSession,
cfg,
sessionKey,
replySpy,
});
return result.opts;
});
@@ -137,7 +139,7 @@ describe("runHeartbeatOnce heartbeat model override", () => {
});
it("uses isolated session key when isolatedSession is enabled", async () => {
await withHeartbeatFixture(async ({ tmpDir, storePath, seedSession }) => {
await withHeartbeatFixture(async ({ tmpDir, storePath, replySpy, seedSession }) => {
const cfg: OpenClawConfig = {
agents: {
defaults: {
@@ -157,6 +159,7 @@ describe("runHeartbeatOnce heartbeat model override", () => {
seedSession,
cfg,
sessionKey,
replySpy,
});
// Isolated heartbeat runs use a dedicated session key with :heartbeat suffix
@@ -165,7 +168,7 @@ describe("runHeartbeatOnce heartbeat model override", () => {
});
it("uses main session key when isolatedSession is not set", async () => {
await withHeartbeatFixture(async ({ tmpDir, storePath, seedSession }) => {
await withHeartbeatFixture(async ({ tmpDir, storePath, replySpy, seedSession }) => {
const cfg: OpenClawConfig = {
agents: {
defaults: {
@@ -184,6 +187,7 @@ describe("runHeartbeatOnce heartbeat model override", () => {
seedSession,
cfg,
sessionKey,
replySpy,
});
expect(result.ctx?.SessionKey).toBe(sessionKey);
@@ -191,7 +195,7 @@ describe("runHeartbeatOnce heartbeat model override", () => {
});
it("passes per-agent heartbeat model override (merged with defaults)", async () => {
await withHeartbeatFixture(async ({ tmpDir, storePath, seedSession }) => {
await withHeartbeatFixture(async ({ tmpDir, storePath, replySpy, seedSession }) => {
const cfg: OpenClawConfig = {
agents: {
defaults: {
@@ -222,6 +226,7 @@ describe("runHeartbeatOnce heartbeat model override", () => {
cfg,
agentId: "ops",
sessionKey,
replySpy,
});
expect(result.replySpy).toHaveBeenCalledWith(

View File

@@ -78,7 +78,7 @@ describe("runHeartbeatOnce ack handling", () => {
async function runTelegramHeartbeatWithDefaults(params: {
tmpDir: string;
storePath: string;
replySpy: ReturnType<typeof vi.spyOn>;
replySpy: ReturnType<typeof vi.fn>;
replyText: string;
messages?: Record<string, unknown>;
telegramOverrides?: Record<string, unknown>;
@@ -108,7 +108,10 @@ describe("runHeartbeatOnce ack handling", () => {
const sendTelegram = createMessageSendSpy();
await runHeartbeatOnce({
cfg,
deps: makeTelegramDeps({ sendTelegram }),
deps: {
...makeTelegramDeps({ sendTelegram }),
getReplyFromConfig: params.replySpy,
},
});
return sendTelegram;
}
@@ -170,7 +173,10 @@ describe("runHeartbeatOnce ack handling", () => {
await runHeartbeatOnce({
cfg,
deps: makeWhatsAppDeps({ sendWhatsApp }),
deps: {
...makeWhatsAppDeps({ sendWhatsApp }),
getReplyFromConfig: replySpy,
},
});
expect(sendWhatsApp).toHaveBeenCalled();
@@ -196,7 +202,10 @@ describe("runHeartbeatOnce ack handling", () => {
await runHeartbeatOnce({
cfg,
deps: makeWhatsAppDeps({ sendWhatsApp }),
deps: {
...makeWhatsAppDeps({ sendWhatsApp }),
getReplyFromConfig: replySpy,
},
});
expect(sendWhatsApp).toHaveBeenCalledTimes(1);
@@ -258,7 +267,10 @@ describe("runHeartbeatOnce ack handling", () => {
const result = await runHeartbeatOnce({
cfg,
deps: makeWhatsAppDeps({ sendWhatsApp }),
deps: {
...makeWhatsAppDeps({ sendWhatsApp }),
getReplyFromConfig: replySpy,
},
});
expect(replySpy).not.toHaveBeenCalled();
@@ -279,7 +291,10 @@ describe("runHeartbeatOnce ack handling", () => {
await runHeartbeatOnce({
cfg,
deps: makeWhatsAppDeps({ sendWhatsApp }),
deps: {
...makeWhatsAppDeps({ sendWhatsApp }),
getReplyFromConfig: replySpy,
},
});
expect(sendWhatsApp).not.toHaveBeenCalled();
@@ -317,7 +332,10 @@ describe("runHeartbeatOnce ack handling", () => {
await runHeartbeatOnce({
cfg,
deps: makeWhatsAppDeps(),
deps: {
...makeWhatsAppDeps(),
getReplyFromConfig: replySpy,
},
});
const finalStore = JSON.parse(await fs.readFile(storePath, "utf-8")) as Record<
@@ -340,11 +358,14 @@ describe("runHeartbeatOnce ack handling", () => {
const res = await runHeartbeatOnce({
cfg,
deps: makeWhatsAppDeps({
sendWhatsApp,
webAuthExists: async () => false,
hasActiveWebListener: () => false,
}),
deps: {
...makeWhatsAppDeps({
sendWhatsApp,
webAuthExists: async () => false,
hasActiveWebListener: () => false,
}),
getReplyFromConfig: replySpy,
},
});
expect(res.status).toBe("skipped");
@@ -376,7 +397,10 @@ describe("runHeartbeatOnce ack handling", () => {
await runHeartbeatOnce({
cfg,
deps: makeTelegramDeps({ sendTelegram }),
deps: {
...makeTelegramDeps({ sendTelegram }),
getReplyFromConfig: replySpy,
},
});
expect(sendTelegram).toHaveBeenCalledTimes(1);

View File

@@ -3,7 +3,6 @@ import os from "node:os";
import path from "node:path";
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { HEARTBEAT_PROMPT } from "../auto-reply/heartbeat.js";
import * as replyModule from "../auto-reply/reply.js";
import type { ChannelOutboundAdapter } from "../channels/plugins/types.js";
import type { OpenClawConfig } from "../config/config.js";
import {
@@ -482,13 +481,17 @@ describe("runHeartbeatOnce", () => {
text: string,
opts?: unknown,
) => Promise<{ messageId: string; toJid: string }>,
nowMs = 0,
options?: {
nowMs?: number;
getReplyFromConfig?: HeartbeatDeps["getReplyFromConfig"];
},
): HeartbeatDeps => ({
whatsapp: sendWhatsApp,
getQueueSize: () => 0,
nowMs: () => nowMs,
nowMs: () => options?.nowMs ?? 0,
webAuthExists: async () => true,
hasActiveWebListener: () => true,
...(options?.getReplyFromConfig ? { getReplyFromConfig: options.getReplyFromConfig } : null),
});
it("skips when agent heartbeat is not enabled", async () => {
@@ -533,7 +536,7 @@ describe("runHeartbeatOnce", () => {
it("uses the last non-empty payload for delivery", async () => {
const tmpDir = await createCaseDir("hb-last-payload");
const storePath = path.join(tmpDir, "sessions.json");
const replySpy = vi.spyOn(replyModule, "getReplyFromConfig");
const replySpy = vi.fn();
try {
const cfg: OpenClawConfig = {
agents: {
@@ -575,7 +578,7 @@ describe("runHeartbeatOnce", () => {
await runHeartbeatOnce({
cfg,
deps: createHeartbeatDeps(sendWhatsApp),
deps: createHeartbeatDeps(sendWhatsApp, { getReplyFromConfig: replySpy }),
});
expect(sendWhatsApp).toHaveBeenCalledTimes(1);
@@ -585,14 +588,14 @@ describe("runHeartbeatOnce", () => {
expect.any(Object),
);
} finally {
replySpy.mockRestore();
replySpy.mockReset();
}
});
it("uses per-agent heartbeat overrides and session keys", async () => {
const tmpDir = await createCaseDir("hb-agent-overrides");
const storePath = path.join(tmpDir, "sessions.json");
const replySpy = vi.spyOn(replyModule, "getReplyFromConfig");
const replySpy = vi.fn();
try {
const cfg: OpenClawConfig = {
agents: {
@@ -640,7 +643,7 @@ describe("runHeartbeatOnce", () => {
await runHeartbeatOnce({
cfg,
agentId: "ops",
deps: createHeartbeatDeps(sendWhatsApp),
deps: createHeartbeatDeps(sendWhatsApp, { getReplyFromConfig: replySpy }),
});
expect(sendWhatsApp).toHaveBeenCalledTimes(1);
expect(sendWhatsApp).toHaveBeenCalledWith(
@@ -662,14 +665,14 @@ describe("runHeartbeatOnce", () => {
cfg,
);
} finally {
replySpy.mockRestore();
replySpy.mockReset();
}
});
it("reuses non-default agent sessionFile from templated stores", async () => {
const tmpDir = await createCaseDir("hb-templated-store");
const storeTemplate = path.join(tmpDir, "agents", "{agentId}", "sessions", "sessions.json");
const replySpy = vi.spyOn(replyModule, "getReplyFromConfig");
const replySpy = vi.fn();
const agentId = "ops";
try {
const cfg: OpenClawConfig = {
@@ -726,7 +729,7 @@ describe("runHeartbeatOnce", () => {
const result = await runHeartbeatOnce({
cfg,
agentId,
deps: createHeartbeatDeps(sendWhatsApp),
deps: createHeartbeatDeps(sendWhatsApp, { getReplyFromConfig: replySpy }),
});
expect(result.status).toBe("ran");
@@ -747,7 +750,7 @@ describe("runHeartbeatOnce", () => {
cfg,
);
} finally {
replySpy.mockRestore();
replySpy.mockReset();
}
});
@@ -779,7 +782,7 @@ describe("runHeartbeatOnce", () => {
])(
"resolves configured and forced session key overrides: $name",
async ({ name, caseDir, peerKind, peerId, message, applyOverride, runOptions }) => {
const replySpy = vi.spyOn(replyModule, "getReplyFromConfig");
const replySpy = vi.fn();
try {
const tmpDir = await createCaseDir(caseDir);
const storePath = path.join(tmpDir, "sessions.json");
@@ -839,7 +842,7 @@ describe("runHeartbeatOnce", () => {
await runHeartbeatOnce({
cfg,
...runOptions({ sessionKey: overrideSessionKey }),
deps: createHeartbeatDeps(sendWhatsApp),
deps: createHeartbeatDeps(sendWhatsApp, { getReplyFromConfig: replySpy }),
});
expect(sendWhatsApp, name).toHaveBeenCalledTimes(1);
@@ -855,7 +858,7 @@ describe("runHeartbeatOnce", () => {
cfg,
);
} finally {
replySpy.mockRestore();
replySpy.mockReset();
}
},
);
@@ -863,7 +866,7 @@ describe("runHeartbeatOnce", () => {
it("suppresses duplicate heartbeat payloads within 24h", async () => {
const tmpDir = await createCaseDir("hb-dup-suppress");
const storePath = path.join(tmpDir, "sessions.json");
const replySpy = vi.spyOn(replyModule, "getReplyFromConfig");
const replySpy = vi.fn();
try {
const cfg: OpenClawConfig = {
agents: {
@@ -904,12 +907,15 @@ describe("runHeartbeatOnce", () => {
await runHeartbeatOnce({
cfg,
deps: createHeartbeatDeps(sendWhatsApp, 60_000),
deps: createHeartbeatDeps(sendWhatsApp, {
nowMs: 60_000,
getReplyFromConfig: replySpy,
}),
});
expect(sendWhatsApp).toHaveBeenCalledTimes(0);
} finally {
replySpy.mockRestore();
replySpy.mockReset();
}
});
@@ -936,7 +942,7 @@ describe("runHeartbeatOnce", () => {
)(
"handles reasoning payload delivery variants: $name",
async ({ name, caseDir, replies, expectedTexts }) => {
const replySpy = vi.spyOn(replyModule, "getReplyFromConfig");
const replySpy = vi.fn();
try {
const tmpDir = await createCaseDir(caseDir);
const storePath = path.join(tmpDir, "sessions.json");
@@ -983,7 +989,7 @@ describe("runHeartbeatOnce", () => {
await runHeartbeatOnce({
cfg,
deps: createHeartbeatDeps(sendWhatsApp),
deps: createHeartbeatDeps(sendWhatsApp, { getReplyFromConfig: replySpy }),
});
expect(sendWhatsApp, name).toHaveBeenCalledTimes(expectedTexts.length);
@@ -996,7 +1002,7 @@ describe("runHeartbeatOnce", () => {
);
}
} finally {
replySpy.mockRestore();
replySpy.mockReset();
}
},
);
@@ -1004,7 +1010,7 @@ describe("runHeartbeatOnce", () => {
it("loads the default agent session from templated stores", async () => {
const tmpDir = await createCaseDir("openclaw-hb");
const storeTemplate = path.join(tmpDir, "agents", "{agentId}", "sessions.json");
const replySpy = vi.spyOn(replyModule, "getReplyFromConfig");
const replySpy = vi.fn();
try {
const cfg: OpenClawConfig = {
agents: {
@@ -1048,7 +1054,7 @@ describe("runHeartbeatOnce", () => {
await runHeartbeatOnce({
cfg,
deps: createHeartbeatDeps(sendWhatsApp),
deps: createHeartbeatDeps(sendWhatsApp, { getReplyFromConfig: replySpy }),
});
expect(sendWhatsApp).toHaveBeenCalledTimes(1);
@@ -1058,7 +1064,7 @@ describe("runHeartbeatOnce", () => {
expect.any(Object),
);
} finally {
replySpy.mockRestore();
replySpy.mockReset();
}
});
@@ -1121,7 +1127,7 @@ describe("runHeartbeatOnce", () => {
});
}
const replySpy = vi.spyOn(replyModule, "getReplyFromConfig");
const replySpy = vi.fn();
replySpy.mockResolvedValue({ text: params.replyText ?? "Checked logs and PRs" });
const sendWhatsApp = vi
.fn<
@@ -1131,7 +1137,7 @@ describe("runHeartbeatOnce", () => {
const res = await runHeartbeatOnce({
cfg,
reason: params.reason,
deps: createHeartbeatDeps(sendWhatsApp),
deps: createHeartbeatDeps(sendWhatsApp, { getReplyFromConfig: replySpy }),
});
return { res, replySpy, sendWhatsApp, workspaceDir };
}
@@ -1262,7 +1268,7 @@ describe("runHeartbeatOnce", () => {
expect(calledCtx.Body, name).toContain("scheduled reminder has been triggered");
}
} finally {
replySpy.mockRestore();
replySpy.mockReset();
}
}
});
@@ -1297,7 +1303,7 @@ describe("runHeartbeatOnce", () => {
contextKey: "cron:rotate-logs",
});
const replySpy = vi.spyOn(replyModule, "getReplyFromConfig");
const replySpy = vi.fn();
replySpy.mockResolvedValue({ text: "Handled internally" });
const sendWhatsApp = vi
.fn<
@@ -1309,7 +1315,7 @@ describe("runHeartbeatOnce", () => {
const res = await runHeartbeatOnce({
cfg,
reason: "interval",
deps: createHeartbeatDeps(sendWhatsApp),
deps: createHeartbeatDeps(sendWhatsApp, { getReplyFromConfig: replySpy }),
});
expect(res.status).toBe("ran");
expect(sendWhatsApp).toHaveBeenCalledTimes(0);
@@ -1318,7 +1324,7 @@ describe("runHeartbeatOnce", () => {
expect(calledCtx.Body).toContain("Handle this reminder internally");
expect(calledCtx.Body).not.toContain("Please relay this reminder to the user");
} finally {
replySpy.mockRestore();
replySpy.mockReset();
}
});
@@ -1352,7 +1358,7 @@ describe("runHeartbeatOnce", () => {
contextKey: "exec:backup",
});
const replySpy = vi.spyOn(replyModule, "getReplyFromConfig");
const replySpy = vi.fn();
replySpy.mockResolvedValue({ text: "Handled internally" });
const sendWhatsApp = vi
.fn<
@@ -1364,7 +1370,7 @@ describe("runHeartbeatOnce", () => {
const res = await runHeartbeatOnce({
cfg,
reason: "exec-event",
deps: createHeartbeatDeps(sendWhatsApp),
deps: createHeartbeatDeps(sendWhatsApp, { getReplyFromConfig: replySpy }),
});
expect(res.status).toBe("ran");
expect(sendWhatsApp).toHaveBeenCalledTimes(0);
@@ -1378,7 +1384,7 @@ describe("runHeartbeatOnce", () => {
expect(calledCtx.Body).toContain("Handle the result internally");
expect(calledCtx.Body).not.toContain("Please relay the command output to the user");
} finally {
replySpy.mockRestore();
replySpy.mockReset();
}
});
});

View File

@@ -0,0 +1 @@
export { getReplyFromConfig } from "../auto-reply/reply.js";

View File

@@ -44,6 +44,7 @@ describe("runHeartbeatOnce", () => {
await runHeartbeatOnce({
cfg,
deps: {
getReplyFromConfig: replySpy,
slack: sendSlack,
getQueueSize: () => 0,
nowMs: () => 0,

View File

@@ -2,7 +2,6 @@ import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { vi } from "vitest";
import * as replyModule from "../auto-reply/reply.js";
import type { OpenClawConfig } from "../config/config.js";
import { resolveMainSessionKey } from "../config/sessions.js";
import { setActivePluginRegistry } from "../plugins/runtime.js";
@@ -48,7 +47,7 @@ export async function withTempHeartbeatSandbox<T>(
fn: (ctx: {
tmpDir: string;
storePath: string;
replySpy: ReturnType<typeof vi.spyOn>;
replySpy: ReturnType<typeof vi.fn>;
}) => Promise<T>,
options?: {
prefix?: string;
@@ -58,7 +57,7 @@ export async function withTempHeartbeatSandbox<T>(
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), options?.prefix ?? "openclaw-hb-"));
await fs.writeFile(path.join(tmpDir, "HEARTBEAT.md"), "- Check status\n", "utf-8");
const storePath = path.join(tmpDir, "sessions.json");
const replySpy = vi.spyOn(replyModule, "getReplyFromConfig");
const replySpy = vi.fn().mockResolvedValue({ text: "ok" });
const previousEnv = new Map<string, string | undefined>();
for (const envName of options?.unsetEnvVars ?? []) {
previousEnv.set(envName, process.env[envName]);
@@ -67,7 +66,7 @@ export async function withTempHeartbeatSandbox<T>(
try {
return await fn({ tmpDir, storePath, replySpy });
} finally {
replySpy.mockRestore();
replySpy.mockReset();
for (const [envName, previousValue] of previousEnv.entries()) {
if (previousValue === undefined) {
delete process.env[envName];
@@ -83,7 +82,7 @@ export async function withTempTelegramHeartbeatSandbox<T>(
fn: (ctx: {
tmpDir: string;
storePath: string;
replySpy: ReturnType<typeof vi.spyOn>;
replySpy: ReturnType<typeof vi.fn>;
}) => Promise<T>,
options?: {
prefix?: string;

View File

@@ -70,7 +70,10 @@ describe("heartbeat transcript pruning", () => {
agentId: undefined,
reason: "test",
cfg,
deps: { sendTelegram: vi.fn() },
deps: {
sendTelegram: vi.fn(),
getReplyFromConfig: replySpy,
},
});
const finalSize = (await fs.stat(transcriptPath)).size;

View File

@@ -19,7 +19,6 @@ import {
resolveHeartbeatPrompt as resolveHeartbeatPromptText,
stripHeartbeatToken,
} from "../auto-reply/heartbeat.js";
import { getReplyFromConfig } from "../auto-reply/reply.js";
import { HEARTBEAT_TOKEN } from "../auto-reply/tokens.js";
import type { ReplyPayload } from "../auto-reply/types.js";
import { getChannelPlugin } from "../channels/plugins/index.js";
@@ -84,12 +83,20 @@ import { peekSystemEventEntries, resolveSystemEventDeliveryContext } from "./sys
export type HeartbeatDeps = OutboundSendDeps &
ChannelHeartbeatDeps & {
getReplyFromConfig?: typeof import("./heartbeat-runner.runtime.js").getReplyFromConfig;
runtime?: RuntimeEnv;
getQueueSize?: (lane?: string) => number;
nowMs?: () => number;
};
const log = createSubsystemLogger("gateway/heartbeat");
let heartbeatRunnerRuntimePromise: Promise<typeof import("./heartbeat-runner.runtime.js")> | null =
null;
function loadHeartbeatRunnerRuntime() {
heartbeatRunnerRuntimePromise ??= import("./heartbeat-runner.runtime.js");
return heartbeatRunnerRuntimePromise;
}
export { areHeartbeatsEnabled, setHeartbeatsEnabled };
export {
@@ -730,6 +737,8 @@ export async function runHeartbeatOnce(opts: {
bootstrapContextMode,
}
: { isHeartbeat: true, suppressToolErrorWarnings, bootstrapContextMode };
const getReplyFromConfig =
opts.deps?.getReplyFromConfig ?? (await loadHeartbeatRunnerRuntime()).getReplyFromConfig;
const replyResult = await getReplyFromConfig(ctx, replyOpts, cfg);
const replyPayload = resolveHeartbeatReplyPayload(replyResult);
const includeReasoning = heartbeat?.includeReasoning === true;