Skip to content

Commit d8a600f

Browse files
context-engine: pass runtime context to ContextEngineFactory (#67243)
Merged via squash. Prepared head SHA: 9aca6a5 Co-authored-by: jarimustonen <1272053+jarimustonen@users.noreply.github.com> Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com> Reviewed-by: @jalehman
1 parent 12c5296 commit d8a600f

15 files changed

Lines changed: 298 additions & 25 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ Docs: https://docs.openclaw.ai
7373
- 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.
7474
- 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.
7575
- 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.
76+
- 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.
7677
- 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.
7778

7879
## 2026.4.27
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
46476e7b4fee105ca27aed9c769c507f70f02b8ce8586c135feb18e751db0de1 plugin-sdk-api-baseline.json
2-
4bc1c0dc66d910c80694fa1a6b7ba3ab488bf737b3566e53b8a5857c16d2e0b1 plugin-sdk-api-baseline.jsonl
1+
e7d03a0d5aed4f1afb5c7d5e235a166e1e248090632248eaa92b0016531e7f3b plugin-sdk-api-baseline.json
2+
b9bbf8e444b358485cb33c634d3f6f6588004a5c32482c1a473167957269ae58 plugin-sdk-api-baseline.jsonl

docs/concepts/context-engine.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ A plugin can register a context engine using the plugin API:
122122
import { buildMemorySystemPromptAddition } from "openclaw/plugin-sdk/core";
123123

124124
export default function register(api) {
125-
api.registerContextEngine("my-engine", () => ({
125+
api.registerContextEngine("my-engine", (ctx) => ({
126126
info: {
127127
id: "my-engine",
128128
name: "My Context Engine",
@@ -154,6 +154,10 @@ export default function register(api) {
154154
}
155155
```
156156

157+
The factory `ctx` includes optional `config`, `agentDir`, and `workspaceDir`
158+
values so plugins can initialize per-agent or per-workspace state before the
159+
first lifecycle hook runs.
160+
157161
Then enable it in config:
158162

159163
```json5

docs/plugins/architecture-internals.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -960,7 +960,7 @@ pipeline rather than just add memory search or hooks.
960960
import { buildMemorySystemPromptAddition } from "openclaw/plugin-sdk/core";
961961

962962
export default function (api) {
963-
api.registerContextEngine("lossless-claw", () => ({
963+
api.registerContextEngine("lossless-claw", (ctx) => ({
964964
info: { id: "lossless-claw", name: "Lossless Claw", ownsCompaction: true },
965965
async ingest() {
966966
return { ingested: true };
@@ -982,6 +982,9 @@ export default function (api) {
982982
}
983983
```
984984

985+
The factory `ctx` exposes optional `config`, `agentDir`, and `workspaceDir`
986+
values for construction-time initialization.
987+
985988
If your engine does **not** own the compaction algorithm, keep `compact()`
986989
implemented and delegate it explicitly:
987990

@@ -992,7 +995,7 @@ import {
992995
} from "openclaw/plugin-sdk/core";
993996

994997
export default function (api) {
995-
api.registerContextEngine("my-memory-engine", () => ({
998+
api.registerContextEngine("my-memory-engine", (ctx) => ({
996999
info: {
9971000
id: "my-memory-engine",
9981001
name: "My Memory Engine",

src/agents/pi-embedded-runner/compact.queued.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,12 @@ export async function compactEmbeddedPiSession(
5151
allowGatewaySubagentBinding: params.allowGatewaySubagentBinding,
5252
});
5353
ensureContextEnginesInitialized();
54-
const contextEngine = await resolveContextEngine(params.config);
5554
const agentDir = params.agentDir ?? resolveOpenClawAgentDir();
55+
const resolvedWorkspaceDir = resolveUserPath(params.workspaceDir);
56+
const contextEngine = await resolveContextEngine(params.config, {
57+
agentDir,
58+
workspaceDir: resolvedWorkspaceDir,
59+
});
5660
let contextTokenBudget = params.contextTokenBudget;
5761
if (!contextTokenBudget || !Number.isFinite(contextTokenBudget) || contextTokenBudget <= 0) {
5862
const resolvedCompactionTarget = resolveEmbeddedCompactionTarget({
@@ -129,7 +133,7 @@ export async function compactEmbeddedPiSession(
129133
sessionId: params.sessionId,
130134
agentId: sessionAgentId,
131135
sessionKey: hookSessionKey,
132-
workspaceDir: resolveUserPath(params.workspaceDir),
136+
workspaceDir: resolvedWorkspaceDir,
133137
messageProvider: resolvedMessageProvider,
134138
};
135139
const runtimeContext = contextEngineRuntimeContext;

src/agents/pi-embedded-runner/run.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -740,7 +740,10 @@ export async function runEmbeddedPiAgent(
740740
// Resolve the context engine once and reuse across retries to avoid
741741
// repeated initialization/connection overhead per attempt.
742742
ensureContextEnginesInitialized();
743-
const contextEngine = await resolveContextEngine(params.config);
743+
const contextEngine = await resolveContextEngine(params.config, {
744+
agentDir,
745+
workspaceDir: resolvedWorkspace,
746+
});
744747
try {
745748
let activeSessionId = params.sessionId;
746749
let activeSessionFile = params.sessionFile;

src/agents/subagent-registry-lifecycle.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export function createSubagentRegistryLifecycleController(params: {
7272
notifyContextEngineSubagentEnded(args: {
7373
childSessionKey: string;
7474
reason: "completed" | "deleted";
75+
agentDir?: string;
7576
workspaceDir?: string;
7677
}): Promise<void>;
7778
resumeSubagentRun(runId: string): void;
@@ -421,6 +422,7 @@ export function createSubagentRegistryLifecycleController(params: {
421422
void params.notifyContextEngineSubagentEnded({
422423
childSessionKey: cleanupParams.entry.childSessionKey,
423424
reason: "deleted",
425+
agentDir: cleanupParams.entry.agentDir,
424426
workspaceDir: cleanupParams.entry.workspaceDir,
425427
});
426428
params.runs.delete(cleanupParams.runId);
@@ -431,6 +433,7 @@ export function createSubagentRegistryLifecycleController(params: {
431433
void params.notifyContextEngineSubagentEnded({
432434
childSessionKey: cleanupParams.entry.childSessionKey,
433435
reason: "completed",
436+
agentDir: cleanupParams.entry.agentDir,
434437
workspaceDir: cleanupParams.entry.workspaceDir,
435438
});
436439
cleanupParams.entry.cleanupCompletedAt = cleanupParams.completedAt;

src/agents/subagent-registry-run-manager.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ export type RegisterSubagentRunParams = {
9191
cleanup: "delete" | "keep";
9292
label?: string;
9393
model?: string;
94+
agentDir?: string;
9495
workspaceDir?: string;
9596
runTimeoutSeconds?: number;
9697
expectsCompletionMessage?: boolean;
@@ -124,6 +125,7 @@ export function createSubagentRunManager(params: {
124125
notifyContextEngineSubagentEnded(args: {
125126
childSessionKey: string;
126127
reason: "completed" | "deleted" | "released";
128+
agentDir?: string;
127129
workspaceDir?: string;
128130
}): Promise<void>;
129131
completeCleanupBookkeeping(args: {
@@ -402,6 +404,7 @@ export function createSubagentRunManager(params: {
402404
spawnMode,
403405
label: registerParams.label,
404406
model: registerParams.model,
407+
agentDir: registerParams.agentDir,
405408
workspaceDir: registerParams.workspaceDir,
406409
runTimeoutSeconds,
407410
createdAt: now,
@@ -458,6 +461,7 @@ export function createSubagentRunManager(params: {
458461
void params.notifyContextEngineSubagentEnded({
459462
childSessionKey: entry.childSessionKey,
460463
reason: "released",
464+
agentDir: entry.agentDir,
461465
workspaceDir: entry.workspaceDir,
462466
});
463467
}

src/agents/subagent-registry.test.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -851,6 +851,7 @@ describe("subagent registry seam flow", () => {
851851
cleanup: "keep",
852852
expectsCompletionMessage: undefined,
853853
spawnMode: "run",
854+
agentDir: "/tmp/agent-alt",
854855
workspaceDir: "/tmp/workspace",
855856
createdAt: 1,
856857
startedAt: 1,
@@ -863,6 +864,7 @@ describe("subagent registry seam flow", () => {
863864

864865
await waitForFast(() => {
865866
expect(mocks.onSubagentEnded).toHaveBeenCalledWith({
867+
agentDir: "/tmp/agent-alt",
866868
childSessionKey: "agent:main:session:child",
867869
reason: "released",
868870
workspaceDir: "/tmp/workspace",
@@ -877,5 +879,82 @@ describe("subagent registry seam flow", () => {
877879
allowGatewaySubagentBinding: true,
878880
});
879881
expect(mocks.ensureContextEnginesInitialized).toHaveBeenCalledTimes(1);
882+
expect(mocks.resolveContextEngine).toHaveBeenCalledWith(
883+
{
884+
agents: { defaults: { subagents: { archiveAfterMinutes: 0 } } },
885+
session: { mainKey: "main", scope: "per-sender" },
886+
},
887+
{
888+
agentDir: "/tmp/agent-alt",
889+
workspaceDir: "/tmp/workspace",
890+
},
891+
);
892+
});
893+
894+
it("passes stored agentDir through swept context-engine cleanup paths", async () => {
895+
const now = Date.parse("2026-03-24T12:00:00Z");
896+
mod.addSubagentRunForTests({
897+
runId: "run-session-swept-context-engine",
898+
childSessionKey: "agent:alt:session:child-session",
899+
controllerSessionKey: "agent:main:session:parent",
900+
requesterSessionKey: "agent:main:session:parent",
901+
requesterOrigin: undefined,
902+
requesterDisplayKey: "parent",
903+
task: "session cleanup",
904+
cleanup: "keep",
905+
expectsCompletionMessage: undefined,
906+
spawnMode: "session",
907+
agentDir: "/tmp/agent-session",
908+
workspaceDir: "/tmp/workspace-session",
909+
createdAt: now - 20_000,
910+
startedAt: now - 10_000,
911+
sessionStartedAt: now - 10_000,
912+
accumulatedRuntimeMs: 0,
913+
endedAt: now - 8_000,
914+
outcome: { status: "ok", startedAt: now - 10_000, endedAt: now - 8_000, elapsedMs: 2_000 },
915+
cleanupHandled: true,
916+
cleanupCompletedAt: now - 6 * 60_000,
917+
});
918+
mod.addSubagentRunForTests({
919+
runId: "run-archive-swept-context-engine",
920+
childSessionKey: "agent:alt:session:child-archive",
921+
controllerSessionKey: "agent:main:session:parent",
922+
requesterSessionKey: "agent:main:session:parent",
923+
requesterOrigin: undefined,
924+
requesterDisplayKey: "parent",
925+
task: "archive cleanup",
926+
cleanup: "delete",
927+
expectsCompletionMessage: undefined,
928+
spawnMode: "run",
929+
agentDir: "/tmp/agent-archive",
930+
workspaceDir: "/tmp/workspace-archive",
931+
createdAt: now - 20_000,
932+
startedAt: now - 10_000,
933+
sessionStartedAt: now - 10_000,
934+
accumulatedRuntimeMs: 0,
935+
endedAt: now - 8_000,
936+
outcome: { status: "ok", startedAt: now - 10_000, endedAt: now - 8_000, elapsedMs: 2_000 },
937+
archiveAtMs: now - 1,
938+
cleanupHandled: true,
939+
});
940+
941+
await mod.__testing.sweepOnceForTests();
942+
943+
await waitForFast(() => {
944+
expect(mocks.resolveContextEngine).toHaveBeenCalledWith(
945+
expect.any(Object),
946+
expect.objectContaining({
947+
agentDir: "/tmp/agent-session",
948+
workspaceDir: "/tmp/workspace-session",
949+
}),
950+
);
951+
expect(mocks.resolveContextEngine).toHaveBeenCalledWith(
952+
expect.any(Object),
953+
expect.objectContaining({
954+
agentDir: "/tmp/agent-archive",
955+
workspaceDir: "/tmp/workspace-archive",
956+
}),
957+
);
958+
});
880959
});
881960
});

src/agents/subagent-registry.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
type SessionEntry,
88
} from "../config/sessions.js";
99
import type { OpenClawConfig } from "../config/types.openclaw.js";
10+
import type { ResolveContextEngineOptions } from "../context-engine/registry.js";
1011
import type { ContextEngine, SubagentEndReason } from "../context-engine/types.js";
1112
import { callGateway } from "../gateway/call.js";
1213
import { getAgentRunContext, onAgentEvent } from "../infra/agent-events.js";
@@ -97,7 +98,10 @@ type SubagentRegistryDeps = {
9798
runSubagentAnnounceFlow: SubagentAnnounceModule["runSubagentAnnounceFlow"];
9899
ensureContextEnginesInitialized?: () => void;
99100
ensureRuntimePluginsLoaded?: typeof ensureRuntimePluginsLoadedFn;
100-
resolveContextEngine?: (cfg: OpenClawConfig) => Promise<ContextEngine>;
101+
resolveContextEngine?: (
102+
cfg?: OpenClawConfig,
103+
options?: ResolveContextEngineOptions,
104+
) => Promise<ContextEngine>;
101105
};
102106

103107
let subagentAnnouncePromise: Promise<SubagentAnnounceModule> | null = null;
@@ -140,7 +144,10 @@ type ContextEngineInitModule = Pick<
140144
>;
141145
type ContextEngineRegistryModule = Pick<
142146
{
143-
resolveContextEngine: (cfg: OpenClawConfig) => Promise<ContextEngine>;
147+
resolveContextEngine: (
148+
cfg?: OpenClawConfig,
149+
options?: ResolveContextEngineOptions,
150+
) => Promise<ContextEngine>;
144151
},
145152
"resolveContextEngine"
146153
>;
@@ -308,7 +315,10 @@ async function ensureSubagentRegistryPluginRuntimeLoaded(params: {
308315
(await loadRuntimePluginsModule()).ensureRuntimePluginsLoaded(params);
309316
}
310317

311-
async function resolveSubagentRegistryContextEngine(cfg: OpenClawConfig) {
318+
async function resolveSubagentRegistryContextEngine(
319+
cfg: OpenClawConfig,
320+
options?: ResolveContextEngineOptions,
321+
) {
312322
const initModule = await loadContextEngineInitModule();
313323
const registryModule = await loadContextEngineRegistryModule();
314324
const ensureContextEnginesInitialized =
@@ -317,7 +327,7 @@ async function resolveSubagentRegistryContextEngine(cfg: OpenClawConfig) {
317327
const resolveContextEngine =
318328
subagentRegistryDeps.resolveContextEngine ?? registryModule.resolveContextEngine;
319329
ensureContextEnginesInitialized();
320-
return await resolveContextEngine(cfg);
330+
return await resolveContextEngine(cfg, options);
321331
}
322332

323333
function persistSubagentRuns() {
@@ -469,6 +479,7 @@ function schedulePendingLifecycleTimeout(params: { runId: string; endedAt: numbe
469479
async function notifyContextEngineSubagentEnded(params: {
470480
childSessionKey: string;
471481
reason: SubagentEndReason;
482+
agentDir?: string;
472483
workspaceDir?: string;
473484
}) {
474485
try {
@@ -478,7 +489,10 @@ async function notifyContextEngineSubagentEnded(params: {
478489
workspaceDir: params.workspaceDir,
479490
allowGatewaySubagentBinding: true,
480491
});
481-
const engine = await resolveSubagentRegistryContextEngine(cfg);
492+
const engine = await resolveSubagentRegistryContextEngine(cfg, {
493+
agentDir: params.agentDir,
494+
workspaceDir: params.workspaceDir,
495+
});
482496
if (!engine.onSubagentEnded) {
483497
return;
484498
}
@@ -812,6 +826,7 @@ async function sweepSubagentRuns() {
812826
void notifyContextEngineSubagentEnded({
813827
childSessionKey: entry.childSessionKey,
814828
reason: "swept",
829+
agentDir: entry.agentDir,
815830
workspaceDir: entry.workspaceDir,
816831
});
817832
subagentRuns.delete(runId);
@@ -851,6 +866,7 @@ async function sweepSubagentRuns() {
851866
void notifyContextEngineSubagentEnded({
852867
childSessionKey: entry.childSessionKey,
853868
reason: "swept",
869+
agentDir: entry.agentDir,
854870
workspaceDir: entry.workspaceDir,
855871
});
856872
}

0 commit comments

Comments
 (0)