Skip to content

Commit 705b50b

Browse files
committed
fix(memory-wiki): route bridge CLI through gateway
1 parent 5915489 commit 705b50b

5 files changed

Lines changed: 370 additions & 15 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,7 @@ Docs: https://docs.openclaw.ai
443443
- TTS/SecretRef: resolve `messages.tts.providers.*.apiKey` from the active runtime snapshot so SecretRef-backed MiniMax and other TTS provider keys work in runtime reply/audio paths. Fixes #68690. Thanks @joshavant.
444444
- Gateway/install: surface systemd user-bus recovery hints during Linux service activation and retry via the target user scope when `systemctl --user` reports no-medium bus failures, without letting stale `SUDO_USER` override `sudo -u` installs. Fixes #39673; refs #44417 and #63561. Thanks @Arbor4, @myrsu, @mssteuer, and @boyuaner.
445445
- CLI/nodes: make unfiltered `openclaw nodes list` prefer the effective paired-node view used by `nodes status` while preserving pending rows, pairing-scope fallback, terminal-safe table rendering, and paired JSON metadata. Fixes #46871; carries forward #65772 through the ProjectClownfish #72619 repair. Thanks @skainguyen1412.
446+
- Memory Wiki/CLI: route active bridge-mode status, doctor, and bridge imports through Gateway RPC so CLI checks use the runtime memory plugin context while disabled bridge imports stay local/offline. Carries forward #67208 and #71479; related #70185. Thanks @moorsecopers99, @vincentkoc, and @prasad-yashdeep.
446447
- CLI/startup: read generated startup metadata from the bundled `dist` layout before falling back to live help rendering, so root/browser help and channel-option bootstrap stay on the fast path. Thanks @vincentkoc.
447448
- Feishu/Lark: stop treating broadcast-only `@all`/`@_all` messages as bot mentions while preserving direct bot mentions, including messages that also include `@all`. Fixes #37706. Thanks @JosepLee.
448449
- CLI/help: treat positional `help` invocations like `openclaw channels help` as help paths for startup gating, avoiding model/auth warmup while preserving positional arguments such as `openclaw docs help`. Thanks @gumadeiras.

docs/cli/wiki.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,18 @@ Inspect current vault mode, health, and Obsidian CLI availability.
6868
Use this first when you are unsure whether the vault is initialized, bridge mode
6969
is healthy, or Obsidian integration is available.
7070

71+
When bridge mode is active and configured to read memory artifacts, this command
72+
queries the running Gateway so it sees the same active memory plugin context as
73+
agent/runtime memory.
74+
7175
### `wiki doctor`
7276

7377
Run wiki health checks and surface configuration or vault problems.
7478

79+
When bridge mode is active and configured to read memory artifacts, this command
80+
queries the running Gateway before building the report. Disabled bridge imports
81+
and bridge configs that do not read memory artifacts remain local/offline.
82+
7583
Typical issues include:
7684

7785
- bridge mode enabled without public memory artifacts
@@ -168,6 +176,11 @@ source pages.
168176
Use this in `bridge` mode when you want the latest exported memory artifacts
169177
pulled into the wiki vault.
170178

179+
For active bridge artifact reads, the CLI routes the import through Gateway RPC
180+
so the import uses the runtime memory plugin context. If bridge imports are
181+
disabled or artifact reads are turned off, the command keeps the local/offline
182+
zero-import behavior.
183+
171184
### `wiki unsafe-local import`
172185

173186
Import from explicitly configured local paths in `unsafe-local` mode.

docs/plugins/memory-wiki.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,12 @@ If bridge mode reports zero exported artifacts, the active memory plugin is not
6565
currently exposing public bridge inputs yet. Run `openclaw wiki doctor` first,
6666
then confirm the active memory plugin supports public artifacts.
6767

68+
When bridge mode is active and `bridge.readMemoryArtifacts` is enabled,
69+
`openclaw wiki status`, `openclaw wiki doctor`, and `openclaw wiki bridge
70+
import` read through the running Gateway. That keeps CLI bridge checks aligned
71+
with the runtime memory plugin context. If bridge is disabled or artifact reads
72+
are turned off, those commands keep their local/offline behavior.
73+
6874
## Vault modes
6975

7076
`memory-wiki` supports three vault modes:

extensions/memory-wiki/src/cli.test.ts

Lines changed: 234 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,25 @@ import os from "node:os";
33
import path from "node:path";
44
import { Command } from "commander";
55
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
6-
import { registerWikiCli, runWikiChatGptImport, runWikiChatGptRollback } from "./cli.js";
6+
import {
7+
registerWikiCli,
8+
runWikiBridgeImport,
9+
runWikiChatGptImport,
10+
runWikiChatGptRollback,
11+
runWikiDoctor,
12+
runWikiStatus,
13+
} from "./cli.js";
714
import type { MemoryWikiPluginConfig } from "./config.js";
815
import { parseWikiMarkdown, renderWikiMarkdown } from "./markdown.js";
16+
import type { MemoryWikiDoctorReport, MemoryWikiStatus } from "./status.js";
917
import { createMemoryWikiTestHarness } from "./test-helpers.js";
1018

