From 33b23214d9455eda766b09dddff1335cdfb52d02 Mon Sep 17 00:00:00 2001 From: Andy Ye <35905412+TurboTheTurtle@users.noreply.github.com> Date: Mon, 22 Jun 2026 21:21:57 -0700 Subject: [PATCH] Fix memory-wiki bridge self-import loop (#95666) Merged via squash. Prepared head SHA: 0f7462954775c3457e519297a79bd235c7b4c1ab Co-authored-by: TurboTheTurtle <35905412+TurboTheTurtle@users.noreply.github.com> Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com> Reviewed-by: @vincentkoc --- extensions/memory-wiki/src/bridge.test.ts | 72 +++++++++++++++++++++++ extensions/memory-wiki/src/bridge.ts | 19 +++++- 2 files changed, 90 insertions(+), 1 deletion(-) diff --git a/extensions/memory-wiki/src/bridge.test.ts b/extensions/memory-wiki/src/bridge.test.ts index 1741de29e1b5..7e055fb9d6ac 100644 --- a/extensions/memory-wiki/src/bridge.test.ts +++ b/extensions/memory-wiki/src/bridge.test.ts @@ -150,6 +150,78 @@ describe("syncMemoryWikiBridgeSources", () => { expect(logLines).toHaveLength(2); }); + it("skips generated artifacts from its own vault", async () => { + const workspaceDir = await createBridgeWorkspace("self-import-workspace"); + const vaultDir = path.join(workspaceDir, "memory", "wiki"); + const { config } = await createVault({ + rootDir: vaultDir, + config: { + vaultMode: "bridge", + bridge: { + enabled: true, + readMemoryArtifacts: true, + indexDailyNotes: true, + }, + }, + }); + + const dailyNotePath = path.join(workspaceDir, "memory", "2026-06-22.md"); + const generatedSourcePath = path.join( + vaultDir, + "sources", + "bridge-workspace-remote-memory-daily-old.md", + ); + const generatedIndexPath = path.join(vaultDir, "index.md"); + await fs.mkdir(path.dirname(dailyNotePath), { recursive: true }); + await fs.mkdir(path.dirname(generatedSourcePath), { recursive: true }); + await fs.writeFile(dailyNotePath, "# Daily Note\n", "utf8"); + await fs.writeFile(generatedSourcePath, "# Previously Imported Source\n", "utf8"); + await fs.writeFile(generatedIndexPath, "# Generated Index\n", "utf8"); + + registerBridgeArtifacts([ + { + kind: "daily-note", + workspaceDir, + relativePath: "memory/2026-06-22.md", + absolutePath: dailyNotePath, + agentIds: ["main"], + contentType: "markdown", + }, + { + kind: "daily-note", + workspaceDir, + relativePath: "memory/wiki/sources/bridge-workspace-remote-memory-daily-old.md", + absolutePath: generatedSourcePath, + agentIds: ["main"], + contentType: "markdown", + }, + { + kind: "daily-note", + workspaceDir, + relativePath: "memory/wiki/index.md", + absolutePath: generatedIndexPath, + agentIds: ["main"], + contentType: "markdown", + }, + ]); + + const appConfig: OpenClawConfig = { + agents: { + list: [{ id: "main", default: true, workspace: workspaceDir }], + }, + }; + + const result = await syncMemoryWikiBridgeSources({ config, appConfig }); + + expect(result.artifactCount).toBe(1); + expect(result.importedCount).toBe(1); + expect(result.pagePaths).toHaveLength(1); + expect(result.pagePaths[0]).not.toContain("memory-wiki-sources"); + const sourcePages = await fs.readdir(path.join(vaultDir, "sources")); + expect(sourcePages.filter((name) => name.startsWith("bridge-"))).toHaveLength(2); + expect(sourcePages.filter((name) => name.includes("memory-wiki-sources-"))).toEqual([]); + }); + it("imports bridge artifacts from legacy providers without agent ids", async () => { const workspaceDir = await createBridgeWorkspace("legacy-agentids-workspace"); const { rootDir: vaultDir, config } = await createVault({ diff --git a/extensions/memory-wiki/src/bridge.ts b/extensions/memory-wiki/src/bridge.ts index 96e1488582e0..0c8e0a45a78e 100644 --- a/extensions/memory-wiki/src/bridge.ts +++ b/extensions/memory-wiki/src/bridge.ts @@ -64,14 +64,19 @@ function shouldImportArtifact( async function collectBridgeArtifacts( bridgeConfig: ResolvedMemoryWikiConfig["bridge"], + vaultRoot: string, artifacts: MemoryPluginPublicArtifact[], ): Promise { const collected: BridgeArtifact[] = []; + const vaultRootKey = await resolveArtifactKey(vaultRoot); for (const artifact of artifacts) { if (!shouldImportArtifact(artifact, bridgeConfig)) { continue; } const syncKey = await resolveArtifactKey(artifact.absolutePath); + if (isPathInsideOrEqual(vaultRootKey, syncKey)) { + continue; + } collected.push({ syncKey, artifactType: artifact.kind === "event-log" ? "memory-events" : "markdown", @@ -87,6 +92,14 @@ async function collectBridgeArtifacts( return [...deduped.values()]; } +function isPathInsideOrEqual(parentPath: string, candidatePath: string): boolean { + const relative = path.relative(parentPath, candidatePath); + return ( + relative === "" || + (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative)) + ); +} + function resolveBridgeTitle(artifact: BridgeArtifact, agentIds: string[]): string { if (artifact.artifactType === "memory-events") { if (agentIds.length === 0) { @@ -227,7 +240,11 @@ export async function syncMemoryWikiBridgeSources(params: { const publicArtifacts = await listActiveMemoryPublicArtifacts({ cfg: params.appConfig }); const results: Array<{ pagePath: string; changed: boolean; created: boolean }> = []; const activeKeys = new Set(); - const artifacts = await collectBridgeArtifacts(params.config.bridge, publicArtifacts); + const artifacts = await collectBridgeArtifacts( + params.config.bridge, + params.config.vault.path, + publicArtifacts, + ); const state = await readMemoryWikiSourceSyncState(params.config.vault.path); assertMemoryWikiSourceSyncStateCapacity({ state,