perf: optimize cold import paths

This commit is contained in:
Peter Steinberger
2026-03-26 23:10:01 +00:00
parent 15181b3a77
commit 4b40d4dfa8
39 changed files with 620 additions and 171 deletions

View File

@@ -0,0 +1,3 @@
import type { SubagentRunRecord } from "./subagent-registry.types.js";
export const subagentRuns = new Map<string, SubagentRunRecord>();

View File

@@ -0,0 +1,126 @@
import { SUBAGENT_ENDED_REASON_KILLED } from "./subagent-lifecycle-events.js";
import { subagentRuns } from "./subagent-registry-memory.js";
import { listRunsForControllerFromRuns } from "./subagent-registry-queries.js";
import { getSubagentRunsSnapshotForRead } from "./subagent-registry-state.js";
import type { SubagentRunRecord } from "./subagent-registry.types.js";
function resolveSubagentSessionStartedAt(
entry: Pick<SubagentRunRecord, "sessionStartedAt" | "startedAt" | "createdAt">,
): number | undefined {
if (typeof entry.sessionStartedAt === "number" && Number.isFinite(entry.sessionStartedAt)) {
return entry.sessionStartedAt;
}
if (typeof entry.startedAt === "number" && Number.isFinite(entry.startedAt)) {
return entry.startedAt;
}
return typeof entry.createdAt === "number" && Number.isFinite(entry.createdAt)
? entry.createdAt
: undefined;
}
export function getSubagentSessionStartedAt(
entry: Pick<SubagentRunRecord, "sessionStartedAt" | "startedAt" | "createdAt"> | null | undefined,
): number | undefined {
return entry ? resolveSubagentSessionStartedAt(entry) : undefined;
}
export function getSubagentSessionRuntimeMs(
entry:
| Pick<SubagentRunRecord, "startedAt" | "endedAt" | "accumulatedRuntimeMs">
| null
| undefined,
now = Date.now(),
): number | undefined {
if (!entry) {
return undefined;
}
const accumulatedRuntimeMs =
typeof entry.accumulatedRuntimeMs === "number" && Number.isFinite(entry.accumulatedRuntimeMs)
? Math.max(0, entry.accumulatedRuntimeMs)
: 0;
if (typeof entry.startedAt !== "number" || !Number.isFinite(entry.startedAt)) {
return entry.accumulatedRuntimeMs != null ? accumulatedRuntimeMs : undefined;
}
const currentRunEndedAt =
typeof entry.endedAt === "number" && Number.isFinite(entry.endedAt) ? entry.endedAt : now;
return Math.max(0, accumulatedRuntimeMs + Math.max(0, currentRunEndedAt - entry.startedAt));
}
export function resolveSubagentSessionStatus(
entry: Pick<SubagentRunRecord, "endedAt" | "endedReason" | "outcome"> | null | undefined,
): "running" | "killed" | "failed" | "timeout" | "done" | undefined {
if (!entry) {
return undefined;
}
if (!entry.endedAt) {
return "running";
}
if (entry.endedReason === SUBAGENT_ENDED_REASON_KILLED) {
return "killed";
}
const status = entry.outcome?.status;
if (status === "error") {
return "failed";
}
if (status === "timeout") {
return "timeout";
}
return "done";
}
export function listSubagentRunsForController(controllerSessionKey: string): SubagentRunRecord[] {
return listRunsForControllerFromRuns(
getSubagentRunsSnapshotForRead(subagentRuns),
controllerSessionKey,
);
}
export function getSubagentRunByChildSessionKey(childSessionKey: string): SubagentRunRecord | null {
const key = childSessionKey.trim();
if (!key) {
return null;
}
let latestActive: SubagentRunRecord | null = null;
let latestEnded: SubagentRunRecord | null = null;
for (const entry of getSubagentRunsSnapshotForRead(subagentRuns).values()) {
if (entry.childSessionKey !== key) {
continue;
}
if (typeof entry.endedAt !== "number") {
if (!latestActive || entry.createdAt > latestActive.createdAt) {
latestActive = entry;
}
continue;
}
if (!latestEnded || entry.createdAt > latestEnded.createdAt) {
latestEnded = entry;
}
}
return latestActive ?? latestEnded;
}
export function getLatestSubagentRunByChildSessionKey(
childSessionKey: string,
): SubagentRunRecord | null {
const key = childSessionKey.trim();
if (!key) {
return null;
}
let latest: SubagentRunRecord | null = null;
for (const entry of getSubagentRunsSnapshotForRead(subagentRuns).values()) {
if (entry.childSessionKey !== key) {
continue;
}
if (!latest || entry.createdAt > latest.createdAt) {
latest = entry;
}
}
return latest;
}

View File