19+
const callGatewayFromCliMock = vi.hoisted(() => vi.fn());
20+
21+
vi.mock("openclaw/plugin-sdk/gateway-runtime", () => ({
22+
callGatewayFromCli: callGatewayFromCliMock,
23+
}));
24+
1125
const { createVault } = createMemoryWikiTestHarness();
1226
let suiteRoot = "";
1327
let caseIndex = 0;
@@ -24,6 +38,7 @@ describe("memory-wiki cli", () => {
2438
});
2539

2640
beforeEach(() => {
41+
callGatewayFromCliMock.mockReset();
2742
vi.spyOn(process.stdout, "write").mockImplementation(
2843
(() => true) as typeof process.stdout.write,
2944
);
@@ -88,6 +103,45 @@ describe("memory-wiki cli", () => {
88103
return exportDir;
89104
}
90105

106+
function createGatewayStatus(config: {
107+
vault: { path: string };
108+
bridge: MemoryWikiStatus["bridge"];
109+
}): MemoryWikiStatus {
110+
return {
111+
vaultMode: "bridge",
112+
renderMode: "native",
113+
vaultPath: config.vault.path,
114+
vaultExists: true,
115+
bridge: config.bridge,
116+
bridgePublicArtifactCount: 2,
117+
obsidianCli: {
118+
enabled: false,
119+
requested: false,
120+
available: false,
121+
command: null,
122+
},
123+
unsafeLocal: {
124+
allowPrivateMemoryCoreAccess: false,
125+
pathCount: 0,
126+
},
127+
pageCounts: {
128+
source: 0,
129+
entity: 0,
130+
concept: 0,
131+
synthesis: 0,
132+
report: 0,
133+
},
134+
sourceCounts: {
135+
native: 0,
136+
bridge: 0,
137+
bridgeEvents: 0,
138+
unsafeLocal: 0,
139+
other: 0,
140+
},
141+
warnings: [],
142+
};
143+
}
144+
91145
it("registers apply synthesis and writes a synthesis page", async () => {
92146
const { rootDir, config } = await createCliVault();
93147
const program = new Command();
@@ -193,6 +247,185 @@ cli note
193247
await program.parseAsync(["wiki", "doctor", "--json"], { from: "user" });
194248

195249
expect(process.exitCode).toBe(1);
250+
expect(callGatewayFromCliMock).not.toHaveBeenCalled();
251+
});
252+
253+
it("routes active bridge status and doctor through the gateway", async () => {
254+
const { config } = await createCliVault({
255+
config: {
256+
vaultMode: "bridge",
257+
bridge: { enabled: true, readMemoryArtifacts: true },
258+
},
259+
initialize: true,
260+
});
261+
const status = createGatewayStatus(config);
262+
const report: MemoryWikiDoctorReport = {
263+
healthy: false,
264+
warningCount: 1,
265+
status: {
266+
...status,
267+
warnings: [
268+
{
269+
code: "bridge-artifacts-missing",
270+
message: "No exported artifacts.",
271+
},
272+
],
273+
},
274+
fixes: [
275+
{
276+
code: "bridge-artifacts-missing",
277+
message: "Create memory artifacts.",
278+
},
279+
],
280+
};
281+
callGatewayFromCliMock.mockResolvedValueOnce(status).mockResolvedValueOnce(report);
282+
283+
await expect(runWikiStatus({ config, json: true })).resolves.toBe(status);
284+
await expect(runWikiDoctor({ config, json: true })).resolves.toBe(report);
285+
286+
expect(process.exitCode).toBe(1);
287+
expect(callGatewayFromCliMock).toHaveBeenNthCalledWith(
288+
1,
289+
"wiki.status",
290+
{ timeout: "30000" },
291+
undefined,
292+
{ progress: false },
293+
);
294+
expect(callGatewayFromCliMock).toHaveBeenNthCalledWith(
295+
2,
296+
"wiki.doctor",
297+
{ timeout: "30000" },
298+
undefined,
299+
{ progress: false },
300+
);
301+
});
302+
303+
it("sanitizes gateway status text output without changing JSON output", async () => {
304+
const { config } = await createCliVault({
305+
config: {
306+
vaultMode: "bridge",
307+
bridge: { enabled: true, readMemoryArtifacts: true },
308+
},
309+
initialize: true,
310+
});
311+
const unsafeStatus = createGatewayStatus({
312+
...config,
313+
vault: { path: "\u001B[2J/tmp/wiki\nforged prompt" },
314+
});
315+
unsafeStatus.warnings = [
316+
{
317+
code: "bridge-artifacts-missing",
318+
message: "missing artifacts\r\nfake success\u001B[31m",
319+
},
320+
];
321+
const textOutput: string[] = [];
322+
callGatewayFromCliMock.mockResolvedValueOnce(unsafeStatus);
323+
324+
await runWikiStatus({
325+
config,
326+
stdout: {
327+
write: ((chunk: string) => textOutput.push(chunk) > 0) as NodeJS.WriteStream["write"],
328+
},
329+
});
330+
331+
const renderedText = textOutput.join("");
332+
expect(renderedText).not.toContain("\u001B");
333+
expect(renderedText).toContain("(/tmp/wiki forged prompt)");
334+
expect(renderedText).toContain("- missing artifacts fake success");
335+
336+
const jsonOutput: string[] = [];
337+
callGatewayFromCliMock.mockResolvedValueOnce(unsafeStatus);
338+
339+
await runWikiStatus({
340+
config,
341+
json: true,
342+
stdout: {
343+
write: ((chunk: string) => jsonOutput.push(chunk) > 0) as NodeJS.WriteStream["write"],
344+
},
345+
});
346+
347+
const renderedJson = jsonOutput.join("");
348+
expect(renderedJson).not.toContain("\u001B");
349+
expect(renderedJson).not.toContain("\r");
350+
expect(renderedJson).toContain("\\u001b[2J/tmp/wiki\\nforged prompt");
351+
expect(renderedJson).toContain("missing artifacts\\r\\nfake success\\u001b[31m");
352+
353+
const parsed = JSON.parse(renderedJson) as MemoryWikiStatus;
354+
expect(parsed.vaultPath).toBe("\u001B[2J/tmp/wiki\nforged prompt");
355+
expect(parsed.warnings[0]?.message).toBe("missing artifacts\r\nfake success\u001B[31m");
356+
});
357+
358+
it("limits gateway status arrays before terminal rendering", async () => {
359+
const { config } = await createCliVault({
360+
config: {
361+
vaultMode: "bridge",
362+
bridge: { enabled: true, readMemoryArtifacts: true },
363+
},
364+
initialize: true,
365+
});
366+
const status = createGatewayStatus(config);
367+
status.warnings = Array.from({ length: 250 }, (_, index) => ({
368+
code: "bridge-artifacts-missing",
369+
message: `warning ${index}`,
370+
}));
371+
const textOutput: string[] = [];
372+
callGatewayFromCliMock.mockResolvedValueOnce(status);
373+
374+
await runWikiStatus({
375+
config,
376+
stdout: {
377+
write: ((chunk: string) => textOutput.push(chunk) > 0) as NodeJS.WriteStream["write"],
378+
},
379+
});
380+
381+
const renderedText = textOutput.join("");
382+
expect(renderedText).toContain("warning 199");
383+
expect(renderedText).not.toContain("warning 200");
384+
});
385+
386+
it("routes active bridge imports through the gateway and keeps disabled bridge imports local", async () => {
387+
const active = await createCliVault({
388+
config: {
389+
vaultMode: "bridge",
390+
bridge: { enabled: true, readMemoryArtifacts: true },
391+
},
392+
initialize: true,
393+
});
394+
callGatewayFromCliMock.mockResolvedValueOnce({
395+
importedCount: 1,
396+
updatedCount: 0,
397+
skippedCount: 0,
398+
removedCount: 0,
399+
artifactCount: 1,
400+
workspaces: 1,
401+
pagePaths: ["sources/bridge-alpha.md"],
402+
indexesRefreshed: true,
403+
indexUpdatedFiles: ["index.md"],
404+
indexRefreshReason: "import-changed",
405+
});
406+
407+
const activeResult = await runWikiBridgeImport({ config: active.config, json: true });
408+
409+
expect(activeResult.importedCount).toBe(1);
410+
expect(callGatewayFromCliMock).toHaveBeenCalledWith(
411+
"wiki.bridge.import",
412+
{ timeout: "30000" },
413+
undefined,
414+
{ progress: false },
415+
);
416+
417+
callGatewayFromCliMock.mockClear();
418+
const disabled = await createCliVault({
419+
config: {
420+
vaultMode: "bridge",
421+
bridge: { enabled: false },
422+
},
423+
});
424+
425+
const disabledResult = await runWikiBridgeImport({ config: disabled.config, json: true });
426+
427+
expect(disabledResult.artifactCount).toBe(0);
428+
expect(callGatewayFromCliMock).not.toHaveBeenCalled();
196429
});
197430

198431
it("imports ChatGPT exports with dry-run, apply, and rollback", async () => {

0 commit comments

Comments
 (0)