context-engine: pass runtime context to ContextEngineFactory (#67243)

Merged via squash.

Prepared head SHA: 9aca6a5af1
Co-authored-by: jarimustonen <1272053+jarimustonen@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
This commit is contained in:
Jari Mustonen
2026-04-29 04:21:14 +03:00
committed by GitHub
parent 12c52963ea
commit d8a600f2ad
15 changed files with 298 additions and 25 deletions

View File

@@ -73,6 +73,7 @@ Docs: https://docs.openclaw.ai
- Auto-reply/session: carry the tail of user/assistant turns into the freshly-rotated transcript on silent in-reply session resets (compaction failure, role-ordering conflict) so direct-chat continuity survives the rebind. Fixes #70853. (#70898) Thanks @neeravmakwana.
- Config: skip malformed non-string `env.vars` entries before env-reference checks, so config loading no longer crashes on JSON values like numbers or booleans. (#42402) Thanks @MiltonHeYan.
- Docker Compose: default missing config and workspace bind mounts to `${HOME:-/tmp}/.openclaw` so manual compose runs do not create invalid empty-source volume specs. (#64485) Thanks @jlapenna.
- Agents/context engines: preserve the child agent's configured `agentDir` when subagent cleanup re-resolves a context engine, so `onSubagentEnded` hooks keep operating on the correct per-agent state. (#67243) Thanks @jarimustonen.
- Channels/WhatsApp: restrict pairing verification replies to real inbound user content, preventing unsolicited prompts from receipts, typing indicators, presence updates, and other non-message Baileys upserts. Fixes #73797. (#73823) Thanks @hclsys.
## 2026.4.27

View File

@@ -1,2 +1,2 @@
46476e7b4fee105ca27aed9c769c507f70f02b8ce8586c135feb18e751db0de1 plugin-sdk-api-baseline.json
4bc1c0dc66d910c80694fa1a6b7ba3ab488bf737b3566e53b8a5857c16d2e0b1 plugin-sdk-api-baseline.jsonl
e7d03a0d5aed4f1afb5c7d5e235a166e1e248090632248eaa92b0016531e7f3b plugin-sdk-api-baseline.json
b9bbf8e444b358485cb33c634d3f6f6588004a5c32482c1a473167957269ae58 plugin-sdk-api-baseline.jsonl

View File

@@ -122,7 +122,7 @@ A plugin can register a context engine using the plugin API:
import { buildMemorySystemPromptAddition } from "openclaw/plugin-sdk/core";
export default function register(api) {
api.registerContextEngine("my-engine", () => ({
api.registerContextEngine("my-engine", (ctx) => ({
info: {
id: "my-engine",
name: "My Context Engine",
@@ -154,6 +154,10 @@ export default function register(api) {
}
```
The factory `ctx` includes optional `config`, `agentDir`, and `workspaceDir`
values so plugins can initialize per-agent or per-workspace state before the
first lifecycle hook runs.
Then enable it in config:
```json5

View File

@@ -960,7 +960,7 @@ pipeline rather than just add memory search or hooks.
import { buildMemorySystemPromptAddition } from "openclaw/plugin-sdk/core";
export default function (api) {
api.registerContextEngine("lossless-claw", () => ({
api.registerContextEngine("lossless-claw", (ctx) => ({
info: { id: "lossless-claw", name: "Lossless Claw", ownsCompaction: true },
async ingest() {
return { ingested: true };
@@ -982,6 +982,9 @@ export default function (api) {
}
```
The factory `ctx` exposes optional `config`, `agentDir`, and `workspaceDir`
values for construction-time initialization.
If your engine does **not** own the compaction algorithm, keep `compact()`
implemented and delegate it explicitly:
@@ -992,7 +995,7 @@ import {
} from "openclaw/plugin-sdk/core";
export default function (api) {
api.registerContextEngine("my-memory-engine", () => ({
api.registerContextEngine("my-memory-engine", (ctx) => ({
info: {
id: "my-memory-engine",
name: "My Memory Engine",

View File

@@ -51,8 +51,12 @@ export async function compactEmbeddedPiSession(
allowGatewaySubagentBinding: params.allowGatewaySubagentBinding,
});
ensureContextEnginesInitialized();
const contextEngine = await resolveContextEngine(params.config);
const agentDir = params.agentDir ?? resolveOpenClawAgentDir();
const resolvedWorkspaceDir = resolveUserPath(params.workspaceDir);
const contextEngine = await resolveContextEngine(params.config, {
agentDir,
workspaceDir: resolvedWorkspaceDir,
});
let contextTokenBudget = params.contextTokenBudget;
if (!contextTokenBudget || !Number.isFinite(contextTokenBudget) || contextTokenBudget <= 0) {
const resolvedCompactionTarget = resolveEmbeddedCompactionTarget({
@@ -129,7 +133,7 @@ export async function compactEmbeddedPiSession(
sessionId: params.sessionId,
agentId: sessionAgentId,
sessionKey: hookSessionKey,
workspaceDir: resolveUserPath(params.workspaceDir),
workspaceDir: resolvedWorkspaceDir,
messageProvider: resolvedMessageProvider,
};
const runtimeContext = contextEngineRuntimeContext;

View File

@@ -740,7 +740,10 @@ export async function runEmbeddedPiAgent(
// Resolve the context engine once and reuse across retries to avoid
// repeated initialization/connection overhead per attempt.
ensureContextEnginesInitialized();
const contextEngine = await resolveContextEngine(params.config);
const contextEngine = await resolveContextEngine(params.config, {
agentDir,
workspaceDir: resolvedWorkspace,
});
try {
let activeSessionId = params.sessionId;
let activeSessionFile = params.sessionFile;

View File

@@ -72,6 +72,7 @@ export function createSubagentRegistryLifecycleController(params: {
notifyContextEngineSubagentEnded(args: {
childSessionKey: string;
reason: "completed" | "deleted";
agentDir?: string;
workspaceDir?: string;
}): Promise<void>;
resumeSubagentRun(runId: string): void;
@@ -421,6 +422,7 @@ export function createSubagentRegistryLifecycleController(params: {
void params.notifyContextEngineSubagentEnded({
childSessionKey: cleanupParams.entry.childSessionKey,
reason: "deleted",
agentDir: cleanupParams.entry.agentDir,
workspaceDir: cleanupParams.entry.workspaceDir,
});
params.runs.delete(cleanupParams.runId);
@@ -431,6 +433,7 @@ export function createSubagentRegistryLifecycleController(params: {
void params.notifyContextEngineSubagentEnded({
childSessionKey: cleanupParams.entry.childSessionKey,
reason: "completed",
agentDir: cleanupParams.entry.agentDir,
workspaceDir: cleanupParams.entry.workspaceDir,
});
cleanupParams.entry.cleanupCompletedAt = cleanupParams.completedAt;

View File

@@ -91,6 +91,7 @@ export type RegisterSubagentRunParams = {
cleanup: "delete" | "keep";
label?: string;
model?: string;
agentDir?: string;
workspaceDir?: string;
runTimeoutSeconds?: number;
expectsCompletionMessage?: boolean;
@@ -124,6 +125,7 @@ export function createSubagentRunManager(params: {
notifyContextEngineSubagentEnded(args: {
childSessionKey: string;
reason: "completed" | "deleted" | "released";
agentDir?: string;
workspaceDir?: string;
}): Promise<void>;
completeCleanupBookkeeping(args: {
@@ -402,6 +404,7 @@ export function createSubagentRunManager(params: {
spawnMode,
label: registerParams.label,
model: registerParams.model,
agentDir: registerParams.agentDir,
workspaceDir: registerParams.workspaceDir,
runTimeoutSeconds,
createdAt: now,
@@ -458,6 +461,7 @@ export function createSubagentRunManager(params: {
void params.notifyContextEngineSubagentEnded({
childSessionKey: entry.childSessionKey,
reason: "released",
agentDir: entry.agentDir,
workspaceDir: entry.workspaceDir,
});
}

View File

@@ -851,6 +851,7 @@ describe("subagent registry seam flow", () => {
cleanup: "keep",
expectsCompletionMessage: undefined,
spawnMode: "run",
agentDir: "/tmp/agent-alt",
workspaceDir: "/tmp/workspace",
createdAt: 1,
startedAt: 1,
@@ -863,6 +864,7 @@ describe("subagent registry seam flow", () => {
await waitForFast(() => {
expect(mocks.onSubagentEnded).toHaveBeenCalledWith({
agentDir: "/tmp/agent-alt",
childSessionKey: "agent:main:session:child",
reason: "released",
workspaceDir: "/tmp/workspace",
@@ -877,5 +879,82 @@ describe("subagent registry seam flow", () => {
allowGatewaySubagentBinding: true,
});
expect(mocks.ensureContextEnginesInitialized).toHaveBeenCalledTimes(1);
expect(mocks.resolveContextEngine).toHaveBeenCalledWith(
{
agents: { defaults: { subagents: { archiveAfterMinutes: 0 } } },
session: { mainKey: "main", scope: "per-sender" },
},
{
agentDir: "/tmp/agent-alt",
workspaceDir: "/tmp/workspace",
},
);
});
it("passes stored agentDir through swept context-engine cleanup paths", async () => {
const now = Date.parse("2026-03-24T12:00:00Z");
mod.addSubagentRunForTests({
runId: "run-session-swept-context-engine",
childSessionKey: "agent:alt:session:child-session",
controllerSessionKey: "agent:main:session:parent",
requesterSessionKey: "agent:main:session:parent",
requesterOrigin: undefined,
requesterDisplayKey: "parent",
task: "session cleanup",
cleanup: "keep",
expectsCompletionMessage: undefined,
spawnMode: "session",
agentDir: "/tmp/agent-session",
workspaceDir: "/tmp/workspace-session",
createdAt: now - 20_000,
startedAt: now - 10_000,
sessionStartedAt: now - 10_000,
accumulatedRuntimeMs: 0,
endedAt: now - 8_000,
outcome: { status: "ok", startedAt: now - 10_000, endedAt: now - 8_000, elapsedMs: 2_000 },
cleanupHandled: true,
cleanupCompletedAt: now - 6 * 60_000,
});
mod.addSubagentRunForTests({
runId: "run-archive-swept-context-engine",
childSessionKey: "agent:alt:session:child-archive",
controllerSessionKey: "agent:main:session:parent",
requesterSessionKey: "agent:main:session:parent",
requesterOrigin: undefined,
requesterDisplayKey: "parent",
task: "archive cleanup",
cleanup: "delete",
expectsCompletionMessage: undefined,
spawnMode: "run",
agentDir: "/tmp/agent-archive",
workspaceDir: "/tmp/workspace-archive",
createdAt: now - 20_000,
startedAt: now - 10_000,
sessionStartedAt: now - 10_000,
accumulatedRuntimeMs: 0,
endedAt: now - 8_000,
outcome: { status: "ok", startedAt: now - 10_000, endedAt: now - 8_000, elapsedMs: 2_000 },
archiveAtMs: now - 1,
cleanupHandled: true,
});
await mod.__testing.sweepOnceForTests();
await waitForFast(() => {
expect(mocks.resolveContextEngine).toHaveBeenCalledWith(
expect.any(Object),
expect.objectContaining({
agentDir: "/tmp/agent-session",
workspaceDir: "/tmp/workspace-session",
}),
);
expect(mocks.resolveContextEngine).toHaveBeenCalledWith(
expect.any(Object),
expect.objectContaining({
agentDir: "/tmp/agent-archive",
workspaceDir: "/tmp/workspace-archive",
}),
);
});
});
});

View File

@@ -7,6 +7,7 @@ import {
type SessionEntry,
} from "../config/sessions.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import type { ResolveContextEngineOptions } from "../context-engine/registry.js";
import type { ContextEngine, SubagentEndReason } from "../context-engine/types.js";
import { callGateway } from "../gateway/call.js";
import { getAgentRunContext, onAgentEvent } from "../infra/agent-events.js";
@@ -97,7 +98,10 @@ type SubagentRegistryDeps = {
runSubagentAnnounceFlow: SubagentAnnounceModule["runSubagentAnnounceFlow"];
ensureContextEnginesInitialized?: () => void;
ensureRuntimePluginsLoaded?: typeof ensureRuntimePluginsLoadedFn;
resolveContextEngine?: (cfg: OpenClawConfig) => Promise<ContextEngine>;
resolveContextEngine?: (
cfg?: OpenClawConfig,
options?: ResolveContextEngineOptions,
) => Promise<ContextEngine>;
};
let subagentAnnouncePromise: Promise<SubagentAnnounceModule> | null = null;
@@ -140,7 +144,10 @@ type ContextEngineInitModule = Pick<
>;
type ContextEngineRegistryModule = Pick<
{
resolveContextEngine: (cfg: OpenClawConfig) => Promise<ContextEngine>;
resolveContextEngine: (
cfg?: OpenClawConfig,
options?: ResolveContextEngineOptions,
) => Promise<ContextEngine>;
},
"resolveContextEngine"
>;
@@ -308,7 +315,10 @@ async function ensureSubagentRegistryPluginRuntimeLoaded(params: {
(await loadRuntimePluginsModule()).ensureRuntimePluginsLoaded(params);
}
async function resolveSubagentRegistryContextEngine(cfg: OpenClawConfig) {
async function resolveSubagentRegistryContextEngine(
cfg: OpenClawConfig,
options?: ResolveContextEngineOptions,
) {
const initModule = await loadContextEngineInitModule();
const registryModule = await loadContextEngineRegistryModule();
const ensureContextEnginesInitialized =
@@ -317,7 +327,7 @@ async function resolveSubagentRegistryContextEngine(cfg: OpenClawConfig) {
const resolveContextEngine =
subagentRegistryDeps.resolveContextEngine ?? registryModule.resolveContextEngine;
ensureContextEnginesInitialized();
return await resolveContextEngine(cfg);
return await resolveContextEngine(cfg, options);
}
function persistSubagentRuns() {
@@ -469,6 +479,7 @@ function schedulePendingLifecycleTimeout(params: { runId: string; endedAt: numbe
async function notifyContextEngineSubagentEnded(params: {
childSessionKey: string;
reason: SubagentEndReason;
agentDir?: string;
workspaceDir?: string;
}) {
try {
@@ -478,7 +489,10 @@ async function notifyContextEngineSubagentEnded(params: {
workspaceDir: params.workspaceDir,
allowGatewaySubagentBinding: true,
});
const engine = await resolveSubagentRegistryContextEngine(cfg);
const engine = await resolveSubagentRegistryContextEngine(cfg, {
agentDir: params.agentDir,
workspaceDir: params.workspaceDir,
});
if (!engine.onSubagentEnded) {
return;
}
@@ -812,6 +826,7 @@ async function sweepSubagentRuns() {
void notifyContextEngineSubagentEnded({
childSessionKey: entry.childSessionKey,
reason: "swept",
agentDir: entry.agentDir,
workspaceDir: entry.workspaceDir,
});
subagentRuns.delete(runId);
@@ -851,6 +866,7 @@ async function sweepSubagentRuns() {
void notifyContextEngineSubagentEnded({
childSessionKey: entry.childSessionKey,
reason: "swept",
agentDir: entry.agentDir,
workspaceDir: entry.workspaceDir,
});
}

View File

@@ -14,6 +14,7 @@ export type SubagentRunRecord = {
cleanup: "delete" | "keep";
label?: string;
model?: string;
agentDir?: string;
workspaceDir?: string;
runTimeoutSeconds?: number;
spawnMode?: SpawnSubagentMode;

View File

@@ -11,6 +11,7 @@ import type { SubagentLifecycleHookRunner } from "../plugins/hooks.js";
import { isValidAgentId, normalizeAgentId, parseAgentSessionKey } from "../routing/session-key.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import type { DeliveryContext } from "../utils/delivery-context.types.js";
import { resolveAgentDir } from "./agent-scope-config.js";
import type { BootstrapContextMode } from "./bootstrap-files.js";
import {
mapToolContextToSpawnedRunMetadata,
@@ -789,6 +790,7 @@ export async function spawnSubagentDirect(
depth: childDepth,
maxSpawnDepth,
});
const targetAgentDir = resolveAgentDir(cfg, targetAgentId);
const targetAgentConfig = resolveAgentConfig(cfg, targetAgentId);
const plan = resolveSubagentModelAndThinkingPlan({
cfg,
@@ -1163,6 +1165,7 @@ export async function spawnSubagentDirect(
cleanup,
label: label || undefined,
model: resolvedModel,
agentDir: targetAgentDir,
workspaceDir: spawnedMetadata.workspaceDir,
runTimeoutSeconds,
expectsCompletionMessage: shouldAnnounceCompletion,

View File

@@ -17,7 +17,11 @@ import {
listContextEngineIds,
resolveContextEngine,
} from "./registry.js";
import type { ContextEngineFactory, ContextEngineRegistrationResult } from "./registry.js";
import type {
ContextEngineFactory,
ContextEngineFactoryContext,
ContextEngineRegistrationResult,
} from "./registry.js";
import type {
ContextEngine,
ContextEngineInfo,
@@ -356,7 +360,7 @@ describe("Engine contract tests", () => {
const resolved = getContextEngineFactory("mock");
expect(resolved).toBe(factory);
const engine = await resolved!();
const engine = await resolved!({});
expect(engine).toBeInstanceOf(MockContextEngine);
expect(engine.info.id).toBe("mock");
});
@@ -693,6 +697,108 @@ describe("Default engine selection", () => {
});
});
// ═══════════════════════════════════════════════════════════════════════════
// 3b. Factory context passing
// ═══════════════════════════════════════════════════════════════════════════
describe("Factory context passing", () => {
it("passes ContextEngineFactoryContext to factories that accept a parameter", async () => {
const engineId = `factory-ctx-${Date.now().toString(36)}`;
let receivedCtx: ContextEngineFactoryContext | undefined;
const factory: ContextEngineFactory = (ctx: ContextEngineFactoryContext) => {
receivedCtx = ctx;
return {
info: { id: engineId, name: "Ctx Engine" },
async ingest() {
return { ingested: true };
},
async assemble({ messages }: { messages: AgentMessage[] }) {
return { messages, estimatedTokens: 0 };
},
async compact() {
return { ok: true, compacted: false };
},
};
};
registerContextEngine(engineId, factory);
const cfg = configWithSlot(engineId);
await resolveContextEngine(cfg, {
agentDir: "/tmp/agent",
workspaceDir: "/tmp/workspace",
});
expect(receivedCtx).toBeDefined();
expect(receivedCtx!.config).toBe(cfg);
expect(receivedCtx!.agentDir).toBe("/tmp/agent");
expect(receivedCtx!.workspaceDir).toBe("/tmp/workspace");
});
it("no-arg factories still work when context is passed", async () => {
const engineId = `factory-noarg-${Date.now().toString(36)}`;
let called = false;
const factory: ContextEngineFactory = () => {
called = true;
return {
info: { id: engineId, name: "No-Arg Engine" },
async ingest() {
return { ingested: true };
},
async assemble({ messages }: { messages: AgentMessage[] }) {
return { messages, estimatedTokens: 0 };
},
async compact() {
return { ok: true, compacted: false };
},
};
};
registerContextEngine(engineId, factory);
const engine = await resolveContextEngine(configWithSlot(engineId), {
agentDir: "/tmp/agent",
workspaceDir: "/tmp/workspace",
});
expect(called).toBe(true);
expect(engine.info.id).toBe(engineId);
});
it("passes undefined config when resolveContextEngine is called without config", async () => {
let receivedCtx: ContextEngineFactoryContext | undefined;
// Override the default "legacy" engine to intercept the no-config path
registerContextEngineForOwner(
"legacy",
(ctx: ContextEngineFactoryContext) => {
receivedCtx = ctx;
return {
info: { id: "legacy", name: "NoConfig Engine", version: "1" },
async ingest() {
return { ingested: true };
},
async assemble({ messages }: { messages: AgentMessage[] }) {
return { messages, estimatedTokens: 0 };
},
async compact() {
return { ok: true, compacted: false };
},
};
},
"core",
{ allowSameOwnerRefresh: true },
);
await resolveContextEngine(undefined);
expect(receivedCtx).toBeDefined();
expect(receivedCtx!.config).toBeUndefined();
expect(receivedCtx!.agentDir).toBeUndefined();
expect(receivedCtx!.workspaceDir).toBeUndefined();
});
});
// ═══════════════════════════════════════════════════════════════════════════
// 4. Invalid engine fallback
// ═══════════════════════════════════════════════════════════════════════════

View File

@@ -4,11 +4,29 @@ import { resolveGlobalSingleton } from "../shared/global-singleton.js";
import { sanitizeForLog } from "../terminal/ansi.js";
import type { ContextEngine } from "./types.js";
/**
* Runtime context passed to context engine factories during resolution.
* Provides config and path information so plugins can initialize engines
* without fragile workarounds.
*/
export type ContextEngineFactoryContext = {
config?: OpenClawConfig;
agentDir?: string;
workspaceDir?: string;
};
/**
* A factory that creates a ContextEngine instance.
* Supports async creation for engines that need DB connections etc.
*
* The factory receives a {@link ContextEngineFactoryContext} with runtime
* environment context (config, paths). Existing no-arg factories remain
* backward compatible because TypeScript permits assigning functions with
* fewer parameters to wider signatures.
*/
export type ContextEngineFactory = () => ContextEngine | Promise<ContextEngine>;
export type ContextEngineFactory = (
ctx: ContextEngineFactoryContext,
) => ContextEngine | Promise<ContextEngine>;
export type ContextEngineRegistrationResult = { ok: true } | { ok: false; existingOwner: string };
type RegisterContextEngineForOwnerOptions = {
@@ -455,6 +473,14 @@ function describeResolvedContextEngineContractError(
// Resolution
// ---------------------------------------------------------------------------
/**
* Options for {@link resolveContextEngine}.
*/
export type ResolveContextEngineOptions = {
agentDir?: string;
workspaceDir?: string;
};
/**
* Resolve which ContextEngine to use based on plugin slot configuration.
*
@@ -462,11 +488,19 @@ function describeResolvedContextEngineContractError(
* 1. `config.plugins.slots.contextEngine` (explicit slot override)
* 2. Default slot value ("legacy")
*
* When `config` is provided it is forwarded to the factory as part of a
* {@link ContextEngineFactoryContext}. Additional runtime paths can be
* supplied via `options`. Existing no-arg factories continue to work
* because JavaScript permits extra arguments at call sites.
*
* Non-default engines that fail (unregistered, factory throw, or contract
* violation) are logged and silently replaced by the default engine.
* Throws only when the default engine itself cannot be resolved.
*/
export async function resolveContextEngine(config?: OpenClawConfig): Promise<ContextEngine> {
export async function resolveContextEngine(
config?: OpenClawConfig,
options?: ResolveContextEngineOptions,
): Promise<ContextEngine> {
const slotValue = config?.plugins?.slots?.contextEngine;
const engineId =
typeof slotValue === "string" && slotValue.trim()
@@ -476,6 +510,12 @@ export async function resolveContextEngine(config?: OpenClawConfig): Promise<Con
const defaultEngineId = defaultSlotIdForKey("contextEngine");
const isDefaultEngine = engineId === defaultEngineId;
const factoryCtx: ContextEngineFactoryContext = {
config,
agentDir: options?.agentDir,
workspaceDir: options?.workspaceDir,
};
const entry = getContextEngineRegistryState().engines.get(engineId);
if (!entry) {
if (isDefaultEngine) {
@@ -488,12 +528,12 @@ export async function resolveContextEngine(config?: OpenClawConfig): Promise<Con
`[context-engine] Context engine "${sanitizeForLog(engineId)}" is not registered; ` +
`falling back to default engine "${defaultEngineId}".`,
);
return resolveDefaultContextEngine(defaultEngineId);
return resolveDefaultContextEngine(defaultEngineId, factoryCtx);
}
let engine: ContextEngine;
try {
engine = await entry.factory();
engine = await entry.factory(factoryCtx);
} catch (factoryError) {
if (isDefaultEngine) {
throw factoryError;
@@ -503,7 +543,7 @@ export async function resolveContextEngine(config?: OpenClawConfig): Promise<Con
`${sanitizeForLog(factoryError instanceof Error ? factoryError.message : String(factoryError))}; ` +
`falling back to default engine "${defaultEngineId}".`,
);
return resolveDefaultContextEngine(defaultEngineId);
return resolveDefaultContextEngine(defaultEngineId, factoryCtx);
}
let contractError: string | null;
@@ -518,7 +558,7 @@ export async function resolveContextEngine(config?: OpenClawConfig): Promise<Con
`${sanitizeForLog(validationError instanceof Error ? validationError.message : String(validationError))}; ` +
`falling back to default engine "${defaultEngineId}".`,
);
return resolveDefaultContextEngine(defaultEngineId);
return resolveDefaultContextEngine(defaultEngineId, factoryCtx);
}
if (contractError) {
if (isDefaultEngine) {
@@ -528,7 +568,7 @@ export async function resolveContextEngine(config?: OpenClawConfig): Promise<Con
console.error(
`[context-engine] ${sanitizeForLog(contractError)}; falling back to default engine "${defaultEngineId}".`,
);
return resolveDefaultContextEngine(defaultEngineId);
return resolveDefaultContextEngine(defaultEngineId, factoryCtx);
}
return wrapContextEngineWithSessionKeyCompat(engine);
@@ -540,7 +580,10 @@ export async function resolveContextEngine(config?: OpenClawConfig): Promise<Con
* This helper is intentionally strict: if the default engine itself fails,
* there is no further fallback and the error must propagate.
*/
async function resolveDefaultContextEngine(defaultEngineId: string): Promise<ContextEngine> {
async function resolveDefaultContextEngine(
defaultEngineId: string,
factoryCtx: ContextEngineFactoryContext,
): Promise<ContextEngine> {
const defaultEntry = getContextEngineRegistryState().engines.get(defaultEngineId);
if (!defaultEntry) {
throw new Error(
@@ -548,7 +591,7 @@ async function resolveDefaultContextEngine(defaultEngineId: string): Promise<Con
`Available engines: ${listContextEngineIds().join(", ") || "(none)"}`,
);
}
const engine = await defaultEntry.factory();
const engine = await defaultEntry.factory(factoryCtx);
const contractError = describeResolvedContextEngineContractError(defaultEngineId, engine);
if (contractError) {
throw new Error(`[context-engine] ${contractError}`);

View File

@@ -95,7 +95,10 @@ export type { RuntimeEnv } from "../runtime.js";
export type { HookEntry } from "../hooks/types.js";
export type { ReplyPayload } from "./reply-payload.js";
export type { WizardPrompter } from "../wizard/prompts.js";
export type { ContextEngineFactory } from "../context-engine/registry.js";
export type {
ContextEngineFactory,
ContextEngineFactoryContext,
} from "../context-engine/registry.js";
export type { DiagnosticEventPayload } from "../infra/diagnostic-events.js";
export type { DiagnosticTraceContext } from "../infra/diagnostic-trace-context.js";
export type {