diff --git a/src/agents/bash-tools.exec-approval-request.ts b/src/agents/bash-tools.exec-approval-request.ts index d2ba7ac01de5..5d447f9efc29 100644 --- a/src/agents/bash-tools.exec-approval-request.ts +++ b/src/agents/bash-tools.exec-approval-request.ts @@ -24,23 +24,19 @@ import { POSIX_SHELL_WRAPPERS, resolveShellWrapperTransportArgv, } from "../infra/shell-wrapper-resolution.js"; +import { createLazyPromise } from "../shared/lazy-runtime.js"; import { DEFAULT_APPROVAL_REQUEST_TIMEOUT_MS, DEFAULT_APPROVAL_TIMEOUT_MS, } from "./bash-tools.exec-runtime.js"; import { callGatewayTool } from "./tools/gateway.js"; -type ExecApprovalCommandSpansRuntime = - typeof import("./bash-tools.exec-approval-request.runtime.js"); - -let execApprovalCommandSpansRuntimePromise: Promise | null = null; const POSIX_COMMAND_HIGHLIGHT_SHELLS: ReadonlySet = POSIX_SHELL_WRAPPERS; -function loadExecApprovalCommandSpansRuntime(): Promise { - execApprovalCommandSpansRuntimePromise ??= - import("./bash-tools.exec-approval-request.runtime.js"); - return execApprovalCommandSpansRuntimePromise; -} +const loadExecApprovalCommandSpansRuntime = createLazyPromise( + () => import("./bash-tools.exec-approval-request.runtime.js"), + { cacheRejections: true }, +); /** Gateway payload fields used to register or wait for an exec approval decision. */ type RequestExecApprovalDecisionParams = { diff --git a/src/agents/code-mode.ts b/src/agents/code-mode.ts index de3ed6eb772a..e4ed6fac25a7 100644 --- a/src/agents/code-mode.ts +++ b/src/agents/code-mode.ts @@ -14,6 +14,7 @@ import { isRecord } from "@openclaw/normalization-core/record-coerce"; import { uniqueValues } from "@openclaw/normalization-core/string-normalization"; import { Type } from "typebox"; import type { OpenClawConfig } from "../config/types.openclaw.js"; +import { createLazyPromiseLoader } from "../shared/lazy-runtime.js"; import { resolveAgentConfig } from "./agent-scope-config.js"; import type { HookContext } from "./agent-tools.before-tool-call.js"; import { @@ -151,7 +152,9 @@ type CodeModeWorkerResult = const activeRuns = new Map(); const resumingRunIds = new Set(); let activeRunReservations = 0; -let typescriptRuntimePromise: Promise | null = null; +const typescriptRuntimeLoader = createLazyPromiseLoader(() => import("typescript"), { + cacheRejections: true, +}); let typescriptRuntimeForTest: typeof import("typescript") | null = null; function normalizeCodeModeRawConfig(value: unknown): Record | undefined { @@ -438,8 +441,7 @@ async function loadTypeScriptRuntime(): Promise { if (typescriptRuntimeForTest) { return typescriptRuntimeForTest; } - typescriptRuntimePromise ??= import("typescript"); - return await typescriptRuntimePromise; + return await typescriptRuntimeLoader.load(); } async function prepareSource(input: { @@ -1274,7 +1276,8 @@ export const testing = { runCodeModeWorker, resolveCodeModeWorkerUrl, resolveCodeModeConfig, - getTypescriptRuntimePromise: () => typescriptRuntimePromise, + getTypescriptRuntimePromise: (): Promise | null => + typescriptRuntimeLoader.peek() ?? null, setTypescriptRuntimeForTest: (runtime: typeof import("typescript") | null) => { typescriptRuntimeForTest = runtime; }, diff --git a/src/agents/embedded-agent-runner/run/attempt.spawn-workspace.test-support.ts b/src/agents/embedded-agent-runner/run/attempt.spawn-workspace.test-support.ts index dd5994d1c471..b081acd34e80 100644 --- a/src/agents/embedded-agent-runner/run/attempt.spawn-workspace.test-support.ts +++ b/src/agents/embedded-agent-runner/run/attempt.spawn-workspace.test-support.ts @@ -19,6 +19,7 @@ import type { import { formatErrorMessage } from "../../../infra/errors.js"; import type { Model } from "../../../llm/types.js"; import type { PluginMetadataSnapshot } from "../../../plugins/plugin-metadata-snapshot.js"; +import { createLazyPromise } from "../../../shared/lazy-runtime.js"; import type { EmbeddedContextFile } from "../../embedded-agent-helpers.js"; import type { MessagingToolSend, @@ -924,18 +925,15 @@ function createCompletedAssistantStream(): TestAgentStream { }, }; } - -let runEmbeddedAttemptPromise: - | Promise - | undefined; const ATTEMPT_SPAWN_WORKSPACE_TEST_SPECIFIER = "./attempt.ts?spawn-workspace-test"; -async function loadRunEmbeddedAttempt() { - runEmbeddedAttemptPromise ??= ( - import(ATTEMPT_SPAWN_WORKSPACE_TEST_SPECIFIER) as Promise - ).then((mod) => mod.runEmbeddedAttempt); - return await runEmbeddedAttemptPromise; -} +const loadRunEmbeddedAttempt = createLazyPromise( + () => + (import(ATTEMPT_SPAWN_WORKSPACE_TEST_SPECIFIER) as Promise).then( + (mod) => mod.runEmbeddedAttempt, + ), + { cacheRejections: true }, +); export async function preloadRunEmbeddedAttemptForTests(): Promise { await loadRunEmbeddedAttempt(); diff --git a/src/cli/devices-cli.ts b/src/cli/devices-cli.ts index cbed91bfc819..40a17adcf91a 100644 --- a/src/cli/devices-cli.ts +++ b/src/cli/devices-cli.ts @@ -1,5 +1,6 @@ // Commander registration for device pairing and auth-token commands. import type { Command } from "commander"; +import { createLazyRuntimeModule } from "../shared/lazy-runtime.js"; import { applyParentDefaultHelpAction } from "./program/parent-default-help.js"; type DevicesRpcOpts = { @@ -18,14 +19,8 @@ type DevicesRpcOpts = { const DEFAULT_DEVICES_TIMEOUT_MS = 10_000; -type DevicesRuntimeModule = typeof import("./devices-cli.runtime.js"); - -let devicesRuntimePromise: Promise | undefined; - -function loadDevicesRuntime(): Promise { - // Keep device-pairing crypto/table dependencies out of root help startup. - return (devicesRuntimePromise ??= import("./devices-cli.runtime.js")); -} +// Keep device-pairing crypto/table dependencies out of root help startup. +const loadDevicesRuntime = createLazyRuntimeModule(() => import("./devices-cli.runtime.js")); const devicesCallOpts = (cmd: Command, defaults?: { timeoutMs?: number }) => cmd diff --git a/src/cli/program/register.agent.ts b/src/cli/program/register.agent.ts index 74ea7fc1aa98..3e8e612a7ea9 100644 --- a/src/cli/program/register.agent.ts +++ b/src/cli/program/register.agent.ts @@ -2,6 +2,7 @@ import type { Command } from "commander"; import { formatDocsLink } from "../../../packages/terminal-core/src/links.js"; import { theme } from "../../../packages/terminal-core/src/theme.js"; +import { createLazyRuntimeModule } from "../../shared/lazy-runtime.js"; import { hasExplicitOptions } from "../command-options.js"; import { formatHelpExamples } from "../help-format.js"; import { collectOption } from "./helpers.js"; @@ -14,11 +15,9 @@ type AgentsListModule = typeof import("../../commands/agents.commands.list.js"); type CliUtilsModule = typeof import("../cli-utils.js"); type RuntimeModule = typeof import("../../runtime.js"); -let agentsBindModulePromise: Promise | undefined; - -function loadAgentsBindModule(): Promise { - return (agentsBindModulePromise ??= import("../../commands/agents.commands.bind.js")); -} +const loadAgentsBindModule = createLazyRuntimeModule( + () => import("../../commands/agents.commands.bind.js"), +); async function loadAgentsAddCommand(): Promise { return (await import("../../commands/agents.commands.add.js")).agentsAddCommand; diff --git a/src/commands/agent-via-gateway.ts b/src/commands/agent-via-gateway.ts index 81824459371d..1c0782d31e0c 100644 --- a/src/commands/agent-via-gateway.ts +++ b/src/commands/agent-via-gateway.ts @@ -37,6 +37,7 @@ import { scopeLegacySessionKeyToAgent, } from "../routing/session-key.js"; import { type RuntimeEnv, writeRuntimeJson } from "../runtime.js"; +import { createLazyPromiseLoader } from "../shared/lazy-runtime.js"; import { normalizeMessageChannel } from "../utils/message-channel-normalize.js"; type AgentGatewayResult = { @@ -104,9 +105,7 @@ type AgentGatewayCallIdentity = Pick< Parameters[0], "clientName" | "mode" | "scopes" >; -type EmbeddedAgentCommandModule = typeof import("./agent.js"); type AgentSessionModule = typeof import("./agent/session.js"); -type RuntimeConfigModule = typeof import("../config/io.js"); type AgentSessionModuleLoader = () => Promise; const AGENT_CLI_SIGNALS: readonly AgentCliSignal[] = ["SIGINT", "SIGTERM"]; @@ -118,53 +117,50 @@ const AGENT_CLI_SIGNAL_EXIT_CODES: Record = { }; const MESSAGE_FILE_DECODER = new TextDecoder("utf-8", { fatal: true }); -let embeddedAgentCommandPromise: Promise | undefined; -let agentSessionModulePromise: Promise | undefined; -let runtimeConfigModulePromise: Promise | undefined; -let replyPayloadModulePromise: - | Promise - | undefined; const defaultAgentSessionModuleLoader: AgentSessionModuleLoader = () => import("./agent/session.js"); let agentSessionModuleLoader: AgentSessionModuleLoader = defaultAgentSessionModuleLoader; +const embeddedAgentCommandLoader = createLazyPromiseLoader( + () => import("./agent.js").then((module) => module.agentCommand), + { cacheRejections: true }, +); +const agentSessionModuleCache = createLazyPromiseLoader(() => agentSessionModuleLoader(), { + cacheRejections: true, +}); +const runtimeConfigModuleLoader = createLazyPromiseLoader(() => import("../config/io.js"), { + cacheRejections: true, +}); +const replyPayloadModuleLoader = createLazyPromiseLoader( + () => import("openclaw/plugin-sdk/reply-payload"), + { cacheRejections: true }, +); let gatewayAbortRetryDelaysMsForTests: readonly number[] | undefined; function resolveGatewayAbortRetryDelaysMs(): readonly number[] { return gatewayAbortRetryDelaysMsForTests ?? GATEWAY_ABORT_RETRY_DELAYS_MS; } -function loadEmbeddedAgentCommand(): Promise { - embeddedAgentCommandPromise ??= import("./agent.js").then((module) => module.agentCommand); - return embeddedAgentCommandPromise; -} - -function loadAgentSessionModule(): Promise { - agentSessionModulePromise ??= agentSessionModuleLoader(); - return agentSessionModulePromise; -} +const loadEmbeddedAgentCommand = embeddedAgentCommandLoader.load; +const loadAgentSessionModule = agentSessionModuleCache.load; async function loadRuntimeConfig(): Promise { - runtimeConfigModulePromise ??= import("../config/io.js"); - const { getRuntimeConfig } = await runtimeConfigModulePromise; + const { getRuntimeConfig } = await runtimeConfigModuleLoader.load(); return getRuntimeConfig(); } -function loadReplyPayloadModule() { - replyPayloadModulePromise ??= import("openclaw/plugin-sdk/reply-payload"); - return replyPayloadModulePromise; -} +const loadReplyPayloadModule = replyPayloadModuleLoader.load; /** Test-only hooks for resetting lazy imports and shortening retry timing. */ export const agentViaGatewayTesting = { resetLazyImportsForTests(): void { - embeddedAgentCommandPromise = undefined; - agentSessionModulePromise = undefined; - runtimeConfigModulePromise = undefined; - replyPayloadModulePromise = undefined; + embeddedAgentCommandLoader.clear(); + agentSessionModuleCache.clear(); + runtimeConfigModuleLoader.clear(); + replyPayloadModuleLoader.clear(); agentSessionModuleLoader = defaultAgentSessionModuleLoader; }, setAgentSessionModuleLoaderForTests(loader: AgentSessionModuleLoader): void { - agentSessionModulePromise = undefined; + agentSessionModuleCache.clear(); agentSessionModuleLoader = loader; }, resolveGatewayAgentTimeoutMs, diff --git a/src/commands/daemon-install-helpers.ts b/src/commands/daemon-install-helpers.ts index 3a178408daaa..a136e4b0aeb2 100644 --- a/src/commands/daemon-install-helpers.ts +++ b/src/commands/daemon-install-helpers.ts @@ -43,6 +43,7 @@ import { import { collectPluginConfigAssignments } from "../secrets/runtime-config-collectors-plugins.js"; import { createResolverContext } from "../secrets/runtime-shared.js"; import { discoverConfigSecretTargets } from "../secrets/target-registry.js"; +import { createLazyPromise } from "../shared/lazy-runtime.js"; import { emitDaemonInstallRuntimeWarning, resolveDaemonInstallRuntimeInputs, @@ -60,13 +61,6 @@ type GatewayInstallPlan = { environmentValueSources?: Record; }; -let daemonInstallAuthProfileSourceRuntimePromise: - | Promise - | undefined; -let daemonInstallAuthProfileStoreRuntimePromise: - | Promise - | undefined; - const NON_PERSISTED_CONFIG_SECRET_ENV_TARGET_IDS = new Set([ "gateway.auth.password", "gateway.auth.token", @@ -83,17 +77,15 @@ function isBlockedExecSecretRefPassEnvKey(key: string): boolean { return !EXEC_SECRET_REF_PASS_ENV_ALLOWED_OVERRIDE_ONLY_KEYS.has(key.toUpperCase()); } -function loadDaemonInstallAuthProfileSourceRuntime() { - daemonInstallAuthProfileSourceRuntimePromise ??= - import("./daemon-install-auth-profiles-source.runtime.js"); - return daemonInstallAuthProfileSourceRuntimePromise; -} +const loadDaemonInstallAuthProfileSourceRuntime = createLazyPromise( + () => import("./daemon-install-auth-profiles-source.runtime.js"), + { cacheRejections: true }, +); -function loadDaemonInstallAuthProfileStoreRuntime() { - daemonInstallAuthProfileStoreRuntimePromise ??= - import("./daemon-install-auth-profiles-store.runtime.js"); - return daemonInstallAuthProfileStoreRuntimePromise; -} +const loadDaemonInstallAuthProfileStoreRuntime = createLazyPromise( + () => import("./daemon-install-auth-profiles-store.runtime.js"), + { cacheRejections: true }, +); async function resolveAuthProfileStoreForServiceEnv( authStore: AuthProfileStore | undefined, diff --git a/src/commands/doctor-config-preflight.ts b/src/commands/doctor-config-preflight.ts index 7c49b346e250..b9e8af62cbe0 100644 --- a/src/commands/doctor-config-preflight.ts +++ b/src/commands/doctor-config-preflight.ts @@ -11,26 +11,17 @@ import { formatConfigIssueLines } from "../config/issue-format.js"; import type { ConfigFileSnapshot, LegacyConfigIssue } from "../config/types.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { isTruthyEnvValue } from "../infra/env.js"; +import { createLazyRuntimeModule } from "../shared/lazy-runtime.js"; import { resolveHomeDir } from "../utils.js"; import { noteIncludeConfinementWarning } from "./doctor-config-analysis.js"; import { findDoctorLegacyConfigIssues } from "./doctor/shared/legacy-config-issues.js"; import { resolveStateMigrationConfigInput } from "./doctor/shared/legacy-config-state-migration-input.js"; -type DoctorStateMigrationsModule = typeof import("./doctor-state-migrations.js"); -type DoctorCronModule = typeof import("./doctor/cron/index.js"); +const loadDoctorStateMigrations = createLazyRuntimeModule( + () => import("./doctor-state-migrations.js"), +); -let doctorStateMigrationsPromise: Promise | null = null; -let doctorCronPromise: Promise | null = null; - -function loadDoctorStateMigrations(): Promise { - doctorStateMigrationsPromise ??= import("./doctor-state-migrations.js"); - return doctorStateMigrationsPromise; -} - -function loadDoctorCron(): Promise { - doctorCronPromise ??= import("./doctor/cron/index.js"); - return doctorCronPromise; -} +const loadDoctorCron = createLazyRuntimeModule(() => import("./doctor/cron/index.js")); async function maybeMigrateLegacyConfig(): Promise { const changes: string[] = []; diff --git a/src/config/doc-baseline.ts b/src/config/doc-baseline.ts index e9011172a13f..63624f195e63 100644 --- a/src/config/doc-baseline.ts +++ b/src/config/doc-baseline.ts @@ -7,6 +7,7 @@ import { fileURLToPath } from "node:url"; import { sortUniqueStrings } from "@openclaw/normalization-core/string-normalization"; import { resolveOpenClawPackageRootSync } from "../infra/openclaw-root.js"; import { replaceFileAtomicSync } from "../infra/replace-file.js"; +import { createLazyRuntimeModule } from "../shared/lazy-runtime.js"; import type { ConfigSchemaResponse } from "./schema.js"; import { schemaHasChildren } from "./schema.shared.js"; @@ -91,8 +92,6 @@ const DEFAULT_CHANNEL_OUTPUT = "docs/.generated/config-baseline.channel.json"; const DEFAULT_PLUGIN_OUTPUT = "docs/.generated/config-baseline.plugin.json"; const DEFAULT_HASH_OUTPUT = "docs/.generated/config-baseline.sha256"; let cachedConfigDocBaselinePromise: Promise | null = null; -let cachedDocBaselineRuntimePromise: Promise | null = - null; const uiHintIndexCache = new WeakMap< ConfigSchemaResponse["uiHints"], Map< @@ -123,10 +122,7 @@ function resolveRepoRoot(): string { return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); } -async function loadDocBaselineRuntime() { - cachedDocBaselineRuntimePromise ??= import("./doc-baseline.runtime.js"); - return await cachedDocBaselineRuntimePromise; -} +const loadDocBaselineRuntime = createLazyRuntimeModule(() => import("./doc-baseline.runtime.js")); function normalizeBaselinePath(rawPath: string): string { return rawPath diff --git a/src/config/sessions/session-accessor.ts b/src/config/sessions/session-accessor.ts index 4b6df9dc2711..b0f76c1f5955 100644 --- a/src/config/sessions/session-accessor.ts +++ b/src/config/sessions/session-accessor.ts @@ -20,6 +20,7 @@ import type { SessionTranscriptUpdate, SessionTranscriptUpdateTarget, } from "../../sessions/transcript-events.js"; +import { createLazyRuntimeModule } from "../../shared/lazy-runtime.js"; import type { OpenClawConfig } from "../types.openclaw.js"; import { formatSessionArchiveTimestamp } from "./artifacts.js"; import { extractGeneratedTranscriptSessionId } from "./generated-transcript-session-id.js"; @@ -441,14 +442,9 @@ type SessionEntryRetirement = { key: string; }; -let sessionArchiveRuntimePromise: Promise< - typeof import("../../gateway/session-archive.runtime.js") -> | null = null; - -function loadSessionArchiveRuntime() { - sessionArchiveRuntimePromise ??= import("../../gateway/session-archive.runtime.js"); - return sessionArchiveRuntimePromise; -} +const loadSessionArchiveRuntime = createLazyRuntimeModule( + () => import("../../gateway/session-archive.runtime.js"), +); export type SessionEntryPatchOptions = { /** Entry to synthesize when a patch operation is allowed to create. */ diff --git a/src/config/sessions/store.ts b/src/config/sessions/store.ts index 4bbe97377342..bbf1c7863b0c 100644 --- a/src/config/sessions/store.ts +++ b/src/config/sessions/store.ts @@ -7,6 +7,7 @@ import { resolveStoredSessionOwnerAgentId } from "../../gateway/session-store-ke import { writeTextAtomic } from "../../infra/json-files.js"; import { createSubsystemLogger } from "../../logging/subsystem.js"; import { emitSessionTranscriptUpdate } from "../../sessions/transcript-events.js"; +import { createLazyRuntimeModule } from "../../shared/lazy-runtime.js"; import { deliveryContextFromChannelRoute, deliveryContextFromSession, @@ -115,26 +116,18 @@ export type SessionEntryPatchProjectionResult | null = null; -let trajectoryCleanupRuntimePromise: Promise | null = - null; const writerStoreFileStats = new WeakMap< Record, ReturnType | null >(); -function loadSessionArchiveRuntime() { - // Archive cleanup is a cold maintenance path, so keep it lazy to avoid gateway import cycles. - sessionArchiveRuntimePromise ??= import("../../gateway/session-archive.runtime.js"); - return sessionArchiveRuntimePromise; -} +const loadSessionArchiveRuntime = createLazyRuntimeModule( + () => import("../../gateway/session-archive.runtime.js"), +); -function loadTrajectoryCleanupRuntime() { - trajectoryCleanupRuntimePromise ??= import("../../trajectory/cleanup.js"); - return trajectoryCleanupRuntimePromise; -} +const loadTrajectoryCleanupRuntime = createLazyRuntimeModule( + () => import("../../trajectory/cleanup.js"), +); function removeThreadFromDeliveryContext(context?: DeliveryContext): DeliveryContext | undefined { if (!context || context.threadId == null) { diff --git a/src/flows/doctor-health.ts b/src/flows/doctor-health.ts index 198de8cdd3d2..d4b87afb6716 100644 --- a/src/flows/doctor-health.ts +++ b/src/flows/doctor-health.ts @@ -3,19 +3,14 @@ import { intro as clackIntro, outro as clackOutro } from "@clack/prompts"; import { stylePromptTitle } from "../../packages/terminal-core/src/prompt-style.js"; import type { DoctorOptions } from "../commands/doctor-prompter.js"; import type { RuntimeEnv } from "../runtime.js"; +import { createLazyRuntimeModule } from "../shared/lazy-runtime.js"; import type { DoctorHealthFlowContext } from "./doctor-health-contributions.js"; // Interactive doctor entrypoint; lazy imports keep normal CLI startup light. const intro = (message: string) => clackIntro(stylePromptTitle(message) ?? message); const outro = (message: string) => clackOutro(stylePromptTitle(message) ?? message); -type ConfigModule = typeof import("../config/config.js"); - -let configModulePromise: Promise | undefined; - -function loadConfigModule(): Promise { - return (configModulePromise ??= import("../config/config.js")); -} +const loadConfigModule = createLazyRuntimeModule(() => import("../config/config.js")); /** Runs the full interactive doctor flow against the provided or default runtime. */ export async function doctorCommand(runtime?: RuntimeEnv, options: DoctorOptions = {}) { diff --git a/src/hooks/install.ts b/src/hooks/install.ts index 104c3100a530..380c0a216be2 100644 --- a/src/hooks/install.ts +++ b/src/hooks/install.ts @@ -13,15 +13,11 @@ import { } from "../plugins/install-security-scan.js"; import { PLUGIN_MANIFEST_FILENAME } from "../plugins/manifest.js"; import type { InstallPolicySource } from "../security/install-policy.js"; +import { createLazyRuntimeModule } from "../shared/lazy-runtime.js"; import { CONFIG_DIR, resolveUserPath } from "../utils.js"; import { parseFrontmatter } from "./frontmatter.js"; -let hookInstallRuntimePromise: Promise | undefined; - -async function loadHookInstallRuntime() { - hookInstallRuntimePromise ??= import("./install.runtime.js"); - return hookInstallRuntimePromise; -} +const loadHookInstallRuntime = createLazyRuntimeModule(() => import("./install.runtime.js")); /** Logger contract used by hook install and update operations. */ export type HookInstallLogger = { diff --git a/src/infra/backup-create.ts b/src/infra/backup-create.ts index 5a89cd804c0a..e5009802b2e5 100644 --- a/src/infra/backup-create.ts +++ b/src/infra/backup-create.ts @@ -15,6 +15,7 @@ import { resolveBackupPlanFromDisk, } from "../commands/backup-shared.js"; import { isPathWithin } from "../commands/cleanup-utils.js"; +import { createLazyRuntimeModule } from "../shared/lazy-runtime.js"; import { resolveOpenClawStateSqlitePath } from "../state/openclaw-state-db.paths.js"; import { resolveHomeDir, resolveUserPath } from "../utils.js"; import { resolveRuntimeServiceVersion } from "../version.js"; @@ -22,14 +23,7 @@ import { isVolatileBackupPath } from "./backup-volatile-filter.js"; import { writeJson } from "./json-files.js"; import { requireNodeSqlite } from "./node-sqlite.js"; -type TarRuntime = typeof import("tar"); - -let tarRuntimePromise: Promise | undefined; - -function loadTarRuntime(): Promise { - tarRuntimePromise ??= import("tar"); - return tarRuntimePromise; -} +const loadTarRuntime = createLazyRuntimeModule(() => import("tar")); type BackupLinkCacheKey = `${number}:${number}`; diff --git a/src/infra/env.ts b/src/infra/env.ts index cb2292649bab..cd1717786939 100644 --- a/src/infra/env.ts +++ b/src/infra/env.ts @@ -1,18 +1,22 @@ // Normalizes env flag values and logs env warnings lazily. import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import type { SubsystemLogger } from "../logging/subsystem.js"; +import { createLazyPromise } from "../shared/lazy-runtime.js"; let log: SubsystemLogger | null = null; -let logPromise: Promise | null = null; +const loadLog = createLazyPromise( + () => + import("../logging/subsystem.js").then(({ createSubsystemLogger }) => + createSubsystemLogger("env"), + ), + { cacheRejections: true }, +); const loggedEnv = new Set(); const ENV_NORMALIZATION_KEY_GROUPS = [["ZAI_API_KEY", "Z_AI_API_KEY"]] as const; async function getLog(): Promise { if (!log) { - logPromise ??= import("../logging/subsystem.js").then(({ createSubsystemLogger }) => - createSubsystemLogger("env"), - ); - log = await logPromise; + log = await loadLog(); } return log; } diff --git a/src/infra/exec-approval-forwarder.ts b/src/infra/exec-approval-forwarder.ts index 51100db0de9c..4ca291f158f3 100644 --- a/src/infra/exec-approval-forwarder.ts +++ b/src/infra/exec-approval-forwarder.ts @@ -20,6 +20,7 @@ import { buildPluginApprovalResolvedReplyPayload, } from "../plugin-sdk/approval-renderers.js"; import { channelRouteDedupeKey } from "../plugin-sdk/channel-route.js"; +import { createLazyRuntimeModule } from "../shared/lazy-runtime.js"; import { isDeliverableMessageChannel, normalizeMessageChannel, @@ -142,14 +143,10 @@ type ExecApprovalForwarderDeps = { const DEFAULT_MODE = "session" as const; const SYNTHETIC_APPROVAL_REQUEST_ID = "__approval-routing__"; -let execApprovalForwarderRuntimePromise: Promise< - typeof import("./exec-approval-forwarder.runtime.js") -> | null = null; -function loadExecApprovalForwarderRuntime() { - execApprovalForwarderRuntimePromise ??= import("./exec-approval-forwarder.runtime.js"); - return execApprovalForwarderRuntimePromise; -} +const loadExecApprovalForwarderRuntime = createLazyRuntimeModule( + () => import("./exec-approval-forwarder.runtime.js"), +); function normalizeMode(mode?: ExecApprovalForwardingConfig["mode"]) { return mode ?? DEFAULT_MODE; diff --git a/src/infra/heartbeat-runner.ts b/src/infra/heartbeat-runner.ts index 19388230fde4..e333e8a82b08 100644 --- a/src/infra/heartbeat-runner.ts +++ b/src/infra/heartbeat-runner.ts @@ -106,6 +106,7 @@ import { toAgentStoreSessionKey, } from "../routing/session-key.js"; import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; +import { createLazyRuntimeModule } from "../shared/lazy-runtime.js"; import { escapeRegExp } from "../utils.js"; import { MAX_SAFE_TIMEOUT_DELAY_MS, resolveSafeTimeoutDelayMs } from "../utils/timer-delay.js"; import { loadOrCreateDeviceIdentity } from "./device-identity.js"; @@ -172,13 +173,10 @@ export type HeartbeatDeps = OutboundSendDeps & }; const log = createSubsystemLogger("gateway/heartbeat"); -let heartbeatRunnerRuntimePromise: Promise | null = - null; -function loadHeartbeatRunnerRuntime() { - heartbeatRunnerRuntimePromise ??= import("./heartbeat-runner.runtime.js"); - return heartbeatRunnerRuntimePromise; -} +const loadHeartbeatRunnerRuntime = createLazyRuntimeModule( + () => import("./heartbeat-runner.runtime.js"), +); const HEARTBEAT_ALWAYS_BUSY_LANES = [CommandLane.Cron, CommandLane.CronNested] as const; const DEFAULT_HEARTBEAT_TIMEOUT_SECONDS = 10 * 60; diff --git a/src/infra/outbound/deliver.ts b/src/infra/outbound/deliver.ts index afb632b7ccb7..758e2cbd059f 100644 --- a/src/infra/outbound/deliver.ts +++ b/src/infra/outbound/deliver.ts @@ -41,6 +41,7 @@ import { createSubsystemLogger } from "../../logging/subsystem.js"; import type { OutboundMediaAccess } from "../../media/load-options.js"; import { resolveAgentScopedOutboundMediaAccess } from "../../media/read-capability.js"; import { getGlobalHookRunner } from "../../plugins/hook-runner-global.js"; +import { createLazyRuntimeModule } from "../../shared/lazy-runtime.js"; import { diagnosticErrorCategory } from "../diagnostic-error-metadata.js"; import { emitInternalDiagnosticEvent as emitDiagnosticEvent, @@ -123,25 +124,16 @@ export type OutboundDurableDeliverySupport = }; const log = createSubsystemLogger("outbound/deliver"); -let transcriptRuntimePromise: - | Promise - | undefined; -async function loadTranscriptRuntime() { - // Transcript writes are optional side effects; keep this lazy for import-only - // delivery policy checks and tests. - transcriptRuntimePromise ??= import("../../config/sessions/transcript.runtime.js"); - return await transcriptRuntimePromise; -} +// Transcript writes are optional side effects; keep this lazy for import-only +// delivery policy checks and tests. +const loadTranscriptRuntime = createLazyRuntimeModule( + () => import("../../config/sessions/transcript.runtime.js"), +); -let channelBootstrapRuntimePromise: - | Promise - | undefined; - -async function loadChannelBootstrapRuntime() { - channelBootstrapRuntimePromise ??= import("./channel-bootstrap.runtime.js"); - return await channelBootstrapRuntimePromise; -} +const loadChannelBootstrapRuntime = createLazyRuntimeModule( + () => import("./channel-bootstrap.runtime.js"), +); type ChannelHandler = { chunker: ChannelOutboundAdapter["chunker"] | null; diff --git a/src/infra/outbound/message-action-runner.ts b/src/infra/outbound/message-action-runner.ts index 468a641a997b..9baebb55c801 100644 --- a/src/infra/outbound/message-action-runner.ts +++ b/src/infra/outbound/message-action-runner.ts @@ -42,6 +42,7 @@ import { extractToolPayload } from "../../plugin-sdk/tool-payload.js"; import { hasPollCreationParams } from "../../poll-params.js"; import { resolvePollMaxSelections } from "../../polls.js"; import { resolveFirstBoundAccountId } from "../../routing/bound-account-read.js"; +import { createLazyRuntimeModule } from "../../shared/lazy-runtime.js"; import { stripUnsupportedCitationControlMarkers } from "../../shared/text/citation-control-markers.js"; import { stripFormattedReasoningMessage } from "../../shared/text/formatted-reasoning-message.js"; import { parseInlineDirectives } from "../../utils/directive-tags.js"; @@ -104,16 +105,11 @@ export type MessageActionRunnerGateway = { mode: GatewayClientMode; }; -let messageActionGatewayRuntimePromise: Promise< - typeof import("./message.gateway.runtime.js") -> | null = null; - -function loadMessageActionGatewayRuntime() { - // Gateway runtime is only needed for remote message action dispatch or - // idempotency keys; keep normal in-process actions import-light. - messageActionGatewayRuntimePromise ??= import("./message.gateway.runtime.js"); - return messageActionGatewayRuntimePromise; -} +// Gateway runtime is only needed for remote message action dispatch or +// idempotency keys; keep normal in-process actions import-light. +const loadMessageActionGatewayRuntime = createLazyRuntimeModule( + () => import("./message.gateway.runtime.js"), +); export type RunMessageActionParams = { cfg: OpenClawConfig; diff --git a/src/infra/outbound/message-action-tts.ts b/src/infra/outbound/message-action-tts.ts index c771c40dfdc9..5a87b101b341 100644 --- a/src/infra/outbound/message-action-tts.ts +++ b/src/infra/outbound/message-action-tts.ts @@ -5,15 +5,13 @@ import { resolveStorePath } from "../../config/sessions.js"; import { loadSessionEntry } from "../../config/sessions/session-accessor.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import type { TtsAutoMode } from "../../config/types.tts.js"; +import { createLazyRuntimeModule } from "../../shared/lazy-runtime.js"; import { shouldAttemptTtsPayload } from "../../tts/tts-config.js"; -let ttsRuntimePromise: Promise | null = null; - -function loadMessageActionTtsRuntime() { - // Keep the TTS runtime lazy so ordinary message sends do not pay the provider import cost. - ttsRuntimePromise ??= import("../../tts/tts.runtime.js"); - return ttsRuntimePromise; -} +// Keep the TTS runtime lazy so ordinary message sends do not pay the provider import cost. +const loadMessageActionTtsRuntime = createLazyRuntimeModule( + () => import("../../tts/tts.runtime.js"), +); /** Reads the session-level TTS auto mode for a message-action send. */ export function resolveMessageActionSessionTtsAuto(params: { diff --git a/src/infra/outbound/message.ts b/src/infra/outbound/message.ts index 5af26675107c..20c036fb0c4a 100644 --- a/src/infra/outbound/message.ts +++ b/src/infra/outbound/message.ts @@ -7,6 +7,7 @@ import type { OpenClawConfig } from "../../config/types.openclaw.js"; import type { OutboundMediaAccess } from "../../media/load-options.js"; import type { PollInput } from "../../polls.js"; import { normalizePollInput } from "../../polls.js"; +import { createLazyRuntimeModule } from "../../shared/lazy-runtime.js"; import { resolveOutboundChannelPlugin } from "./channel-resolution.js"; import { resolveMessageChannelSelection } from "./channel-selection.js"; import { @@ -29,23 +30,17 @@ import { import { buildOutboundSessionContext } from "./session-context.js"; import { resolveOutboundTarget } from "./targets.js"; -let messageConfigRuntimePromise: Promise | null = - null; -let messageGatewayRuntimePromise: Promise | null = - null; const SEND_BUFFER_MEDIA_URL = "buffer://message-send/attachment"; -function loadMessageConfigRuntime() { - // Keep config/runtime loading lazy so importing message helpers does not - // bootstrap plugin registries or gateway clients. - messageConfigRuntimePromise ??= import("./message.config.runtime.js"); - return messageConfigRuntimePromise; -} +const loadMessageConfigRuntime = createLazyRuntimeModule( + () => import("./message.config.runtime.js"), +); -function loadMessageGatewayRuntime() { - messageGatewayRuntimePromise ??= import("./message.gateway.runtime.js"); - return messageGatewayRuntimePromise; -} +// Keep config/runtime loading lazy so importing message helpers does not +// bootstrap plugin registries or gateway clients. +const loadMessageGatewayRuntime = createLazyRuntimeModule( + () => import("./message.gateway.runtime.js"), +); export type MessageGatewayOptions = OutboundMessageGatewayOptionsInput; diff --git a/src/infra/push-web.ts b/src/infra/push-web.ts index 751dd6c3e24d..5c60fc19f794 100644 --- a/src/infra/push-web.ts +++ b/src/infra/push-web.ts @@ -2,6 +2,7 @@ import { createHash, randomUUID } from "node:crypto"; import path from "node:path"; import { resolveStateDir } from "../config/paths.js"; +import { createLazyRuntimeModule } from "../shared/lazy-runtime.js"; import { createAsyncLock, tryReadJson, writeJson } from "./json-files.js"; // --- Types --- @@ -44,14 +45,9 @@ const withLock = createAsyncLock(); type WebPushRuntime = typeof import("web-push"); type WebPushRuntimeModule = WebPushRuntime & { default?: WebPushRuntime }; -let webPushRuntimePromise: Promise | undefined; - -async function loadWebPushRuntime(): Promise { - webPushRuntimePromise ??= import("web-push").then( - (mod: WebPushRuntimeModule) => mod.default ?? mod, - ); - return await webPushRuntimePromise; -} +const loadWebPushRuntime = createLazyRuntimeModule(() => + import("web-push").then((mod: WebPushRuntimeModule) => mod.default ?? mod), +); // --- Helpers --- diff --git a/src/infra/session-maintenance-warning.ts b/src/infra/session-maintenance-warning.ts index a59dc5a98705..5871de1311c3 100644 --- a/src/infra/session-maintenance-warning.ts +++ b/src/infra/session-maintenance-warning.ts @@ -3,6 +3,7 @@ import type { SessionMaintenanceWarning } from "../config/sessions/store-mainten import type { SessionEntry } from "../config/sessions/types.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; +import { createLazyPromiseLoader } from "../shared/lazy-runtime.js"; import { deliveryContextFromSession } from "../utils/delivery-context.shared.js"; import { isDeliverableMessageChannel, normalizeMessageChannel } from "../utils/message-channel.js"; import { buildOutboundSessionContext } from "./outbound/session-context.js"; @@ -19,21 +20,21 @@ type WarningParams = { const warnedContexts = new Map(); const log = createSubsystemLogger("session-maintenance-warning"); -let messageRuntimePromise: Promise | null = null; +const messageRuntimeLoader = createLazyPromiseLoader( + () => import("../channels/message/runtime.js"), + { cacheRejections: true }, +); function resetSessionMaintenanceWarningForTests() { warnedContexts.clear(); - messageRuntimePromise = null; + messageRuntimeLoader.clear(); } export const testing = { resetSessionMaintenanceWarningForTests, } as const; -function loadDeliverRuntime() { - messageRuntimePromise ??= import("../channels/message/runtime.js"); - return messageRuntimePromise; -} +const loadDeliverRuntime = messageRuntimeLoader.load; function shouldSendWarning(): boolean { return process.env.NODE_ENV !== "test"; diff --git a/src/logging/diagnostic.ts b/src/logging/diagnostic.ts index b6909ca8ca54..d4664d45a11c 100644 --- a/src/logging/diagnostic.ts +++ b/src/logging/diagnostic.ts @@ -10,6 +10,7 @@ import { type DiagnosticPhaseSnapshot, type DiagnosticLivenessWarningReason, } from "../infra/diagnostic-events.js"; +import { createLazyRuntimeModule } from "../shared/lazy-runtime.js"; import { emitDiagnosticMemorySample, resetDiagnosticMemoryForTest } from "./diagnostic-memory.js"; import { getCurrentDiagnosticPhase, @@ -65,6 +66,7 @@ import { startDiagnosticStabilityRecorder, stopDiagnosticStabilityRecorder, } from "./diagnostic-stability.js"; + export { diagnosticLogger, logLaneDequeue, logLaneEnqueue } from "./diagnostic-runtime.js"; const webhookStats = { @@ -84,12 +86,9 @@ const DEFAULT_LIVENESS_EVENT_LOOP_DELAY_WARN_MS = 1_000; const DEFAULT_LIVENESS_EVENT_LOOP_UTILIZATION_WARN = 0.95; const DEFAULT_LIVENESS_CPU_CORE_RATIO_WARN = 0.9; const DEFAULT_LIVENESS_WARN_COOLDOWN_MS = 120_000; -let commandPollBackoffRuntimePromise: Promise< - typeof import("../agents/command-poll-backoff.runtime.js") -> | null = null; -let stuckSessionRecoveryRuntimePromise: Promise< - typeof import("./diagnostic-stuck-session-recovery.runtime.js") -> | null = null; +const loadStuckSessionRecoveryRuntime = createLazyRuntimeModule( + () => import("./diagnostic-stuck-session-recovery.runtime.js"), +); type EmitDiagnosticMemorySample = typeof emitDiagnosticMemorySample; type EventLoopDelayMonitor = ReturnType; @@ -153,16 +152,14 @@ let lastDiagnosticLivenessEventLoopUtilization: EventLoopUtilization | null = nu let lastDiagnosticLivenessEventAt = 0; let lastDiagnosticLivenessWarnAt = 0; -function loadCommandPollBackoffRuntime() { - commandPollBackoffRuntimePromise ??= import("../agents/command-poll-backoff.runtime.js"); - return commandPollBackoffRuntimePromise; -} +const loadCommandPollBackoffRuntime = createLazyRuntimeModule( + () => import("../agents/command-poll-backoff.runtime.js"), +); async function recoverStuckSession( params: StuckSessionRecoveryRequest, ): Promise { - stuckSessionRecoveryRuntimePromise ??= import("./diagnostic-stuck-session-recovery.runtime.js"); - return stuckSessionRecoveryRuntimePromise + return loadStuckSessionRecoveryRuntime() .then(({ recoverStuckDiagnosticSession }) => recoverStuckDiagnosticSession(params)) .catch((err: unknown) => { diag.warn(`stuck session recovery unavailable: ${String(err)}`); diff --git a/src/media-understanding/echo-transcript.ts b/src/media-understanding/echo-transcript.ts index 0928d2b81ab5..e25c615218be 100644 --- a/src/media-understanding/echo-transcript.ts +++ b/src/media-understanding/echo-transcript.ts @@ -4,16 +4,12 @@ import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/st import type { MsgContext } from "../auto-reply/templating.js"; import type { OpenClawConfig } from "../config/types.js"; import { logVerbose, shouldLogVerbose } from "../globals.js"; +import { createLazyRuntimeModule } from "../shared/lazy-runtime.js"; import { isDeliverableMessageChannel } from "../utils/message-channel.js"; -let messageRuntimePromise: Promise | null = null; - -function loadMessageRuntime() { - // The message runtime is heavy and only needed when echo delivery actually - // proceeds to a deliverable channel. - messageRuntimePromise ??= import("../channels/message/runtime.js"); - return messageRuntimePromise; -} +// The message runtime is heavy and only needed when echo delivery actually +// proceeds to a deliverable channel. +const loadMessageRuntime = createLazyRuntimeModule(() => import("../channels/message/runtime.js")); /** Default operator-visible transcript echo format for preflight audio transcription. */ export const DEFAULT_ECHO_TRANSCRIPT_FORMAT = '๐Ÿ“ "{transcript}"'; diff --git a/src/media-understanding/runner.entries.ts b/src/media-understanding/runner.entries.ts index d1541195f2e0..fd0f4b3e8eb3 100644 --- a/src/media-understanding/runner.entries.ts +++ b/src/media-understanding/runner.entries.ts @@ -43,6 +43,7 @@ import { import { resolveOfficialExternalPluginRepairHint } from "../plugins/official-external-plugin-repair-hints.js"; import { runExec } from "../process/exec.js"; import { providerOperationRetryConfig } from "../provider-runtime/operation-retry.js"; +import { createLazyRuntimeModule } from "../shared/lazy-runtime.js"; import { MediaAttachmentCache } from "./attachments.js"; import { CLI_OUTPUT_MAX_BUFFER, @@ -65,20 +66,7 @@ import type { } from "./types.js"; type ProviderRegistry = Map; -type ResolveApiKeyForProvider = typeof import("../agents/model-auth.js").resolveApiKeyForProvider; -type RequireApiKey = typeof import("../agents/model-auth.js").requireApiKey; -type IsProviderAuthError = typeof import("../agents/model-auth.js").isProviderAuthError; - -let cachedModelAuth: { - resolveApiKeyForProvider: ResolveApiKeyForProvider; - requireApiKey: RequireApiKey; - isProviderAuthError: IsProviderAuthError; -} | null = null; - -async function loadModelAuth() { - cachedModelAuth ??= await import("../agents/model-auth.js"); - return cachedModelAuth; -} +const loadModelAuth = createLazyRuntimeModule(async () => await import("../agents/model-auth.js")); function resolveLiteralProviderApiKey(params: { cfg: OpenClawConfig; diff --git a/src/media-understanding/runner.ts b/src/media-understanding/runner.ts index 1c16497ab563..24cf5f2ff227 100644 --- a/src/media-understanding/runner.ts +++ b/src/media-understanding/runner.ts @@ -15,6 +15,9 @@ import { normalizeStringEntries, uniqueStrings, } from "@openclaw/normalization-core/string-normalization"; +import type { ActiveMediaModel } from "../../packages/media-understanding-common/src/active-model.js"; +import { isMediaUnderstandingSkipError } from "../../packages/media-understanding-common/src/errors.js"; +import { providerSupportsCapability } from "../../packages/media-understanding-common/src/provider-supports.js"; import { isMinimaxVlmModel, isMinimaxVlmProvider } from "../agents/minimax-vlm.js"; import { buildModelAliasIndex, @@ -38,9 +41,7 @@ import { logWarn } from "../logger.js"; import { resolveChannelInboundAttachmentRoots } from "../media/channel-inbound-roots.js"; import { getDefaultMediaLocalRoots } from "../media/local-roots.js"; import { runExec } from "../process/exec.js"; -import type { ActiveMediaModel } from "../../packages/media-understanding-common/src/active-model.js"; -import { isMediaUnderstandingSkipError } from "../../packages/media-understanding-common/src/errors.js"; -import { providerSupportsCapability } from "../../packages/media-understanding-common/src/provider-supports.js"; +import { createLazyRuntimeModule, createLazyRuntimeNamedExport } from "../shared/lazy-runtime.js"; import { MediaAttachmentCache, selectAttachments } from "./attachments.js"; import { fileExists } from "./fs.js"; import { resolveOpenAiAudioAuthModelApi } from "./openai-audio-api.js"; @@ -64,12 +65,11 @@ import type { MediaUnderstandingOutput, MediaUnderstandingProvider, } from "./types.js"; + export { createMediaAttachmentCache, normalizeMediaAttachments } from "./runner.attachments.js"; export type { ActiveMediaModel } from "../../packages/media-understanding-common/src/active-model.js"; type ProviderRegistry = Map; -type HasAvailableAuthForProvider = - typeof import("../agents/model-auth.js").hasAvailableAuthForProvider; type ModelCatalogApi = typeof import("../agents/model-catalog.js"); type ModelCatalog = Awaited>; @@ -78,13 +78,14 @@ export type RunCapabilityResult = { decision: MediaUnderstandingDecision; }; -let cachedHasAvailableAuthForProvider: HasAvailableAuthForProvider | null = null; -let cachedModelCatalogApi: ModelCatalogApi | null = null; +const loadHasAvailableAuthForProvider = createLazyRuntimeNamedExport( + () => import("../agents/model-auth.js"), + "hasAvailableAuthForProvider", +); -async function loadModelCatalogApi(): Promise { - cachedModelCatalogApi ??= await import("../agents/model-catalog.js"); - return cachedModelCatalogApi; -} +const loadModelCatalogApi = createLazyRuntimeModule( + async () => await import("../agents/model-catalog.js"), +); function resolveLiteralProviderApiKey( cfg: OpenClawConfig | undefined, @@ -107,9 +108,8 @@ async function hasProviderAuthAvailable(params: { if (resolveLiteralProviderApiKey(params.cfg, params.provider)) { return true; } - cachedHasAvailableAuthForProvider ??= (await import("../agents/model-auth.js")) - .hasAvailableAuthForProvider; - return await cachedHasAvailableAuthForProvider({ + const hasAvailableAuthForProvider = await loadHasAvailableAuthForProvider(); + return await hasAvailableAuthForProvider({ ...params, modelApi: resolveOpenAiAudioAuthModelApi({ capability: params.capability, diff --git a/src/plugins/contracts/tts-contract-suites.ts b/src/plugins/contracts/tts-contract-suites.ts index f54fd53206b7..ebd414010ee3 100644 --- a/src/plugins/contracts/tts-contract-suites.ts +++ b/src/plugins/contracts/tts-contract-suites.ts @@ -10,6 +10,7 @@ import { withEnv, withEnvAsync } from "openclaw/plugin-sdk/test-env"; import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { AssistantMessage, Model } from "../../llm/types.js"; import { resolveWorkspacePackagePublicModuleUrl } from "../../plugin-sdk/test-helpers/public-surface-loader.js"; +import { createLazyRuntimeModule } from "../../shared/lazy-runtime.js"; type TtsRuntimeModule = typeof import("openclaw/plugin-sdk/tts-runtime"); type TtsCoreModule = typeof import("openclaw/plugin-sdk/speech-core"); @@ -21,9 +22,7 @@ const speechCoreRuntimeApiModuleId = resolveWorkspacePackagePublicModuleUrl({ }); let ttsRuntime: TtsRuntimeModule; -let ttsRuntimePromise: Promise | null = null; let ttsRuntimeInitialized = false; -let ttsCorePromise: Promise | null = null; let completeSimple: typeof import("openclaw/plugin-sdk/llm").completeSimple; let prepareSimpleCompletionModelMock: SummarizeTextDeps["prepareSimpleCompletionModel"]; let requireApiKeyMock: SummarizeTextDeps["requireApiKey"]; @@ -413,15 +412,11 @@ function buildTestGoogleSpeechProvider(): SpeechProviderPlugin { }; } -async function loadTtsRuntime(): Promise { - ttsRuntimePromise ??= import(speechCoreRuntimeApiModuleId) as Promise; - return await ttsRuntimePromise; -} +const loadTtsRuntime = createLazyRuntimeModule( + () => import(speechCoreRuntimeApiModuleId) as Promise, +); -async function loadTtsCore(): Promise { - ttsCorePromise ??= import("openclaw/plugin-sdk/speech-core"); - return await ttsCorePromise; -} +const loadTtsCore = createLazyRuntimeModule(() => import("openclaw/plugin-sdk/speech-core")); function createPrepareSimpleCompletionModelMock(): SummarizeTextDeps["prepareSimpleCompletionModel"] { return vi.fn(async ({ provider, modelId }) => ({ diff --git a/src/plugins/host-hook-attachments.ts b/src/plugins/host-hook-attachments.ts index 01a21c899f5f..6e3b41057ef1 100644 --- a/src/plugins/host-hook-attachments.ts +++ b/src/plugins/host-hook-attachments.ts @@ -14,6 +14,7 @@ import { extractDeliveryInfo } from "../config/sessions/delivery-info.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { formatErrorMessage } from "../infra/errors.js"; import { resolveAgentIdFromSessionKey } from "../routing/session-key.js"; +import { createLazyRuntimeModule } from "../shared/lazy-runtime.js"; import { isDeliverableMessageChannel, normalizeMessageChannel } from "../utils/message-channel.js"; import type { PluginAttachmentChannelHints, @@ -31,17 +32,10 @@ export const attachmentProbeFs = { const MAX_ATTACHMENT_FILES = 10; type SendMessage = typeof import("../infra/outbound/message.js").sendMessage; -let sendMessagePromise: Promise | undefined; -async function loadSendMessage(): Promise { - sendMessagePromise ??= import("../infra/outbound/message.js").then( - (module) => module.sendMessage, - ); - return sendMessagePromise; -} - -type GetChannelPlugin = typeof import("../channels/plugins/index.js").getChannelPlugin; -let getChannelPluginPromise: Promise | undefined; +const loadSendMessage = createLazyRuntimeModule(() => + import("../infra/outbound/message.js").then((module) => module.sendMessage), +); type AttachmentDeliveryChannelPlugin = { outbound?: { @@ -49,12 +43,9 @@ type AttachmentDeliveryChannelPlugin = { }; }; -async function loadGetChannelPlugin(): Promise { - getChannelPluginPromise ??= import("../channels/plugins/index.js").then( - (module) => module.getChannelPlugin, - ); - return getChannelPluginPromise; -} +const loadGetChannelPlugin = createLazyRuntimeModule(() => + import("../channels/plugins/index.js").then((module) => module.getChannelPlugin), +); type ResolvedAttachmentDelivery = { parseMode?: "HTML"; diff --git a/src/process/supervisor/adapters/pty.ts b/src/process/supervisor/adapters/pty.ts index 8a610ccba599..397f9a18195d 100644 --- a/src/process/supervisor/adapters/pty.ts +++ b/src/process/supervisor/adapters/pty.ts @@ -1,3 +1,4 @@ +import { createLazyRuntimeModule } from "../../../shared/lazy-runtime.js"; // PTY adapter wraps pseudo-terminal processes for the process supervisor. import { signalProcessTree } from "../../kill-tree.js"; import { prepareOomScoreAdjustedSpawn } from "../../linux-oom-score.js"; @@ -36,12 +37,9 @@ type PtyModule = { export type PtyAdapter = SpawnProcessAdapter; -let ptyModulePromise: Promise | null = null; - -async function loadPtyModule(): Promise { - ptyModulePromise ??= import("@lydell/node-pty") as Promise as Promise; - return ptyModulePromise; -} +const loadPtyModule = createLazyRuntimeModule( + () => import("@lydell/node-pty") as Promise as Promise, +); export async function createPtyAdapter(params: { shell: string; diff --git a/src/process/supervisor/supervisor.ts b/src/process/supervisor/supervisor.ts index 30a50f7f7984..cacd82d372bc 100644 --- a/src/process/supervisor/supervisor.ts +++ b/src/process/supervisor/supervisor.ts @@ -3,6 +3,7 @@ import crypto from "node:crypto"; import { performance } from "node:perf_hooks"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { getShellConfig } from "../../agents/shell-utils.js"; +import { createLazyRuntimeModule } from "../../shared/lazy-runtime.js"; import { createChildAdapter } from "./adapters/child.js"; import { createPtyAdapter } from "./adapters/pty.js"; import { createRunRegistry } from "./registry.js"; @@ -15,8 +16,6 @@ import type { TerminationReason, } from "./types.js"; -type SupervisorLogRuntime = typeof import("./supervisor-log.runtime.js"); - type ActiveRun = { run: ManagedRun; scopeKey?: string; @@ -25,12 +24,9 @@ type ActiveRun = { const GRACEFUL_CANCEL_TIMEOUT_MS = 5000; const DEFAULT_MAX_CAPTURED_OUTPUT_CHARS = 1024 * 1024; -let supervisorLogRuntimePromise: Promise | undefined; - -function loadSupervisorLogRuntime(): Promise { - supervisorLogRuntimePromise ??= import("./supervisor-log.runtime.js"); - return supervisorLogRuntimePromise; -} +const loadSupervisorLogRuntime = createLazyRuntimeModule( + () => import("./supervisor-log.runtime.js"), +); function clampTimeout(value?: number): number | undefined { if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) { diff --git a/src/secrets/runtime.ts b/src/secrets/runtime.ts index 6a67dbad8d33..bf692efec988 100644 --- a/src/secrets/runtime.ts +++ b/src/secrets/runtime.ts @@ -12,6 +12,7 @@ import type { OpenClawConfig } from "../config/types.openclaw.js"; import type { PluginManifestRegistry } from "../plugins/manifest-registry.js"; import type { PluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.js"; import type { PluginOrigin } from "../plugins/plugin-origin.types.js"; +import { createLazyRuntimeModule } from "../shared/lazy-runtime.js"; import { resolveUserPath } from "../utils.js"; import { canUseSecretsRuntimeFastPath, @@ -41,18 +42,13 @@ export type { PreparedSecretsRuntimeSnapshot } from "./runtime-state.js"; registerSecretsRuntimeStateClearHook(clearRuntimeAuthProfileStoreSnapshots); -let runtimeManifestPromise: Promise | null = null; -let runtimePreparePromise: Promise | null = null; +const loadRuntimeManifestHelpers = createLazyRuntimeModule( + () => import("./runtime-manifest.runtime.js"), +); -function loadRuntimeManifestHelpers() { - runtimeManifestPromise ??= import("./runtime-manifest.runtime.js"); - return runtimeManifestPromise; -} - -function loadRuntimePrepareHelpers() { - runtimePreparePromise ??= import("./runtime-prepare.runtime.js"); - return runtimePreparePromise; -} +const loadRuntimePrepareHelpers = createLazyRuntimeModule( + () => import("./runtime-prepare.runtime.js"), +); async function resolveLoadablePluginOrigins(params: { config: OpenClawConfig; diff --git a/src/security/audit-deep-code-safety.ts b/src/security/audit-deep-code-safety.ts index 70abc91131f8..5de4e78238ad 100644 --- a/src/security/audit-deep-code-safety.ts +++ b/src/security/audit-deep-code-safety.ts @@ -1,14 +1,10 @@ // Audits code paths for deep safety risks that require manual review. import type { OpenClawConfig } from "../config/types.openclaw.js"; +import { createLazyRuntimeModule } from "../shared/lazy-runtime.js"; import type { SecurityAuditFinding } from "./audit.types.js"; -let auditDeepModulePromise: Promise | undefined; - /** Lazily load deep audit code paths so normal audits avoid plugin/skill scans. */ -async function loadAuditDeepModule() { - auditDeepModulePromise ??= import("./audit.deep.runtime.js"); - return await auditDeepModulePromise; -} +const loadAuditDeepModule = createLazyRuntimeModule(() => import("./audit.deep.runtime.js")); /** Collect plugin and installed-skill code safety findings when deep audit is enabled. */ export async function collectDeepCodeSafetyFindings(params: { diff --git a/src/security/audit-extra.async.ts b/src/security/audit-extra.async.ts index 585796c05adb..4c11d57fe802 100644 --- a/src/security/audit-extra.async.ts +++ b/src/security/audit-extra.async.ts @@ -22,6 +22,7 @@ import type { OpenClawConfig, ConfigFileSnapshot } from "../config/config.js"; import { collectIncludePathsRecursive } from "../config/includes-scan.js"; import { resolveOAuthDir } from "../config/paths.js"; import { normalizeAgentId } from "../routing/session-key.js"; +import { createLazyRuntimeModule, createLazyRuntimeNamedExport } from "../shared/lazy-runtime.js"; import type { SkillScanFinding } from "../skills/security/scanner.js"; import { shouldIgnoreInstalledPluginDirName } from "./installed-plugin-dirs.js"; import { extensionUsesSkippedScannerPath, isPathInside } from "./scan-paths.js"; @@ -49,73 +50,42 @@ type ExecDockerRawFn = ( const DEFAULT_SANDBOX_BROWSER_DOCKER_PROBE_TIMEOUT_MS = 5000; type CodeSafetySummaryCache = Map>; -let skillsModulePromise: Promise | undefined; -let configModulePromise: Promise | undefined; -let agentScopeModulePromise: Promise | undefined; -let agentWorkspaceDirsModulePromise: - | Promise - | undefined; -let skillSourceModulePromise: Promise | undefined; -let sandboxDockerModulePromise: Promise | undefined; -let sandboxConstantsModulePromise: - | Promise - | undefined; -let auditPluginsTrustModulePromise: Promise | undefined; -let auditFsModulePromise: Promise | undefined; -let skillScannerModulePromise: Promise | undefined; +const loadSkillsModule = createLazyRuntimeModule(() => import("../skills/loading/workspace.js")); -function loadSkillsModule() { - skillsModulePromise ??= import("../skills/loading/workspace.js"); - return skillsModulePromise; -} +const loadConfigModule = createLazyRuntimeModule(() => import("../config/config.js")); -function loadConfigModule() { - configModulePromise ??= import("../config/config.js"); - return configModulePromise; -} +const loadAuditFsModule = createLazyRuntimeModule(() => import("./audit-fs.js")); -function loadAuditFsModule() { - auditFsModulePromise ??= import("./audit-fs.js"); - return auditFsModulePromise; -} +const loadAgentScopeModule = createLazyRuntimeModule(() => import("../agents/agent-scope.js")); -function loadAgentScopeModule() { - agentScopeModulePromise ??= import("../agents/agent-scope.js"); - return agentScopeModulePromise; -} +const loadAgentWorkspaceDirsModule = createLazyRuntimeModule( + () => import("../agents/workspace-dirs.js"), +); -function loadAgentWorkspaceDirsModule() { - agentWorkspaceDirsModulePromise ??= import("../agents/workspace-dirs.js"); - return agentWorkspaceDirsModulePromise; -} +const loadSkillSourceModule = createLazyRuntimeModule(() => import("../skills/loading/source.js")); -function loadSkillSourceModule() { - skillSourceModulePromise ??= import("../skills/loading/source.js"); - return skillSourceModulePromise; -} +const loadSkillScannerModule = createLazyRuntimeModule( + () => import("../skills/security/scanner.js"), +); -function loadSkillScannerModule() { - skillScannerModulePromise ??= import("../skills/security/scanner.js"); - return skillScannerModulePromise; -} +const loadExecDockerRaw = createLazyRuntimeNamedExport( + () => import("../agents/sandbox/docker.js"), + "execDockerRaw", +) satisfies () => Promise; -async function loadExecDockerRaw(): Promise { - sandboxDockerModulePromise ??= import("../agents/sandbox/docker.js"); - const { execDockerRaw } = await sandboxDockerModulePromise; - return execDockerRaw; -} +const loadSandboxBrowserSecurityHashEpoch = createLazyRuntimeNamedExport( + () => import("../agents/sandbox/constants.js"), + "SANDBOX_BROWSER_SECURITY_HASH_EPOCH", +); -async function loadSandboxBrowserSecurityHashEpoch(): Promise { - sandboxConstantsModulePromise ??= import("../agents/sandbox/constants.js"); - const { SANDBOX_BROWSER_SECURITY_HASH_EPOCH } = await sandboxConstantsModulePromise; - return SANDBOX_BROWSER_SECURITY_HASH_EPOCH; -} +const loadAuditPluginsTrustModule = createLazyRuntimeModule( + () => import("./audit-plugins-trust.js"), +); export async function collectPluginsTrustFindings( params: CollectPluginsTrustFindingsParams, ): Promise { - auditPluginsTrustModulePromise ??= import("./audit-plugins-trust.js"); - const { collectPluginsTrustFindings: collect } = await auditPluginsTrustModulePromise; + const { collectPluginsTrustFindings: collect } = await loadAuditPluginsTrustModule(); return await collect(params); } diff --git a/src/security/audit-plugins-trust.ts b/src/security/audit-plugins-trust.ts index d9ac8108a15f..d1485bc1a664 100644 --- a/src/security/audit-plugins-trust.ts +++ b/src/security/audit-plugins-trust.ts @@ -15,6 +15,7 @@ import { createPluginRegistryIdNormalizer, loadPluginRegistrySnapshot, } from "../plugins/plugin-registry.js"; +import { createLazyPromise } from "../shared/lazy-runtime.js"; import type { SecurityAuditFinding } from "./audit.types.js"; import { shouldIgnoreInstalledPluginDirName } from "./installed-plugin-dirs.js"; @@ -28,25 +29,24 @@ type PluginTrustPolicyDeps = { resolveToolProfilePolicy: typeof import("../agents/tool-policy.js").resolveToolProfilePolicy; }; -let pluginTrustPolicyDepsPromise: Promise | undefined; - /** Lazily load tool-policy helpers so basic security imports avoid agent policy modules. */ -async function loadPluginTrustPolicyDeps(): Promise { - pluginTrustPolicyDepsPromise ??= Promise.all([ - import("../agents/sandbox/config.js"), - import("../agents/sandbox/tool-policy.js"), - import("../agents/tool-policy-match.js"), - import("../agents/tool-policy.js"), - import("../agents/sandbox-tool-policy.js"), - ]).then(([sandboxConfig, sandboxToolPolicy, toolPolicyMatch, toolPolicy, auditToolPolicy]) => ({ - isToolAllowedByPolicies: toolPolicyMatch.isToolAllowedByPolicies, - pickSandboxToolPolicy: auditToolPolicy.pickSandboxToolPolicy, - resolveSandboxConfigForAgent: sandboxConfig.resolveSandboxConfigForAgent, - resolveSandboxToolPolicyForAgent: sandboxToolPolicy.resolveSandboxToolPolicyForAgent, - resolveToolProfilePolicy: toolPolicy.resolveToolProfilePolicy, - })); - return await pluginTrustPolicyDepsPromise; -} +const loadPluginTrustPolicyDeps = createLazyPromise( + () => + Promise.all([ + import("../agents/sandbox/config.js"), + import("../agents/sandbox/tool-policy.js"), + import("../agents/tool-policy-match.js"), + import("../agents/tool-policy.js"), + import("../agents/sandbox-tool-policy.js"), + ]).then(([sandboxConfig, sandboxToolPolicy, toolPolicyMatch, toolPolicy, auditToolPolicy]) => ({ + isToolAllowedByPolicies: toolPolicyMatch.isToolAllowedByPolicies, + pickSandboxToolPolicy: auditToolPolicy.pickSandboxToolPolicy, + resolveSandboxConfigForAgent: sandboxConfig.resolveSandboxConfigForAgent, + resolveSandboxToolPolicyForAgent: sandboxToolPolicy.resolveSandboxToolPolicyForAgent, + resolveToolProfilePolicy: toolPolicy.resolveToolProfilePolicy, + })), + { cacheRejections: true }, +); function readChannelCommandSetting( cfg: OpenClawConfig, diff --git a/src/security/audit.ts b/src/security/audit.ts index d09513763702..b2dfdd899d9c 100644 --- a/src/security/audit.ts +++ b/src/security/audit.ts @@ -40,6 +40,7 @@ import { } from "../infra/exec-safe-bin-runtime-policy.js"; import { listRiskyConfiguredSafeBins } from "../infra/exec-safe-bin-semantics.js"; import { DEFAULT_AGENT_ID } from "../routing/session-key.js"; +import { createLazyRuntimeModule } from "../shared/lazy-runtime.js"; import { collectDeepCodeSafetyFindings } from "./audit-deep-code-safety.js"; import { collectDeepProbeFindings } from "./audit-deep-probe-findings.js"; import { @@ -147,70 +148,32 @@ export type AuditExecutionContext = { workspaceDir?: string; }; -let readOnlyChannelPluginsModulePromise: - | Promise - | undefined; -let auditNonDeepModulePromise: Promise | undefined; -let auditChannelModulePromise: - | Promise - | undefined; -let pluginMetadataRegistryLoaderModulePromise: - | Promise - | undefined; -let pluginAutoEnableModulePromise: - | Promise - | undefined; -let channelPluginIdsModulePromise: - | Promise - | undefined; -let pluginRuntimeModulePromise: Promise | undefined; -let gatewayProbeDepsPromise: - | Promise<{ - buildGatewayConnectionDetails: typeof import("../gateway/call.js").buildGatewayConnectionDetails; - resolveGatewayProbeAuthSafe: typeof import("../gateway/probe-auth.js").resolveGatewayProbeAuthSafe; - resolveGatewayProbeTarget: typeof import("../gateway/probe-auth.js").resolveGatewayProbeTarget; - probeGateway: typeof import("../gateway/probe.js").probeGateway; - }> - | undefined; +const loadReadOnlyChannelPlugins = createLazyRuntimeModule( + () => import("../channels/plugins/read-only.js"), +); -async function loadReadOnlyChannelPlugins() { - readOnlyChannelPluginsModulePromise ??= import("../channels/plugins/read-only.js"); - return await readOnlyChannelPluginsModulePromise; -} +const loadAuditNonDeepModule = createLazyRuntimeModule(() => import("./audit.nondeep.runtime.js")); -async function loadAuditNonDeepModule() { - auditNonDeepModulePromise ??= import("./audit.nondeep.runtime.js"); - return await auditNonDeepModulePromise; -} +const loadAuditChannelModule = createLazyRuntimeModule( + () => import("./audit-channel.collect.runtime.js"), +); -async function loadAuditChannelModule() { - auditChannelModulePromise ??= import("./audit-channel.collect.runtime.js"); - return await auditChannelModulePromise; -} +const loadPluginMetadataRegistryLoaderModule = createLazyRuntimeModule( + () => import("../plugins/runtime/metadata-registry-loader.js"), +); -async function loadPluginMetadataRegistryLoaderModule() { - pluginMetadataRegistryLoaderModulePromise ??= - import("../plugins/runtime/metadata-registry-loader.js"); - return await pluginMetadataRegistryLoaderModulePromise; -} +const loadPluginAutoEnableModule = createLazyRuntimeModule( + () => import("../config/plugin-auto-enable.js"), +); -async function loadPluginAutoEnableModule() { - pluginAutoEnableModulePromise ??= import("../config/plugin-auto-enable.js"); - return await pluginAutoEnableModulePromise; -} +const loadChannelPluginIdsModule = createLazyRuntimeModule( + () => import("../plugins/channel-plugin-ids.js"), +); -async function loadChannelPluginIdsModule() { - channelPluginIdsModulePromise ??= import("../plugins/channel-plugin-ids.js"); - return await channelPluginIdsModulePromise; -} +const loadPluginRuntimeModule = createLazyRuntimeModule(() => import("../plugins/runtime.js")); -async function loadPluginRuntimeModule() { - pluginRuntimeModulePromise ??= import("../plugins/runtime.js"); - return await pluginRuntimeModulePromise; -} - -async function loadGatewayProbeDeps() { - gatewayProbeDepsPromise ??= Promise.all([ +const loadGatewayProbeDeps = createLazyRuntimeModule(() => + Promise.all([ import("../gateway/call.js"), import("../gateway/probe-auth.js"), import("../gateway/probe.js"), @@ -219,9 +182,8 @@ async function loadGatewayProbeDeps() { resolveGatewayProbeAuthSafe: probeAuthModule.resolveGatewayProbeAuthSafe, resolveGatewayProbeTarget: probeAuthModule.resolveGatewayProbeTarget, probeGateway: probeModule.probeGateway, - })); - return await gatewayProbeDepsPromise; -} + })), +); function countBySeverity(findings: SecurityAuditFinding[]): SecurityAuditSummary { let critical = 0; diff --git a/src/status/status-text.ts b/src/status/status-text.ts index 59779e08f2c1..0810029217d3 100644 --- a/src/status/status-text.ts +++ b/src/status/status-text.ts @@ -40,6 +40,7 @@ import { } from "../infra/provider-usage.js"; import { normalizeAccountId } from "../routing/account-id.js"; import { resolveNormalizedAccountEntry } from "../routing/account-lookup.js"; +import { createLazyPromise, createLazyRuntimeModule } from "../shared/lazy-runtime.js"; import { listTasksForAgentIdForStatus, listTasksForSessionKeyForStatus, @@ -98,52 +99,23 @@ function resolveStatusChannelFeatureLine(params: { : "Telegram rich messages: off ยท set channels.telegram.richMessages=true for tables/details/rich media"; } -let statusMessageRuntimePromise: Promise | null = - null; -let agentHarnessSelectionRuntimePromise: Promise< - typeof import("../agents/harness/selection.js") -> | null = null; -let statusQueueRuntimePromise: Promise | null = null; -let statusSubagentsRuntimePromise: Promise | null = - null; -let statusPluginHealthRuntimePromise: Promise< - typeof import("./status-plugin-health.runtime.js") -> | null = null; +const loadStatusMessageRuntime = createLazyPromise( + () => + import("./status-message.runtime.js").then((module) => module.loadStatusMessageRuntimeModule()), + { cacheRejections: true }, +); +const loadAgentHarnessSelectionRuntime = createLazyRuntimeModule( + () => import("../agents/harness/selection.js"), +); +const loadStatusSubagentsRuntime = createLazyRuntimeModule( + () => import("./status-subagents.runtime.js"), +); -function loadStatusMessageRuntime(): Promise { - const runtimePromise = (statusMessageRuntimePromise ??= - import("./status-message.runtime.js").then((module) => - module.loadStatusMessageRuntimeModule(), - )); - return runtimePromise; -} +const loadStatusQueueRuntime = createLazyRuntimeModule(() => import("./status-queue.runtime.js")); -function loadAgentHarnessSelectionRuntime(): Promise< - typeof import("../agents/harness/selection.js") -> { - const runtimePromise = (agentHarnessSelectionRuntimePromise ??= - import("../agents/harness/selection.js")); - return runtimePromise; -} - -function loadStatusSubagentsRuntime(): Promise { - const runtimePromise = (statusSubagentsRuntimePromise ??= - import("./status-subagents.runtime.js")); - return runtimePromise; -} - -function loadStatusQueueRuntime(): Promise { - const runtimePromise = (statusQueueRuntimePromise ??= import("./status-queue.runtime.js")); - return runtimePromise; -} - -function loadStatusPluginHealthRuntime(): Promise< - typeof import("./status-plugin-health.runtime.js") -> { - const runtimePromise = (statusPluginHealthRuntimePromise ??= - import("./status-plugin-health.runtime.js")); - return runtimePromise; -} +const loadStatusPluginHealthRuntime = createLazyRuntimeModule( + () => import("./status-plugin-health.runtime.js"), +); // Context lookup stays synchronous/non-refreshing so status output does not // trigger provider/catalog IO while rendering a command response. diff --git a/src/wizard/setup.finalize.ts b/src/wizard/setup.finalize.ts index b1a7fa7cfca3..44ab4ac8aade 100644 --- a/src/wizard/setup.finalize.ts +++ b/src/wizard/setup.finalize.ts @@ -37,6 +37,7 @@ import { ensureControlUiAssetsBuilt } from "../infra/control-ui-assets.js"; import { formatErrorMessage } from "../infra/errors.js"; import { formatWindowsGatewayFirewallGuidance } from "../infra/windows-gateway-firewall-diagnostics.js"; import type { RuntimeEnv } from "../runtime.js"; +import { createLazyRuntimeModule } from "../shared/lazy-runtime.js"; import { launchTuiCli } from "../tui/tui-launch.js"; import { resolveUserPath } from "../utils.js"; import { listConfiguredWebSearchProviders } from "../web-search/runtime.js"; @@ -58,9 +59,6 @@ type FinalizeOnboardingOptions = { runtime: RuntimeEnv; }; -type OnboardSearchModule = typeof import("../commands/onboard-search.js"); - -let onboardSearchModulePromise: Promise | undefined; const HATCH_TUI_TIMEOUT_MS = 5 * 60 * 1000; function buildSessionGatewayAuthOverride(params: { @@ -186,10 +184,9 @@ function getLocalizedGatewayDaemonRuntimeOptions() { })); } -function loadOnboardSearchModule(): Promise { - onboardSearchModulePromise ??= import("../commands/onboard-search.js"); - return onboardSearchModulePromise; -} +const loadOnboardSearchModule = createLazyRuntimeModule( + () => import("../commands/onboard-search.js"), +); export async function finalizeSetupWizard( options: FinalizeOnboardingOptions, diff --git a/src/wizard/setup.migration-import.ts b/src/wizard/setup.migration-import.ts index 59b4c243641e..93f701d498fc 100644 --- a/src/wizard/setup.migration-import.ts +++ b/src/wizard/setup.migration-import.ts @@ -25,6 +25,7 @@ import type { MigrationProviderPlugin, } from "../plugins/types.js"; import type { RuntimeEnv } from "../runtime.js"; +import { createLazyRuntimeModule } from "../shared/lazy-runtime.js"; import { resolveUserPath } from "../utils.js"; import { t } from "./i18n/index.js"; import { WizardCancelledError, type WizardPrompter } from "./prompts.js"; @@ -68,27 +69,15 @@ const MEANINGFUL_WORKSPACE_ENTRIES = [ ] as const; const MEANINGFUL_STATE_ENTRIES = ["credentials", "sessions", "agents"] as const; -let migrationProviderRuntimeModulePromise: Promise< - typeof import("../plugins/migration-provider-runtime.js") -> | null = null; -let migrationContextModulePromise: Promise | null = - null; -let configPathsModulePromise: Promise | null = null; +const loadMigrationProviderRuntimeModule = createLazyRuntimeModule( + () => import("../plugins/migration-provider-runtime.js"), +); -const loadMigrationProviderRuntimeModule = async () => { - migrationProviderRuntimeModulePromise ??= import("../plugins/migration-provider-runtime.js"); - return await migrationProviderRuntimeModulePromise; -}; +const loadMigrationContextModule = createLazyRuntimeModule( + () => import("../commands/migrate/context.js"), +); -const loadMigrationContextModule = async () => { - migrationContextModulePromise ??= import("../commands/migrate/context.js"); - return await migrationContextModulePromise; -}; - -const loadConfigPathsModule = async () => { - configPathsModulePromise ??= import("../config/paths.js"); - return await configPathsModulePromise; -}; +const loadConfigPathsModule = createLazyRuntimeModule(() => import("../config/paths.js")); async function exists(candidate: string): Promise { try { diff --git a/src/wizard/setup.plugin-config.ts b/src/wizard/setup.plugin-config.ts index f8d6590f255d..dd34b838aea5 100644 --- a/src/wizard/setup.plugin-config.ts +++ b/src/wizard/setup.plugin-config.ts @@ -5,6 +5,7 @@ import type { PluginManifestRecord } from "../plugins/manifest-registry.js"; import type { PluginConfigUiHint } from "../plugins/types.js"; import { getPath, setPathCreateStrict } from "../secrets/path-utils.js"; import type { JsonSchemaObject } from "../shared/json-schema.types.js"; +import { createLazyRuntimeModule } from "../shared/lazy-runtime.js"; import { t } from "./i18n/index.js"; import type { WizardPrompter } from "./prompts.js"; @@ -20,14 +21,9 @@ export type ConfigurablePlugin = { jsonSchema?: JsonSchemaObject; }; -type PluginMetadataSnapshotModule = typeof import("../plugins/plugin-metadata-snapshot.js"); - -let pluginMetadataSnapshotModulePromise: Promise | undefined; - -function loadPluginMetadataSnapshotModule(): Promise { - pluginMetadataSnapshotModulePromise ??= import("../plugins/plugin-metadata-snapshot.js"); - return pluginMetadataSnapshotModulePromise; -} +const loadPluginMetadataSnapshotModule = createLazyRuntimeModule( + () => import("../plugins/plugin-metadata-snapshot.js"), +); type JsonSchemaProperty = { type?: string; diff --git a/src/wizard/setup.post-install-migration.ts b/src/wizard/setup.post-install-migration.ts index 8f48b222075a..47dad1de4b28 100644 --- a/src/wizard/setup.post-install-migration.ts +++ b/src/wizard/setup.post-install-migration.ts @@ -8,6 +8,7 @@ import { } from "../plugin-sdk/migration.js"; import type { MigrationProviderPlugin } from "../plugins/types.js"; import type { RuntimeEnv } from "../runtime.js"; +import { createLazyRuntimeModule } from "../shared/lazy-runtime.js"; import type { WizardPrompter } from "./prompts.js"; export type PostInstallMigrationOptions = { @@ -34,19 +35,11 @@ type ResolvedProviderCandidate = { source?: string; }; -let migrationContextModulePromise: Promise | null = - null; -let configPathsModulePromise: Promise | null = null; +const loadMigrationContextModule = createLazyRuntimeModule( + () => import("../commands/migrate/context.js"), +); -const loadMigrationContextModule = async () => { - migrationContextModulePromise ??= import("../commands/migrate/context.js"); - return await migrationContextModulePromise; -}; - -const loadConfigPathsModule = async () => { - configPathsModulePromise ??= import("../config/paths.js"); - return await configPathsModulePromise; -}; +const loadConfigPathsModule = createLazyRuntimeModule(() => import("../config/paths.js")); async function resolveCandidates(params: { config: OpenClawConfig; diff --git a/src/wizard/setup.ts b/src/wizard/setup.ts index 9bd8b1ebbe91..cdefb9f3f295 100644 --- a/src/wizard/setup.ts +++ b/src/wizard/setup.ts @@ -24,6 +24,7 @@ import { } from "../plugins/status.js"; import type { RuntimeEnv } from "../runtime.js"; import { defaultRuntime } from "../runtime.js"; +import { createLazyRuntimeModule } from "../shared/lazy-runtime.js"; import { resolveUserPath } from "../utils.js"; import { t } from "./i18n/index.js"; import { runWizardWithPromptNavigation } from "./navigation-prompter.js"; @@ -43,37 +44,18 @@ import type { QuickstartGatewayDefaults, WizardFlow } from "./setup.types.js"; type SetupFlowChoice = WizardFlow | "import" | "keep-model" | `import:${string}`; -type AuthChoiceModule = typeof import("../commands/auth-choice.js"); -type ConfigLoggingModule = typeof import("../config/logging.js"); -type ModelPickerModule = typeof import("../commands/model-picker.js"); -type OnboardConfigModule = typeof import("../commands/onboard-config.js"); type KeepCurrentAuthChoice = typeof import("../commands/auth-choice-prompt.js").KEEP_CURRENT_AUTH_CHOICE; -let authChoiceModulePromise: Promise | undefined; -let configLoggingModulePromise: Promise | undefined; -let modelPickerModulePromise: Promise | undefined; -let onboardConfigModulePromise: Promise | undefined; +const loadAuthChoiceModule = createLazyRuntimeModule(() => import("../commands/auth-choice.js")); -function loadAuthChoiceModule(): Promise { - authChoiceModulePromise ??= import("../commands/auth-choice.js"); - return authChoiceModulePromise; -} +const loadConfigLoggingModule = createLazyRuntimeModule(() => import("../config/logging.js")); -function loadConfigLoggingModule(): Promise { - configLoggingModulePromise ??= import("../config/logging.js"); - return configLoggingModulePromise; -} +const loadModelPickerModule = createLazyRuntimeModule(() => import("../commands/model-picker.js")); -function loadModelPickerModule(): Promise { - modelPickerModulePromise ??= import("../commands/model-picker.js"); - return modelPickerModulePromise; -} - -function loadOnboardConfigModule(): Promise { - onboardConfigModulePromise ??= import("../commands/onboard-config.js"); - return onboardConfigModulePromise; -} +const loadOnboardConfigModule = createLazyRuntimeModule( + () => import("../commands/onboard-config.js"), +); async function writeWizardConfigFile( configInput: OpenClawConfig,