Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions examples/openclaw-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ This matters because the plugin is built to support multi-agent and multi-sessio

![Automatic recall flow before prompt build](./images/openclaw-plugin-recall-flow.png)

Today the main recall path still lives in `before_prompt_build`:
Today there are two recall-related paths: legacy hook injection in `before_prompt_build`, and context reconstruction in `assemble()`. They should not both inject prompt context for the same run:

1. Extract the latest user text from `messages` or `prompt`.
2. Resolve the agent routing for the current `sessionId/sessionKey`.
Expand Down Expand Up @@ -182,7 +182,7 @@ The repo also contains a more future-looking design draft at `docs/design/opencl

- this README describes current implemented behavior
- the older draft discusses a stronger future move into context-engine-owned lifecycle control
- in the current version, the main automatic recall path still lives in `before_prompt_build`, not fully in `assemble()`
- in the current version, hook-based auto-recall still exists in `before_prompt_build`, but when the context-engine path is active it should not run alongside `assemble()`
- in the current version, `afterTurn()` already appends to the OpenViking session, but commit remains threshold-triggered and asynchronous on that path
- in the current version, `compact()` already uses `commit(wait=true)`, but it is still focused on synchronous commit plus readback rather than owning every orchestration concern

Expand Down
6 changes: 6 additions & 0 deletions examples/openclaw-plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -903,6 +903,12 @@ const contextEnginePlugin = {
);
return;
}
if (contextEngineRef) {
verboseRoutingInfo(
`openviking: skipping before_prompt_build auto-recall because context-engine is active (sessionKey=${ctx?.sessionKey ?? "none"}, sessionId=${ctx?.sessionId ?? "none"})`,
);
return;
}
const agentId = resolveAgentId(ctx?.sessionId, ctx?.sessionKey);
let client: OpenVikingClient;
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,56 @@ describe("plugin bypass session patterns", () => {
);
});

it("skips before_prompt_build auto-recall once context-engine is active", async () => {
const { handlers, logger, registerContextEngine } = setupPlugin();

const factory = registerContextEngine.mock.calls[0]?.[1] as (() => unknown) | undefined;
expect(factory).toBeTruthy();
factory!();

const hook = handlers.get("before_prompt_build");
expect(hook).toBeTruthy();

const result = await hook!(
{
messages: [{ role: "user", content: "remember the launch checklist" }],
prompt: "remember the launch checklist",
},
{
sessionId: "runtime-session",
sessionKey: "agent:main:test:1",
},
);

expect(result).toBeUndefined();
expect(logger.warn).not.toHaveBeenCalledWith(
expect.stringContaining("failed to get client"),
);
});

it("does not try to commit on before_reset when the context-engine was never instantiated", async () => {
const { handlers, logger, registerContextEngine } = setupPlugin();

expect(registerContextEngine).toHaveBeenCalledTimes(1);

const hook = handlers.get("before_reset");
expect(hook).toBeTruthy();

await expect(
hook!(
{},
{
sessionId: "runtime-session",
sessionKey: "agent:main:test:1",
},
),
).resolves.toBeUndefined();

expect(logger.warn).not.toHaveBeenCalledWith(
expect.stringContaining("failed to commit OV session on reset"),
);
});

it("bypasses before_reset without calling commitOVSession", async () => {
const { handlers, registerContextEngine } = setupPlugin({
bypassSessionPatterns: ["agent:*:cron:**"],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,82 @@ describe("plugin normal flow with healthy backend", () => {
await once(server, "close");
});

it("skips hook recall after the context-engine is instantiated but still assembles context", async () => {
const handlers = new Map<string, (event: unknown, ctx?: unknown) => unknown>();
let service:
| {
start: () => Promise<void>;
stop?: () => Promise<void> | void;
}
| null = null;
let contextEngineFactory: (() => unknown) | null = null;

plugin.register({
logger: {
debug: () => {},
error: () => {},
info: () => {},
warn: () => {},
},
on: (name, handler) => {
handlers.set(name, handler);
},
pluginConfig: {
autoCapture: true,
autoRecall: true,
baseUrl,
commitTokenThreshold: 20000,
ingestReplyAssist: false,
mode: "remote",
},
registerContextEngine: (_id, factory) => {
contextEngineFactory = factory as () => unknown;
},
registerService: (entry) => {
service = entry;
},
registerTool: () => {},
});

expect(service).toBeTruthy();
expect(contextEngineFactory).toBeTruthy();

await service!.start();

const contextEngine = contextEngineFactory!() as {
assemble: (params: {
sessionId: string;
messages: Array<{ role: string; content: string }>;
}) => Promise<{ messages: Array<{ role: string; content: unknown }> }>;
};

const beforePromptBuild = handlers.get("before_prompt_build");
expect(beforePromptBuild).toBeTruthy();
const hookResult = await beforePromptBuild!(
{ messages: [{ role: "user", content: "what backend language should we use?" }] },
{ agentId: "main", sessionId: "session-normal", sessionKey: "agent:main:normal" },
);

expect(hookResult).toBeUndefined();

const assembled = await contextEngine.assemble({
sessionId: "session-normal",
messages: [{ role: "user", content: "fallback" }],
});

expect(assembled.messages[0]).toEqual({
role: "user",
content: "[Session History Summary]
Earlier work focused on backend stack choices.",
});
expect(assembled.messages[1]).toEqual({
role: "assistant",
content: [{ type: "text", text: "Stored answer from OpenViking." }],
});

await service?.stop?.();
});

it("keeps normal prompt-build and context-engine flow working", async () => {
const handlers = new Map<string, (event: unknown, ctx?: unknown) => unknown>();
let service:
Expand Down
10 changes: 10 additions & 0 deletions examples/openclaw-plugin/tests/ut/tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,16 @@ describe("Plugin registration", () => {
);
});

it("registers the context engine exactly once", () => {
const { api } = setupPlugin();
contextEnginePlugin.register(api as any);
expect(api.registerContextEngine).toHaveBeenCalledTimes(1);
expect(api.registerContextEngine).toHaveBeenCalledWith(
"openviking",
expect.any(Function),
);
});

it("registers context engine when api.registerContextEngine is available", () => {
const { api } = setupPlugin();
contextEnginePlugin.register(api as any);
Expand Down
Loading