@@ -41,6 +41,7 @@ import {
resolveLifecycleOutcomeFromRunOutcome,
runOutcomesEqual,
} from "./subagent-registry-completion.js";
import { subagentRuns } from "./subagent-registry-memory.js";
import {
countActiveDescendantRunsFromRuns,
countActiveRunsForSessionFromRuns,
@@ -64,7 +65,6 @@ import { resolveAgentTimeoutMs } from "./timeout.js";
export type { SubagentRunRecord } from "./subagent-registry.types.js";
const log = createSubsystemLogger("agents/subagent-registry");
const subagentRuns = new Map<string, SubagentRunRecord>();
let sweeper: NodeJS.Timeout | null = null;
let listenerStarted = false;
let listenerStop: (() => void) | null = null;

View File

@@ -788,7 +788,7 @@ describe("mention helpers", () => {
});
describe("resolveGroupRequireMention", () => {
it("respects Discord guild/channel requireMention settings", () => {
it("respects Discord guild/channel requireMention settings", async () => {
resetPluginRuntimeStateForTest();
const cfg: OpenClawConfig = {
channels: {
@@ -816,10 +816,10 @@ describe("resolveGroupRequireMention", () => {
chatType: "group",
};
expect(resolveGroupRequireMention({ cfg, ctx, groupResolution })).toBe(false);
await expect(resolveGroupRequireMention({ cfg, ctx, groupResolution })).resolves.toBe(false);
});
it("respects Slack channel requireMention settings", () => {
it("respects Slack channel requireMention settings", async () => {
resetPluginRuntimeStateForTest();
const cfg: OpenClawConfig = {
channels: {
@@ -842,10 +842,10 @@ describe("resolveGroupRequireMention", () => {
chatType: "group",
};
expect(resolveGroupRequireMention({ cfg, ctx, groupResolution })).toBe(false);
await expect(resolveGroupRequireMention({ cfg, ctx, groupResolution })).resolves.toBe(false);
});
it("uses Slack fallback resolver semantics for default-account wildcard channels", () => {
it("uses Slack fallback resolver semantics for default-account wildcard channels", async () => {
resetPluginRuntimeStateForTest();
const cfg: OpenClawConfig = {
channels: {
@@ -873,10 +873,10 @@ describe("resolveGroupRequireMention", () => {
chatType: "group",
};
expect(resolveGroupRequireMention({ cfg, ctx, groupResolution })).toBe(false);
await expect(resolveGroupRequireMention({ cfg, ctx, groupResolution })).resolves.toBe(false);
});
it("matches the Slack plugin resolver for default-account wildcard fallbacks", () => {
it("matches the Slack plugin resolver for default-account wildcard fallbacks", async () => {
resetPluginRuntimeStateForTest();
const cfg: OpenClawConfig = {
channels: {
@@ -904,7 +904,7 @@ describe("resolveGroupRequireMention", () => {
chatType: "group",
};
expect(resolveGroupRequireMention({ cfg, ctx, groupResolution })).toBe(
await expect(resolveGroupRequireMention({ cfg, ctx, groupResolution })).resolves.toBe(
resolveSlackGroupRequireMention({
cfg,
groupId: groupResolution.id,
@@ -913,7 +913,7 @@ describe("resolveGroupRequireMention", () => {
);
});
it("uses Discord fallback resolver semantics for guild slug matches", () => {
it("uses Discord fallback resolver semantics for guild slug matches", async () => {
resetPluginRuntimeStateForTest();
const cfg: OpenClawConfig = {
channels: {
@@ -940,10 +940,10 @@ describe("resolveGroupRequireMention", () => {
chatType: "group",
};
expect(resolveGroupRequireMention({ cfg, ctx, groupResolution })).toBe(false);
await expect(resolveGroupRequireMention({ cfg, ctx, groupResolution })).resolves.toBe(false);
});
it("matches the Discord plugin resolver for slug + wildcard guild fallbacks", () => {
it("matches the Discord plugin resolver for slug + wildcard guild fallbacks", async () => {
resetPluginRuntimeStateForTest();
const cfg: OpenClawConfig = {
channels: {
@@ -972,7 +972,7 @@ describe("resolveGroupRequireMention", () => {
chatType: "group",
};
expect(resolveGroupRequireMention({ cfg, ctx, groupResolution })).toBe(
await expect(resolveGroupRequireMention({ cfg, ctx, groupResolution })).resolves.toBe(
resolveDiscordGroupRequireMention({
cfg,
groupId: groupResolution.id,
@@ -982,7 +982,7 @@ describe("resolveGroupRequireMention", () => {
);
});
it("respects LINE prefixed group keys in reply-stage requireMention resolution", () => {
it("respects LINE prefixed group keys in reply-stage requireMention resolution", async () => {
resetPluginRuntimeStateForTest();
const cfg: OpenClawConfig = {
channels: {
@@ -1004,10 +1004,10 @@ describe("resolveGroupRequireMention", () => {
chatType: "group",
};
expect(resolveGroupRequireMention({ cfg, ctx, groupResolution })).toBe(false);
await expect(resolveGroupRequireMention({ cfg, ctx, groupResolution })).resolves.toBe(false);
});
it("preserves plugin-backed channel requireMention resolution", () => {
it("preserves plugin-backed channel requireMention resolution", async () => {
resetPluginRuntimeStateForTest();
const cfg: OpenClawConfig = {
channels: {
@@ -1029,6 +1029,6 @@ describe("resolveGroupRequireMention", () => {
chatType: "group",
};
expect(resolveGroupRequireMention({ cfg, ctx, groupResolution })).toBe(false);
await expect(resolveGroupRequireMention({ cfg, ctx, groupResolution })).resolves.toBe(false);
});
});

View File

@@ -56,10 +56,10 @@ vi.mock("./reply/directive-handling.defaults.js", () => ({
vi.mock("./reply/inbound-context.js", () => ({
finalizeInboundContext: vi.fn((ctx: unknown) => ctx),
}));
vi.mock("./reply/session-reset-model.js", () => ({
vi.mock("./reply/session-reset-model.runtime.js", () => ({
applyResetModelOverride: vi.fn(async () => undefined),
}));
vi.mock("./reply/stage-sandbox-media.js", () => ({
vi.mock("./reply/stage-sandbox-media.runtime.js", () => ({
stageSandboxMedia: vi.fn(async () => undefined),
}));
vi.mock("./reply/typing.js", () => ({

View File

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

View File

@@ -15,7 +15,13 @@ const childProcessMocks = vi.hoisted(() => ({
}));
vi.mock("../agents/sandbox.js", () => sandboxMocks);
vi.mock("node:child_process", () => childProcessMocks);
vi.mock("node:child_process", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:child_process")>();
return {
...actual,
spawn: childProcessMocks.spawn,
};
});
import { stageSandboxMedia } from "./reply/stage-sandbox-media.js";

View File

@@ -22,7 +22,13 @@ let stageSandboxMedia: typeof import("./reply/stage-sandbox-media.js").stageSand
async function loadFreshStageSandboxMediaModuleForTest() {
vi.resetModules();
vi.doMock(sandboxModuleId, () => sandboxMocks);
vi.doMock("node:child_process", () => childProcessMocks);
vi.doMock("node:child_process", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:child_process")>();
return {
...actual,
spawn: childProcessMocks.spawn,
};
});
vi.doMock(fsSafeModuleId, async (importOriginal) => {
const actual = await importOriginal<typeof import("../infra/fs-safe.js")>();
return {

View File

@@ -361,7 +361,7 @@ export async function resolveReplyDirectives(params: {
};
}
const requireMention = resolveGroupRequireMention({
const requireMention = await resolveGroupRequireMention({
cfg,
ctx: sessionCtx,
groupResolution,

View File

@@ -20,9 +20,15 @@ vi.mock("../../config/sessions/paths.js", () => ({
resolveSessionFilePathOptions: vi.fn().mockReturnValue({}),
}));
vi.mock("../../config/sessions/store.js", () => ({
updateSessionStore: vi.fn(),
}));
const storeRuntimeLoads = vi.hoisted(() => vi.fn());
const updateSessionStore = vi.hoisted(() => vi.fn());
vi.mock("../../config/sessions/store.runtime.js", () => {
storeRuntimeLoads();
return {
updateSessionStore,
};
});
vi.mock("../../globals.js", () => ({
logVerbose: vi.fn(),
@@ -181,10 +187,18 @@ function baseParams(
describe("runPreparedReply media-only handling", () => {
beforeEach(async () => {
storeRuntimeLoads.mockClear();
updateSessionStore.mockReset();
vi.clearAllMocks();
await loadFreshGetReplyRunModuleForTest();
});
it("does not load session store runtime on module import", async () => {
await loadFreshGetReplyRunModuleForTest();
expect(storeRuntimeLoads).not.toHaveBeenCalled();
});
it("allows media-only prompts and preserves thread context in queued followups", async () => {
const result = await runPreparedReply(baseParams());
expect(result).toEqual({ text: "ok" });

View File

@@ -8,7 +8,6 @@ import {
resolveSessionFilePath,
resolveSessionFilePathOptions,
} from "../../config/sessions/paths.js";
import { updateSessionStore } from "../../config/sessions/store.js";
import type { SessionEntry } from "../../config/sessions/types.js";
import { logVerbose } from "../../globals.js";
import { clearCommandLane, getQueueSize } from "../../process/command-queue.js";
@@ -54,6 +53,9 @@ let agentRunnerRuntimePromise: Promise<typeof import("./agent-runner.runtime.js"
let routeReplyRuntimePromise: Promise<typeof import("./route-reply.runtime.js")> | null = null;
let sessionUpdatesRuntimePromise: Promise<typeof import("./session-updates.runtime.js")> | null =
null;
let sessionStoreRuntimePromise: Promise<
typeof import("../../config/sessions/store.runtime.js")
> | null = null;
function loadPiEmbeddedRuntime() {
piEmbeddedRuntimePromise ??= import("../../agents/pi-embedded.runtime.js");
@@ -75,6 +77,11 @@ function loadSessionUpdatesRuntime() {
return sessionUpdatesRuntimePromise;
}
function loadSessionStoreRuntime() {
sessionStoreRuntimePromise ??= import("../../config/sessions/store.runtime.js");
return sessionStoreRuntimePromise;
}
function buildResetSessionNoticeText(params: {
provider: string;
model: string;
@@ -439,6 +446,7 @@ export async function runPreparedReply(
sessionEntry.updatedAt = Date.now();
sessionStore[sessionKey] = sessionEntry;
if (storePath) {
const { updateSessionStore } = await loadSessionStoreRuntime();
await updateSessionStore(storePath, (store) => {
store[sessionKey] = sessionEntry;
});

View File

@@ -0,0 +1,27 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
describe("get-reply module imports", () => {
beforeEach(() => {
vi.resetModules();
});
it("does not load reset-model runtime on module import", async () => {
const resetModelRuntimeLoads = vi.fn();
const sandboxMediaRuntimeLoads = vi.fn();
vi.doMock("./session-reset-model.runtime.js", async (importOriginal) => {
resetModelRuntimeLoads();
return await importOriginal<typeof import("./session-reset-model.runtime.js")>();
});
vi.doMock("./stage-sandbox-media.runtime.js", async (importOriginal) => {
sandboxMediaRuntimeLoads();
return await importOriginal<typeof import("./stage-sandbox-media.runtime.js")>();
});
await import("./get-reply.js");
expect(resetModelRuntimeLoads).not.toHaveBeenCalled();
expect(sandboxMediaRuntimeLoads).not.toHaveBeenCalled();
vi.doUnmock("./session-reset-model.runtime.js");
vi.doUnmock("./stage-sandbox-media.runtime.js");
});
});

View File

@@ -51,10 +51,10 @@ export function registerGetReplyCommonMocks(): void {
vi.mock("./inbound-context.js", () => ({
finalizeInboundContext: vi.fn((ctx: unknown) => ctx),
}));
vi.mock("./session-reset-model.js", () => ({
vi.mock("./session-reset-model.runtime.js", () => ({
applyResetModelOverride: vi.fn(async () => undefined),
}));
vi.mock("./stage-sandbox-media.js", () => ({
vi.mock("./stage-sandbox-media.runtime.js", () => ({
stageSandboxMedia: vi.fn(async () => undefined),
}));
vi.mock("./typing.js", () => ({

View File

@@ -22,13 +22,28 @@ import { handleInlineActions } from "./get-reply-inline-actions.js";
import { runPreparedReply } from "./get-reply-run.js";
import { finalizeInboundContext } from "./inbound-context.js";
import { emitPreAgentMessageHooks } from "./message-preprocess-hooks.js";
import { applyResetModelOverride } from "./session-reset-model.js";
import { initSessionState } from "./session.js";
import { stageSandboxMedia } from "./stage-sandbox-media.js";
import { createTypingController } from "./typing.js";
type ResetCommandAction = "new" | "reset";
let sessionResetModelRuntimePromise: Promise<
typeof import("./session-reset-model.runtime.js")
> | null = null;
let stageSandboxMediaRuntimePromise: Promise<
typeof import("./stage-sandbox-media.runtime.js")
> | null = null;
function loadSessionResetModelRuntime() {
sessionResetModelRuntimePromise ??= import("./session-reset-model.runtime.js");
return sessionResetModelRuntimePromise;
}
function loadStageSandboxMediaRuntime() {
stageSandboxMediaRuntimePromise ??= import("./stage-sandbox-media.runtime.js");
return stageSandboxMediaRuntimePromise;
}
function mergeSkillFilters(channelFilter?: string[], agentFilter?: string[]): string[] | undefined {
const normalize = (list?: string[]) => {
if (!Array.isArray(list)) {
@@ -222,21 +237,24 @@ export async function getReplyFromConfig(
bodyStripped,
} = sessionState;
await applyResetModelOverride({
cfg,
agentId,
resetTriggered,
bodyStripped,
sessionCtx,
ctx: finalized,
sessionEntry,
sessionStore,
sessionKey,
storePath,
defaultProvider,
defaultModel,
aliasIndex,
});
if (resetTriggered && bodyStripped?.trim()) {
const { applyResetModelOverride } = await loadSessionResetModelRuntime();
await applyResetModelOverride({
cfg,
agentId,
resetTriggered,
bodyStripped,
sessionCtx,
ctx: finalized,
sessionEntry,
sessionStore,
sessionKey,
storePath,
defaultProvider,
defaultModel,
aliasIndex,
});
}
const channelModelOverride = resolveChannelModelOverride({
cfg,
@@ -400,13 +418,16 @@ export async function getReplyFromConfig(
directives = inlineActionResult.directives;
abortedLastRun = inlineActionResult.abortedLastRun ?? abortedLastRun;
await stageSandboxMedia({
ctx,
sessionCtx,
cfg,
sessionKey,
workspaceDir,
});
if (sessionKey && hasInboundMedia(ctx)) {
const { stageSandboxMedia } = await loadStageSandboxMediaRuntime();
await stageSandboxMedia({
ctx,
sessionCtx,
cfg,
sessionKey,
workspaceDir,
});
}
return runPreparedReply({
ctx,

View File

@@ -0,0 +1,3 @@
export { resolveDiscordGroupRequireMention } from "../../../extensions/discord/api.js";
export { resolveSlackGroupRequireMention } from "../../../extensions/slack/api.js";
export { getChannelPlugin, normalizeChannelId } from "../../channels/plugins/index.js";

View File

@@ -0,0 +1,76 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../../config/config.js";
import { resetPluginRuntimeStateForTest } from "../../plugins/runtime.js";
describe("group runtime loading", () => {
beforeEach(() => {
resetPluginRuntimeStateForTest();
vi.resetModules();
});
it("keeps prompt helpers off the heavy group runtime", async () => {
const groupsRuntimeLoads = vi.fn();
vi.doMock("./groups.runtime.js", async (importOriginal) => {
groupsRuntimeLoads();
return await importOriginal<typeof import("./groups.runtime.js")>();
});
const groups = await import("./groups.js");
expect(groupsRuntimeLoads).not.toHaveBeenCalled();
expect(
groups.buildGroupChatContext({
sessionCtx: {
ChatType: "group",
GroupSubject: "Ops",
Provider: "whatsapp",
},
}),
).toContain('You are in the WhatsApp group chat "Ops".');
expect(
groups.buildGroupIntro({
cfg: {} as OpenClawConfig,
sessionCtx: { Provider: "whatsapp" },
defaultActivation: "mention",
silentToken: "NO_REPLY",
}),
).toContain("WhatsApp IDs:");
expect(groupsRuntimeLoads).not.toHaveBeenCalled();
vi.doUnmock("./groups.runtime.js");
});
it("loads the group runtime only when requireMention resolution needs it", async () => {
const groupsRuntimeLoads = vi.fn();
vi.doMock("./groups.runtime.js", async (importOriginal) => {
groupsRuntimeLoads();
return await importOriginal<typeof import("./groups.runtime.js")>();
});
const groups = await import("./groups.js");
await expect(
groups.resolveGroupRequireMention({
cfg: {
channels: {
slack: {
channels: {
C123: { requireMention: false },
},
},
},
},
ctx: {
Provider: "slack",
From: "slack:channel:C123",
GroupSubject: "#general",
},
groupResolution: {
key: "slack:group:C123",
channel: "slack",
id: "C123",
chatType: "group",
},
}),
).resolves.toBe(false);
expect(groupsRuntimeLoads).toHaveBeenCalled();
vi.doUnmock("./groups.runtime.js");
});
});

View File

@@ -1,11 +1,4 @@
import { resolveDiscordGroupRequireMention } from "../../../extensions/discord/api.js";
import { resolveSlackGroupRequireMention } from "../../../extensions/slack/api.js";
import {
getChannelPlugin,
normalizeChannelId as normalizePluginChannelId,
} from "../../channels/plugins/index.js";
import type { ChannelId } from "../../channels/plugins/types.js";
import { resolveWhatsAppGroupIntroHint } from "../../channels/plugins/whatsapp-shared.js";
import type { OpenClawConfig } from "../../config/config.js";
import { resolveChannelGroupRequireMention } from "../../config/group-policy.js";
import type { GroupKeyResolution, SessionEntry } from "../../config/sessions.js";
@@ -14,56 +7,88 @@ import { normalizeGroupActivation } from "../group-activation.js";
import type { TemplateContext } from "../templating.js";
import { extractExplicitGroupId } from "./group-id.js";
const WHATSAPP_GROUP_INTRO_HINT =
"WhatsApp IDs: SenderId is the participant JID (group participant id).";
const CHANNEL_LABELS: Partial<Record<ChannelId, string>> = {
bluebubbles: "BlueBubbles",
discord: "Discord",
imessage: "iMessage",
line: "LINE",
signal: "Signal",
slack: "Slack",
telegram: "Telegram",
webchat: "WebChat",
whatsapp: "WhatsApp",
};
let groupsRuntimePromise: Promise<typeof import("./groups.runtime.js")> | null = null;
function loadGroupsRuntime() {
groupsRuntimePromise ??= import("./groups.runtime.js");
return groupsRuntimePromise;
}
function resolveGroupId(raw: string | undefined | null): string | undefined {
const trimmed = (raw ?? "").trim();
return extractExplicitGroupId(trimmed) ?? (trimmed || undefined);
}
function resolveDockChannelId(raw?: string | null): ChannelId | null {
function resolveLooseChannelId(raw?: string | null): ChannelId | null {
const normalized = raw?.trim().toLowerCase();
if (!normalized) {
return null;
}
return normalized as ChannelId;
}
async function resolveRuntimeChannelId(raw?: string | null): Promise<ChannelId | null> {
const normalized = resolveLooseChannelId(raw);
if (!normalized) {
return null;
}
const { getChannelPlugin, normalizeChannelId } = await loadGroupsRuntime();
try {
if (getChannelPlugin(normalized as ChannelId)) {
return normalized as ChannelId;
if (getChannelPlugin(normalized)) {
return normalized;
}
} catch {
// Plugin registry may not be initialized in shared/test contexts.
}
try {
return normalizePluginChannelId(raw) ?? (normalized as ChannelId);
return normalizeChannelId(raw) ?? normalized;
} catch {
return normalized as ChannelId;
return normalized;
}
}
function resolveBuiltInRequireMentionFromConfig(params: {
async function resolveBuiltInRequireMentionFromConfig(params: {
cfg: OpenClawConfig;
channel: ChannelId;
groupChannel?: string;
groupId?: string;
groupSpace?: string;
accountId?: string | null;
}): boolean | undefined {
}): Promise<boolean | undefined> {
const runtime = await loadGroupsRuntime();
switch (params.channel) {
case "discord":
return resolveDiscordGroupRequireMention(params);
return runtime.resolveDiscordGroupRequireMention(params);
case "slack":
return resolveSlackGroupRequireMention(params);
return runtime.resolveSlackGroupRequireMention(params);
default:
return undefined;
}
}
export function resolveGroupRequireMention(params: {
export async function resolveGroupRequireMention(params: {
cfg: OpenClawConfig;
ctx: TemplateContext;
groupResolution?: GroupKeyResolution;
}): boolean {
}): Promise<boolean> {
const { cfg, ctx, groupResolution } = params;
const rawChannel = groupResolution?.channel ?? ctx.Provider?.trim();
const channel = resolveDockChannelId(rawChannel);
const channel = await resolveRuntimeChannelId(rawChannel);
if (!channel) {
return true;
}
@@ -71,8 +96,9 @@ export function resolveGroupRequireMention(params: {
const groupChannel = ctx.GroupChannel?.trim() ?? ctx.GroupSubject?.trim();
const groupSpace = ctx.GroupSpace?.trim();
let requireMention: boolean | undefined;
const runtime = await loadGroupsRuntime();
try {
requireMention = getChannelPlugin(channel)?.groups?.resolveRequireMention?.({
requireMention = runtime.getChannelPlugin(channel)?.groups?.resolveRequireMention?.({
cfg,
groupId,
groupChannel,
@@ -85,7 +111,7 @@ export function resolveGroupRequireMention(params: {
if (typeof requireMention === "boolean") {
return requireMention;
}
const builtInRequireMention = resolveBuiltInRequireMentionFromConfig({
const builtInRequireMention = await resolveBuiltInRequireMentionFromConfig({
cfg,
channel,
groupChannel,
@@ -108,9 +134,6 @@ export function defaultGroupActivation(requireMention: boolean): "always" | "men
return !requireMention ? "always" : "mention";
}
/**
* Resolve a human-readable provider label from the raw provider string.
*/
function resolveProviderLabel(rawProvider: string | undefined): string {
const providerKey = rawProvider?.trim().toLowerCase() ?? "";
if (!providerKey) {
@@ -119,20 +142,13 @@ function resolveProviderLabel(rawProvider: string | undefined): string {
if (isInternalMessageChannel(providerKey)) {
return "WebChat";
}
const providerId = resolveDockChannelId(rawProvider?.trim());
const providerId = resolveLooseChannelId(rawProvider?.trim());
if (providerId) {
return getChannelPlugin(providerId)?.meta.label ?? providerId;
return CHANNEL_LABELS[providerId] ?? providerId;
}
return `${providerKey.at(0)?.toUpperCase() ?? ""}${providerKey.slice(1)}`;
}
/**
* Build a persistent group-chat context block that is always included in the
* system prompt for group-chat sessions (every turn, not just the first).
*
* Contains: group name, participants, and an explicit instruction to reply
* directly instead of using the message tool.
*/
export function buildGroupChatContext(params: { sessionCtx: TemplateContext }): string {
const subject = params.sessionCtx.GroupSubject?.trim();
const members = params.sessionCtx.GroupMembers?.trim();
@@ -162,25 +178,12 @@ export function buildGroupIntro(params: {
}): string {
const activation =
normalizeGroupActivation(params.sessionEntry?.groupActivation) ?? params.defaultActivation;
const rawProvider = params.sessionCtx.Provider?.trim();
const providerId = resolveDockChannelId(rawProvider);
const providerId = resolveLooseChannelId(params.sessionCtx.Provider?.trim());
const activationLine =
activation === "always"
? "Activation: always-on (you receive every group message)."
: "Activation: trigger-only (you are invoked only when explicitly mentioned; recent context may be included).";
const groupId = params.sessionEntry?.groupId ?? resolveGroupId(params.sessionCtx.From);
const groupChannel =
params.sessionCtx.GroupChannel?.trim() ?? params.sessionCtx.GroupSubject?.trim();
const groupSpace = params.sessionCtx.GroupSpace?.trim();
const providerIdsLine = providerId
? (getChannelPlugin(providerId)?.groups?.resolveGroupIntroHint?.({
cfg: params.cfg,
groupId,
groupChannel,
groupSpace,
accountId: params.sessionCtx.AccountId,
}) ?? (providerId === "whatsapp" ? resolveWhatsAppGroupIntroHint() : undefined))
: undefined;
const providerIdsLine = providerId === "whatsapp" ? WHATSAPP_GROUP_INTRO_HINT : undefined;
const silenceLine =
activation === "always"
? `If no response is needed, reply with exactly "${params.silentToken}" (and nothing else) so OpenClaw stays silent. Do not add any other words, punctuation, tags, markdown/code blocks, or explanations.`

View File

@@ -1,5 +1,6 @@
import { resolveAgentConfig } from "../../agents/agent-scope.js";
import { getChannelPlugin, normalizeChannelId } from "../../channels/plugins/index.js";
import type { ChannelId } from "../../channels/plugins/types.js";
import type { OpenClawConfig } from "../../config/config.js";
import { createSubsystemLogger } from "../../logging/subsystem.js";
import { compileConfigRegexes, type ConfigRegexRejectReason } from "../../security/config-regex.js";
@@ -208,7 +209,10 @@ export function stripMentions(
agentId?: string,
): string {
let result = text;
const providerId = ctx.Provider ? normalizeChannelId(ctx.Provider) : null;
const providerId =
(ctx.Provider ? normalizeChannelId(ctx.Provider) : null) ??
(ctx.Provider?.trim().toLowerCase() as ChannelId | undefined) ??
null;
const providerMentions = providerId ? getChannelPlugin(providerId)?.mentions : undefined;
const configRegexes = compileMentionPatternsCached({
patterns: normalizeMentionPatterns(resolveMentionPatterns(cfg, agentId)),

View File

@@ -0,0 +1 @@
export { applyResetModelOverride } from "./session-reset-model.js";

View File

@@ -0,0 +1,20 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
describe("reply session module imports", () => {
beforeEach(() => {
vi.resetModules();
});
it("does not load archive runtime on module import", async () => {
const archiveRuntimeLoads = vi.fn();
vi.doMock("../../gateway/session-archive.runtime.js", async (importOriginal) => {
archiveRuntimeLoads();
return await importOriginal<typeof import("../../gateway/session-archive.runtime.js")>();
});
await import("./session.js");
expect(archiveRuntimeLoads).not.toHaveBeenCalled();
vi.doUnmock("../../gateway/session-archive.runtime.js");
});
});

View File

@@ -29,7 +29,6 @@ import {
type SessionScope,
} from "../../config/sessions/types.js";
import type { TtsAutoMode } from "../../config/types.tts.js";
import { archiveSessionTranscripts } from "../../gateway/session-archive.fs.js";
import { resolveConversationIdFromTargets } from "../../infra/outbound/conversation-id.js";
import { deliverSessionMaintenanceWarning } from "../../infra/session-maintenance-warning.js";
import { createSubsystemLogger } from "../../logging/subsystem.js";
@@ -51,6 +50,14 @@ import { forkSessionFromParent, resolveParentForkMaxTokens } from "./session-for
import { buildSessionEndHookPayload, buildSessionStartHookPayload } from "./session-hooks.js";
const log = createSubsystemLogger("session-init");
let sessionArchiveRuntimePromise: Promise<
typeof import("../../gateway/session-archive.runtime.js")
> | null = null;
function loadSessionArchiveRuntime() {
sessionArchiveRuntimePromise ??= import("../../gateway/session-archive.runtime.js");
return sessionArchiveRuntimePromise;
}
export type SessionInitResult = {
sessionCtx: TemplateContext;
@@ -573,6 +580,7 @@ export async function initSessionState(params: {
// Archive old transcript so it doesn't accumulate on disk (#14869).
if (previousSessionEntry?.sessionId) {
const { archiveSessionTranscripts } = await loadSessionArchiveRuntime();
archiveSessionTranscripts({
sessionId: previousSessionEntry.sessionId,
storePath,

View File

@@ -0,0 +1 @@
export { stageSandboxMedia } from "./stage-sandbox-media.js";

View File

@@ -1,5 +1,4 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { createDefaultDeps } from "./deps.js";
const moduleLoads = vi.hoisted(() => ({
whatsapp: vi.fn(),
@@ -19,6 +18,13 @@ const sendFns = vi.hoisted(() => ({
imessage: vi.fn(async () => ({ messageId: "i1", chatId: "imessage:1" })),
}));
const whatsappBoundaryLoads = vi.hoisted(() => vi.fn());
vi.mock("../plugins/runtime/runtime-whatsapp-boundary.js", async (importOriginal) => {
whatsappBoundaryLoads();
return await importOriginal<typeof import("../plugins/runtime/runtime-whatsapp-boundary.js")>();
});
vi.mock("./send-runtime/whatsapp.js", () => {
moduleLoads.whatsapp();
return { runtimeSend: { sendMessage: sendFns.whatsapp } };
@@ -50,6 +56,10 @@ vi.mock("./send-runtime/imessage.js", () => {
});
describe("createDefaultDeps", () => {
async function loadCreateDefaultDeps() {
return (await import("./deps.js")).createDefaultDeps;
}
function expectUnusedModulesNotLoaded(exclude: keyof typeof moduleLoads): void {
const keys = Object.keys(moduleLoads) as Array<keyof typeof moduleLoads>;
for (const key of keys) {
@@ -62,9 +72,11 @@ describe("createDefaultDeps", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.resetModules();
});
it("does not load provider modules until a dependency is used", async () => {
const createDefaultDeps = await loadCreateDefaultDeps();
const deps = createDefaultDeps();
expect(moduleLoads.whatsapp).not.toHaveBeenCalled();
@@ -83,6 +95,7 @@ describe("createDefaultDeps", () => {
});
it("reuses module cache after first dynamic import", async () => {
const createDefaultDeps = await loadCreateDefaultDeps();
const deps = createDefaultDeps();
const sendDiscord = deps["discord"] as (...args: unknown[]) => Promise<unknown>;
@@ -92,4 +105,10 @@ describe("createDefaultDeps", () => {
expect(moduleLoads.discord).toHaveBeenCalledTimes(1);
expect(sendFns.discord).toHaveBeenCalledTimes(2);
});
it("does not import the whatsapp runtime boundary on deps module load", async () => {
await import("./deps.js");
expect(whatsappBoundaryLoads).not.toHaveBeenCalled();
});
});

View File

@@ -38,6 +38,8 @@ function createLazySender(
}
export function createDefaultDeps(): CliDeps {
// Keep the default dependency barrel limited to lazy senders so callers that
// only need outbound deps do not pull channel runtime boundaries on import.
return {
whatsapp: createLazySender(
"whatsapp",
@@ -69,5 +71,3 @@ export function createDefaultDeps(): CliDeps {
export function createOutboundSendDeps(deps: CliDeps): OutboundSendDeps {
return createOutboundSendDepsFromCliSource(deps);
}
export { logWebSelfId } from "../plugins/runtime/runtime-whatsapp-boundary.js";

View File

@@ -0,0 +1,20 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
describe("session store module imports", () => {
beforeEach(() => {
vi.resetModules();
});
it("does not load archive runtime on module import", async () => {
const archiveRuntimeLoads = vi.fn();
vi.doMock("../gateway/session-archive.runtime.js", async (importOriginal) => {
archiveRuntimeLoads();
return await importOriginal<typeof import("../gateway/session-archive.runtime.js")>();
});
await import("./sessions/store.js");
expect(archiveRuntimeLoads).not.toHaveBeenCalled();
vi.doUnmock("../gateway/session-archive.runtime.js");
});
});

View File

@@ -2,10 +2,6 @@ import fs from "node:fs";
import path from "node:path";
import { acquireSessionWriteLock } from "../../agents/session-write-lock.js";
import type { MsgContext } from "../../auto-reply/templating.js";
import {
archiveSessionTranscripts,
cleanupArchivedSessionTranscripts,
} from "../../gateway/session-archive.fs.js";
import { writeTextAtomic } from "../../infra/json-files.js";
import { createSubsystemLogger } from "../../logging/subsystem.js";
import {
@@ -45,6 +41,14 @@ import {
} from "./types.js";
const log = createSubsystemLogger("sessions/store");
let sessionArchiveRuntimePromise: Promise<
typeof import("../../gateway/session-archive.runtime.js")
> | null = null;
function loadSessionArchiveRuntime() {
sessionArchiveRuntimePromise ??= import("../../gateway/session-archive.runtime.js");
return sessionArchiveRuntimePromise;
}
function isSessionStoreRecord(value: unknown): value is Record<string, SessionEntry> {
return !!value && typeof value === "object" && !Array.isArray(value);
@@ -472,7 +476,7 @@ async function saveSessionStoreUnlocked(
.map((entry) => entry?.sessionId)
.filter((id): id is string => Boolean(id)),
);
const archivedForDeletedSessions = archiveRemovedSessionTranscripts({
const archivedForDeletedSessions = await archiveRemovedSessionTranscripts({
removedSessionFiles,
referencedSessionIds,
storePath,
@@ -483,6 +487,7 @@ async function saveSessionStoreUnlocked(
archivedDirs.add(archivedDir);
}
if (archivedDirs.size > 0 || maintenance.resetArchiveRetentionMs != null) {
const { cleanupArchivedSessionTranscripts } = await loadSessionArchiveRuntime();
const targetDirs =
archivedDirs.size > 0 ? [...archivedDirs] : [path.dirname(path.resolve(storePath))];
await cleanupArchivedSessionTranscripts({
@@ -643,13 +648,14 @@ function rememberRemovedSessionFile(
}
}
export function archiveRemovedSessionTranscripts(params: {
export async function archiveRemovedSessionTranscripts(params: {
removedSessionFiles: Iterable<[string, string | undefined]>;
referencedSessionIds: ReadonlySet<string>;
storePath: string;
reason: "deleted" | "reset";
restrictToStoreDir?: boolean;
}): Set<string> {
}): Promise<Set<string>> {
const { archiveSessionTranscripts } = await loadSessionArchiveRuntime();
const archivedDirs = new Set<string>();
for (const [sessionId, sessionFile] of params.removedSessionFiles) {
if (params.referencedSessionIds.has(sessionId)) {

View File

@@ -116,7 +116,7 @@ export async function sweepCronRunSessions(params: {
.map((entry) => entry?.sessionId)
.filter((id): id is string => Boolean(id)),
);
const archivedDirs = archiveRemovedSessionTranscripts({
const archivedDirs = await archiveRemovedSessionTranscripts({
removedSessionFiles: prunedSessions,
referencedSessionIds,
storePath,

View File

@@ -1,21 +0,0 @@
import * as extensionApi from "openclaw/extension-api";
import { describe, expect, it } from "vitest";
describe("extension-api compat surface", () => {
it("keeps legacy agent helpers importable", () => {
expect(typeof extensionApi.runEmbeddedPiAgent).toBe("function");
expect(typeof extensionApi.resolveAgentDir).toBe("function");
expect(typeof extensionApi.resolveAgentWorkspaceDir).toBe("function");
expect(typeof extensionApi.resolveAgentTimeoutMs).toBe("function");
expect(typeof extensionApi.ensureAgentWorkspace).toBe("function");
});
it("keeps legacy defaults and session helpers importable", () => {
expect(typeof extensionApi.DEFAULT_MODEL).toBe("string");
expect(typeof extensionApi.DEFAULT_PROVIDER).toBe("string");
expect(typeof extensionApi.resolveStorePath).toBe("function");
expect(typeof extensionApi.loadSessionStore).toBe("function");
expect(typeof extensionApi.saveSessionStore).toBe("function");
expect(typeof extensionApi.resolveSessionFilePath).toBe("function");
});
});

View File

@@ -67,8 +67,11 @@ vi.mock("../../infra/agent-events.js", () => ({
onAgentEvent: vi.fn(),
}));
vi.mock("../../agents/subagent-registry.js", () => ({
vi.mock("../../agents/subagent-registry-read.js", () => ({
getLatestSubagentRunByChildSessionKey: mocks.getLatestSubagentRunByChildSessionKey,
}));
vi.mock("../session-subagent-reactivation.runtime.js", () => ({
replaceSubagentRunAfterSteer: mocks.replaceSubagentRunAfterSteer,
}));

View File

@@ -656,7 +656,7 @@ export const agentHandlers: GatewayRequestHandlers = {
respond(true, accepted, undefined, { runId });
if (resolvedSessionKey) {
reactivateCompletedSubagentSession({
await reactivateCompletedSubagentSession({
sessionKey: resolvedSessionKey,
runId,
});

View File

@@ -19,16 +19,19 @@ vi.mock("../session-utils.js", async (importOriginal) => {
};
});
vi.mock("../../agents/subagent-registry.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../agents/subagent-registry.js")>();
vi.mock("../../agents/subagent-registry-read.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../agents/subagent-registry-read.js")>();
return {
...actual,
getLatestSubagentRunByChildSessionKey: (...args: unknown[]) =>
getLatestSubagentRunByChildSessionKeyMock(...args),
replaceSubagentRunAfterSteer: (...args: unknown[]) => replaceSubagentRunAfterSteerMock(...args),
};
});
vi.mock("../session-subagent-reactivation.runtime.js", () => ({
replaceSubagentRunAfterSteer: (...args: unknown[]) => replaceSubagentRunAfterSteerMock(...args),
}));
vi.mock("./chat.js", () => ({
chatHandlers: {
"chat.send": (...args: unknown[]) => chatSendMock(...args),

View File

@@ -467,7 +467,7 @@ async function handleSessionSend(params: {
});
if (sendAcked) {
if (shouldAttachPendingMessageSeq({ payload: sendPayload, cached: sendCached })) {
reactivateCompletedSubagentSession({
await reactivateCompletedSubagentSession({
sessionKey: canonicalKey,
runId: startedRunId,
});

View File

@@ -0,0 +1,4 @@
export {
archiveSessionTranscripts,
cleanupArchivedSessionTranscripts,
} from "./session-archive.fs.js";

View File

@@ -0,0 +1 @@
export { replaceSubagentRunAfterSteer } from "../agents/subagent-registry.js";

View File

@@ -3,16 +3,19 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
const getLatestSubagentRunByChildSessionKeyMock = vi.fn();
const replaceSubagentRunAfterSteerMock = vi.fn();
vi.mock("../agents/subagent-registry.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../agents/subagent-registry.js")>();
vi.mock("../agents/subagent-registry-read.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../agents/subagent-registry-read.js")>();
return {
...actual,
getLatestSubagentRunByChildSessionKey: (...args: unknown[]) =>
getLatestSubagentRunByChildSessionKeyMock(...args),
replaceSubagentRunAfterSteer: (...args: unknown[]) => replaceSubagentRunAfterSteerMock(...args),
};
});
vi.mock("./session-subagent-reactivation.runtime.js", () => ({
replaceSubagentRunAfterSteer: (...args: unknown[]) => replaceSubagentRunAfterSteerMock(...args),
}));
import { reactivateCompletedSubagentSession } from "./session-subagent-reactivation.js";
describe("reactivateCompletedSubagentSession", () => {
@@ -21,7 +24,7 @@ describe("reactivateCompletedSubagentSession", () => {
replaceSubagentRunAfterSteerMock.mockReset();
});
it("reactivates the newest ended row even when stale active rows still exist for the same child session", () => {
it("reactivates the newest ended row even when stale active rows still exist for the same child session", async () => {
const childSessionKey = "agent:main:subagent:followup-race";
const latestEndedRun = {
runId: "run-current-ended",
@@ -39,12 +42,12 @@ describe("reactivateCompletedSubagentSession", () => {
getLatestSubagentRunByChildSessionKeyMock.mockReturnValue(latestEndedRun);
replaceSubagentRunAfterSteerMock.mockReturnValue(true);
expect(
await expect(
reactivateCompletedSubagentSession({
sessionKey: childSessionKey,
runId: "run-next",
}),
).toBe(true);
).resolves.toBe(true);
expect(getLatestSubagentRunByChildSessionKeyMock).toHaveBeenCalledWith(childSessionKey);
expect(replaceSubagentRunAfterSteerMock).toHaveBeenCalledWith({

View File

@@ -1,12 +1,13 @@
import {
getLatestSubagentRunByChildSessionKey,
replaceSubagentRunAfterSteer,
} from "../agents/subagent-registry.js";
import { getLatestSubagentRunByChildSessionKey } from "../agents/subagent-registry-read.js";
export function reactivateCompletedSubagentSession(params: {
async function loadSessionSubagentReactivationRuntime() {
return import("./session-subagent-reactivation.runtime.js");
}
export async function reactivateCompletedSubagentSession(params: {
sessionKey: string;
runId?: string;
}): boolean {
}): Promise<boolean> {
const runId = params.runId?.trim();
if (!runId) {
return false;
@@ -15,6 +16,7 @@ export function reactivateCompletedSubagentSession(params: {
if (!existing || typeof existing.endedAt !== "number") {
return false;
}
const { replaceSubagentRunAfterSteer } = await loadSessionSubagentReactivationRuntime();
return replaceSubagentRunAfterSteer({
previousRunId: existing.runId,
nextRunId: runId,

View File

@@ -15,7 +15,7 @@ import {
getSubagentSessionStartedAt,
listSubagentRunsForController,
resolveSubagentSessionStatus,
} from "../agents/subagent-registry.js";
} from "../agents/subagent-registry-read.js";
import { type OpenClawConfig, loadConfig } from "../config/config.js";
import { resolveStateDir } from "../config/paths.js";
import {

43
src/library.test.ts Normal file
View File

@@ -0,0 +1,43 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
describe("library module imports", () => {
beforeEach(() => {
vi.resetModules();
});
it("does not load lazy runtimes on module import", async () => {
const replyRuntimeLoads = vi.fn();
const promptRuntimeLoads = vi.fn();
const binariesRuntimeLoads = vi.fn();
const whatsappRuntimeLoads = vi.fn();
vi.doMock("./auto-reply/reply.runtime.js", async (importOriginal) => {
replyRuntimeLoads();
return await importOriginal<typeof import("./auto-reply/reply.runtime.js")>();
});
vi.doMock("./cli/prompt.js", async (importOriginal) => {
promptRuntimeLoads();
return await importOriginal<typeof import("./cli/prompt.js")>();
});
vi.doMock("./infra/binaries.js", async (importOriginal) => {
binariesRuntimeLoads();
return await importOriginal<typeof import("./infra/binaries.js")>();
});
vi.doMock("./plugins/runtime/runtime-whatsapp-boundary.js", async (importOriginal) => {
whatsappRuntimeLoads();
return await importOriginal<
typeof import("./plugins/runtime/runtime-whatsapp-boundary.js")
>();
});
await import("./library.js");
expect(replyRuntimeLoads).not.toHaveBeenCalled();
expect(promptRuntimeLoads).not.toHaveBeenCalled();
expect(binariesRuntimeLoads).not.toHaveBeenCalled();
expect(whatsappRuntimeLoads).not.toHaveBeenCalled();
vi.doUnmock("./auto-reply/reply.runtime.js");
vi.doUnmock("./cli/prompt.js");
vi.doUnmock("./infra/binaries.js");
vi.doUnmock("./plugins/runtime/runtime-whatsapp-boundary.js");
});
});

View File

@@ -1,47 +1,85 @@
import { getReplyFromConfig } from "./auto-reply/reply.js";
import { applyTemplate } from "./auto-reply/templating.js";
import { createDefaultDeps } from "./cli/deps.js";
import { promptYesNo } from "./cli/prompt.js";
import { waitForever } from "./cli/wait.js";
import { loadConfig } from "./config/config.js";
import {
deriveSessionKey,
loadSessionStore,
resolveSessionKey,
resolveStorePath,
saveSessionStore,
} from "./config/sessions.js";
import { ensureBinary } from "./infra/binaries.js";
import { resolveStorePath } from "./config/sessions/paths.js";
import { deriveSessionKey, resolveSessionKey } from "./config/sessions/session-key.js";
import { loadSessionStore, saveSessionStore } from "./config/sessions/store.js";
import {
describePortOwner,
ensurePortAvailable,
handlePortError,
PortInUseError,
} from "./infra/ports.js";
import { monitorWebChannel } from "./plugins/runtime/runtime-whatsapp-boundary.js";
import { runCommandWithTimeout, runExec } from "./process/exec.js";
import { assertWebChannel, normalizeE164, toWhatsappJid } from "./utils.js";
type GetReplyFromConfig = typeof import("./auto-reply/reply.runtime.js").getReplyFromConfig;
type PromptYesNo = typeof import("./cli/prompt.js").promptYesNo;
type EnsureBinary = typeof import("./infra/binaries.js").ensureBinary;
type RunExec = typeof import("./process/exec.js").runExec;
type RunCommandWithTimeout = typeof import("./process/exec.js").runCommandWithTimeout;
type MonitorWebChannel =
typeof import("./plugins/runtime/runtime-whatsapp-boundary.js").monitorWebChannel;
let replyRuntimePromise: Promise<typeof import("./auto-reply/reply.runtime.js")> | null = null;
let promptRuntimePromise: Promise<typeof import("./cli/prompt.js")> | null = null;
let binariesRuntimePromise: Promise<typeof import("./infra/binaries.js")> | null = null;
let execRuntimePromise: Promise<typeof import("./process/exec.js")> | null = null;
let whatsappRuntimePromise: Promise<
typeof import("./plugins/runtime/runtime-whatsapp-boundary.js")
> | null = null;
function loadReplyRuntime() {
replyRuntimePromise ??= import("./auto-reply/reply.runtime.js");
return replyRuntimePromise;
}
function loadPromptRuntime() {
promptRuntimePromise ??= import("./cli/prompt.js");
return promptRuntimePromise;
}
function loadBinariesRuntime() {
binariesRuntimePromise ??= import("./infra/binaries.js");
return binariesRuntimePromise;
}
function loadExecRuntime() {
execRuntimePromise ??= import("./process/exec.js");
return execRuntimePromise;
}
function loadWhatsAppRuntime() {
whatsappRuntimePromise ??= import("./plugins/runtime/runtime-whatsapp-boundary.js");
return whatsappRuntimePromise;
}
export const getReplyFromConfig: GetReplyFromConfig = async (...args) =>
(await loadReplyRuntime()).getReplyFromConfig(...args);
export const promptYesNo: PromptYesNo = async (...args) =>
(await loadPromptRuntime()).promptYesNo(...args);
export const ensureBinary: EnsureBinary = async (...args) =>
(await loadBinariesRuntime()).ensureBinary(...args);
export const runExec: RunExec = async (...args) => (await loadExecRuntime()).runExec(...args);
export const runCommandWithTimeout: RunCommandWithTimeout = async (...args) =>
(await loadExecRuntime()).runCommandWithTimeout(...args);
export const monitorWebChannel: MonitorWebChannel = async (...args) =>
(await loadWhatsAppRuntime()).monitorWebChannel(...args);
export {
assertWebChannel,
applyTemplate,
createDefaultDeps,
deriveSessionKey,
describePortOwner,
ensureBinary,
ensurePortAvailable,
getReplyFromConfig,
handlePortError,
loadConfig,
loadSessionStore,
monitorWebChannel,
normalizeE164,
PortInUseError,
promptYesNo,
resolveSessionKey,
resolveStorePath,
runCommandWithTimeout,
runExec,
saveSessionStore,
toWhatsappJid,
waitForever,