fix(runtime): hide lazy command and context seams from static graph

This commit is contained in:
Vincent Koc
2026-04-12 12:22:24 +01:00
parent 1fe14627a2
commit 74f31241ed
7 changed files with 179 additions and 24 deletions

View File

@@ -5,6 +5,7 @@ import type { ContextEngine, SubagentEndReason } from "../context-engine/types.j
import { callGateway } from "../gateway/call.js";
import { onAgentEvent } from "../infra/agent-events.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
import { importRuntimeModule } from "../shared/runtime-import.js";
import { type DeliveryContext, normalizeDeliveryContext } from "../utils/delivery-context.js";
import type { ensureRuntimePluginsLoaded as ensureRuntimePluginsLoadedFn } from "./runtime-plugins.js";
import type { SubagentRunOutcome } from "./subagent-announce-output.js";
@@ -93,9 +94,30 @@ const defaultSubagentRegistryDeps: SubagentRegistryDeps = {
};
let subagentRegistryDeps: SubagentRegistryDeps = defaultSubagentRegistryDeps;
let subagentRegistryRuntimePromise: Promise<
typeof import("./subagent-registry.runtime.js")
> | null = null;
type ContextEngineInitModule = Pick<
{
ensureContextEnginesInitialized: () => void;
},
"ensureContextEnginesInitialized"
>;
type ContextEngineRegistryModule = Pick<
{
resolveContextEngine: (cfg: OpenClawConfig) => Promise<ContextEngine>;
},
"resolveContextEngine"
>;
type RuntimePluginsModule = Pick<
{
ensureRuntimePluginsLoaded: typeof ensureRuntimePluginsLoadedFn;
},
"ensureRuntimePluginsLoaded"
>;
const SUBAGENT_REGISTRY_RUNTIME_SPEC = ["./subagent-registry.runtime", ".js"] as const;
let contextEngineInitPromise: Promise<ContextEngineInitModule> | null = null;
let contextEngineRegistryPromise: Promise<ContextEngineRegistryModule> | null = null;
let runtimePluginsPromise: Promise<RuntimePluginsModule> | null = null;
let sweeper: NodeJS.Timeout | null = null;
let sweepInProgress = false;
@@ -117,9 +139,28 @@ const SESSION_RUN_TTL_MS = 5 * 60_000; // 5 minutes
/** Absolute TTL for orphaned pendingLifecycleError entries. */
const PENDING_ERROR_TTL_MS = 5 * 60_000; // 5 minutes
function loadSubagentRegistryRuntime() {
subagentRegistryRuntimePromise ??= import("./subagent-registry.runtime.js");
return subagentRegistryRuntimePromise;
function loadContextEngineInitModule(): Promise<ContextEngineInitModule> {
contextEngineInitPromise ??= importRuntimeModule<ContextEngineInitModule>(
import.meta.url,
SUBAGENT_REGISTRY_RUNTIME_SPEC,
);
return contextEngineInitPromise;
}
function loadContextEngineRegistryModule(): Promise<ContextEngineRegistryModule> {
contextEngineRegistryPromise ??= importRuntimeModule<ContextEngineRegistryModule>(
import.meta.url,
SUBAGENT_REGISTRY_RUNTIME_SPEC,
);
return contextEngineRegistryPromise;
}
function loadRuntimePluginsModule(): Promise<RuntimePluginsModule> {
runtimePluginsPromise ??= importRuntimeModule<RuntimePluginsModule>(
import.meta.url,
SUBAGENT_REGISTRY_RUNTIME_SPEC,
);
return runtimePluginsPromise;
}
async function ensureSubagentRegistryPluginRuntimeLoaded(params: {
@@ -132,16 +173,17 @@ async function ensureSubagentRegistryPluginRuntimeLoaded(params: {
ensureRuntimePluginsLoaded(params);
return;
}
const runtime = await loadSubagentRegistryRuntime();
runtime.ensureRuntimePluginsLoaded(params);
(await loadRuntimePluginsModule()).ensureRuntimePluginsLoaded(params);
}
async function resolveSubagentRegistryContextEngine(cfg: OpenClawConfig) {
const runtime = await loadSubagentRegistryRuntime();
const initModule = await loadContextEngineInitModule();
const registryModule = await loadContextEngineRegistryModule();
const ensureContextEnginesInitialized =
subagentRegistryDeps.ensureContextEnginesInitialized ?? runtime.ensureContextEnginesInitialized;
subagentRegistryDeps.ensureContextEnginesInitialized ??
initModule.ensureContextEnginesInitialized;
const resolveContextEngine =
subagentRegistryDeps.resolveContextEngine ?? runtime.resolveContextEngine;
subagentRegistryDeps.resolveContextEngine ?? registryModule.resolveContextEngine;
ensureContextEnginesInitialized();
return await resolveContextEngine(cfg);
}
@@ -696,7 +738,9 @@ export function resetSubagentRegistryForTests(opts?: { persist?: boolean }) {
resumedRuns.clear();
endedHookInFlightRunIds.clear();
clearAllPendingLifecycleErrors();
subagentRegistryRuntimePromise = null;
contextEngineInitPromise = null;
contextEngineRegistryPromise = null;
runtimePluginsPromise = null;
resetAnnounceQueuesForTests();
stopSweeper();
sweepInProgress = false;

View File

@@ -21,6 +21,7 @@ import {
resolveAgentIdFromSessionKey,
} from "../../routing/session-key.js";
import { applyModelOverrideToSessionEntry } from "../../sessions/model-overrides.js";
import { importRuntimeModule } from "../../shared/runtime-import.js";
import { normalizeOptionalLowercaseString } from "../../shared/string-coerce.js";
import { buildTaskStatusSnapshotForRelatedSessionKeyForOwner } from "../../tasks/task-owner-access.js";
import { formatTaskStatusDetail, formatTaskStatusTitle } from "../../tasks/task-status.js";
@@ -54,12 +55,47 @@ const SessionStatusToolSchema = Type.Object({
model: Type.Optional(Type.String()),
});
let commandsStatusRuntimePromise: Promise<
typeof import("../../auto-reply/reply/commands-status.runtime.js")
> | null = null;
type CommandsStatusRuntimeModule = {
buildStatusText: (params: {
cfg: OpenClawConfig;
sessionEntry?: SessionEntry;
sessionKey: string;
parentSessionKey?: string;
sessionScope?: "per-sender" | "per-thread" | "shared";
storePath?: string;
statusChannel: string;
provider: string;
model: string;
contextTokens?: number;
resolvedThinkLevel?: ThinkLevel;
resolvedFastMode?: boolean;
resolvedVerboseLevel: VerboseLevel;
resolvedReasoningLevel: ReasoningLevel;
resolvedElevatedLevel?: ElevatedLevel;
resolveDefaultThinkingLevel: () => Promise<ThinkLevel | undefined>;
isGroup: boolean;
defaultGroupActivation: () => "always" | "mention";
taskLineOverride?: string;
skipDefaultTaskLookup?: boolean;
primaryModelLabelOverride?: string;
modelAuthOverride?: string;
activeModelAuthOverride?: string;
includeTranscriptUsage?: boolean;
}) => Promise<string>;
};
function loadCommandsStatusRuntime() {
commandsStatusRuntimePromise ??= import("../../auto-reply/reply/commands-status.runtime.js");
const COMMANDS_STATUS_RUNTIME_SPEC = [
"../../auto-reply/reply/commands-status.runtime",
".js",
] as const;
let commandsStatusRuntimePromise: Promise<CommandsStatusRuntimeModule> | null = null;
function loadCommandsStatusRuntime(): Promise<CommandsStatusRuntimeModule> {
commandsStatusRuntimePromise ??= importRuntimeModule<CommandsStatusRuntimeModule>(
import.meta.url,
COMMANDS_STATUS_RUNTIME_SPEC,
);
return commandsStatusRuntimePromise;
}

View File

@@ -0,0 +1,3 @@
export { listControlledSubagentRuns } from "../../agents/subagent-control.js";
export { countPendingDescendantRuns } from "../../agents/subagent-registry.js";
export { buildSubagentsStatusLine } from "./commands-status-subagents.js";

View File

@@ -7,8 +7,6 @@ import {
} from "../../agents/agent-scope.js";
import { resolveFastModeState } from "../../agents/fast-mode.js";
import { resolveModelAuthLabel } from "../../agents/model-auth-label.js";
import { listControlledSubagentRuns } from "../../agents/subagent-control.js";
import { countPendingDescendantRuns } from "../../agents/subagent-registry.js";
import {
resolveInternalSessionKey,
resolveMainSessionAlias,
@@ -23,6 +21,7 @@ import {
resolveUsageProviderId,
} from "../../infra/provider-usage.js";
import type { MediaUnderstandingDecision } from "../../media-understanding/types.js";
import { importRuntimeModule } from "../../shared/runtime-import.js";
import { normalizeOptionalLowercaseString } from "../../shared/string-coerce.js";
import {
listTasksForAgentIdForStatus,
@@ -35,10 +34,8 @@ import {
} from "../../tasks/task-status.js";
import { normalizeGroupActivation } from "../group-activation.js";
import { resolveSelectedAndActiveModel } from "../model-runtime.js";
import { buildStatusMessage } from "../status.js";
import type { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel } from "../thinking.js";
import type { ReplyPayload } from "../types.js";
import { buildSubagentsStatusLine } from "./commands-status-subagents.js";
import type { CommandContext } from "./commands-types.js";
import { getFollowupQueueDepth, resolveQueueSettings } from "./queue.js";
@@ -51,6 +48,43 @@ const USAGE_OAUTH_ONLY_PROVIDERS = new Set([
"openai-codex",
]);
type StatusRuntimeModule = {
buildStatusMessage: (args: Record<string, unknown>) => string;
};
type CommandsStatusSubagentsModule = {
buildSubagentsStatusLine: (params: {
runs: Array<{ childSessionKey: string; endedAt?: number | null }>;
verboseEnabled: boolean;
pendingDescendantsForRun: (entry: { childSessionKey: string }) => number;
}) => string | undefined;
countPendingDescendantRuns: (rootSessionKey: string) => number;
listControlledSubagentRuns: (
controllerSessionKey: string,
) => Array<{ childSessionKey: string; endedAt?: number | null }>;
};
const STATUS_RUNTIME_SPEC = ["../status.runtime", ".js"] as const;
const COMMANDS_STATUS_DEPS_RUNTIME_SPEC = ["./commands-status-deps.runtime", ".js"] as const;
let statusRuntimePromise: Promise<StatusRuntimeModule> | null = null;
let commandsStatusDepsRuntimePromise: Promise<CommandsStatusSubagentsModule> | null = null;
function loadStatusRuntime(): Promise<StatusRuntimeModule> {
statusRuntimePromise ??= importRuntimeModule<StatusRuntimeModule>(
import.meta.url,
STATUS_RUNTIME_SPEC,
);
return statusRuntimePromise;
}
function loadCommandsStatusDepsRuntime(): Promise<CommandsStatusSubagentsModule> {
commandsStatusDepsRuntimePromise ??= importRuntimeModule<CommandsStatusSubagentsModule>(
import.meta.url,
COMMANDS_STATUS_DEPS_RUNTIME_SPEC,
);
return commandsStatusDepsRuntimePromise;
}
function shouldLoadUsageSummary(params: {
provider?: string;
selectedModelAuth?: string;
@@ -274,6 +308,8 @@ export async function buildStatusText(params: {
if (!taskLine && !params.skipDefaultTaskLookup) {
taskLine = formatAgentTaskCountsLine(statusAgentId);
}
const { buildSubagentsStatusLine, countPendingDescendantRuns, listControlledSubagentRuns } =
await loadCommandsStatusDepsRuntime();
const runs = listControlledSubagentRuns(requesterKey);
const verboseEnabled = resolvedVerboseLevel && resolvedVerboseLevel !== "off";
subagentsLine = buildSubagentsStatusLine({
@@ -297,6 +333,7 @@ export async function buildStatusText(params: {
sessionEntry,
}).enabled;
const agentFallbacksOverride = resolveAgentModelFallbacksOverride(cfg, statusAgentId);
const { buildStatusMessage } = await loadStatusRuntime();
const statusText = buildStatusMessage({
config: cfg,
agent: {

View File

@@ -0,0 +1 @@
export { buildStatusMessage } from "./status.js";

View File

@@ -1,8 +1,36 @@
import { normalizeStructuredPromptSection } from "../agents/prompt-cache-stability.js";
import type { MemoryCitationsMode } from "../config/types.memory.js";
import { buildMemoryPromptSection } from "../plugins/memory-state.js";
import { importRuntimeModule } from "../shared/runtime-import.js";
import type { ContextEngine, CompactResult, ContextEngineRuntimeContext } from "./types.js";
type CompactRuntimeModule = {
compactEmbeddedPiSessionDirect: (params: Record<string, unknown>) => Promise<{
ok: boolean;
compacted: boolean;
reason?: string;
result?: {
summary?: string;
firstKeptEntryId?: string;
tokensBefore?: number;
tokensAfter?: number;
details?: unknown;
};
}>;
};
const COMPACT_RUNTIME_SPEC = ["../agents/pi-embedded-runner/compact.runtime", ".js"] as const;
let compactRuntimePromise: Promise<CompactRuntimeModule> | null = null;
function loadCompactRuntime(): Promise<CompactRuntimeModule> {
compactRuntimePromise ??= importRuntimeModule<CompactRuntimeModule>(
import.meta.url,
COMPACT_RUNTIME_SPEC,
);
return compactRuntimePromise;
}
/**
* Delegate a context-engine compaction request to OpenClaw's built-in runtime compaction path.
*
@@ -19,9 +47,9 @@ import type { ContextEngine, CompactResult, ContextEngineRuntimeContext } from "
export async function delegateCompactionToRuntime(
params: Parameters<ContextEngine["compact"]>[0],
): Promise<CompactResult> {
// Import through a dedicated runtime boundary so the lazy edge remains effective.
const { compactEmbeddedPiSessionDirect } =
await import("../agents/pi-embedded-runner/compact.runtime.js");
// Load through the dedicated runtime boundary without introducing another
// source-level static edge into the embedded runner graph.
const { compactEmbeddedPiSessionDirect } = await loadCompactRuntime();
type RuntimeCompactionParams = Parameters<typeof compactEmbeddedPiSessionDirect>[0];
// runtimeContext carries the full CompactEmbeddedPiSessionParams fields set

View File

@@ -0,0 +1,6 @@
export async function importRuntimeModule<T>(
baseUrl: string,
parts: readonly string[],
): Promise<T> {
return (await import(new URL(parts.join(""), baseUrl).href)) as T;
}