diff --git a/docs/refactor/database-first.md b/docs/refactor/database-first.md index 4144b26be3b..a7c2c5e6689 100644 --- a/docs/refactor/database-first.md +++ b/docs/refactor/database-first.md @@ -304,6 +304,9 @@ The remaining cleanup is mostly consolidation and deletion: `.openclaw-wiki/log.jsonl`. The Memory Wiki migration provider imports old JSONL logs; wiki markdown and user vault content stay file-backed as workspace content. +- Memory Wiki no longer creates `.openclaw-wiki/state.json` or the unused + `.openclaw-wiki/locks` directory. The migration provider removes those retired + plugin metadata files if an older vault still has them. - Crestodian audit entries now use core SQLite plugin state instead of `audit/crestodian.jsonl`. Doctor imports the legacy JSONL audit log and removes it after successful import. @@ -1082,6 +1085,8 @@ Add a repo check that fails new runtime writes to legacy state paths: - `crestodian/rescue-pending/*.json` - `plugins/phone-control/armed.json` - Memory Wiki `.openclaw-wiki/log.jsonl` +- Memory Wiki `.openclaw-wiki/state.json` +- Memory Wiki `.openclaw-wiki/locks/` - Memory Wiki `.openclaw-wiki/source-sync.json` - Memory Wiki `.openclaw-wiki/import-runs/*.json` - Memory Wiki `.openclaw-wiki/cache/agent-digest.json` diff --git a/extensions/memory-wiki/src/source-sync-migration.test.ts b/extensions/memory-wiki/src/source-sync-migration.test.ts new file mode 100644 index 00000000000..7eb3f46a9ca --- /dev/null +++ b/extensions/memory-wiki/src/source-sync-migration.test.ts @@ -0,0 +1,72 @@ +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import type { MigrationProviderContext } from "openclaw/plugin-sdk/migration"; +import { afterEach, describe, expect, it } from "vitest"; +import type { ResolvedMemoryWikiConfig } from "./config.js"; +import { createMemoryWikiSourceSyncMigrationProvider } from "./source-sync-migration.js"; + +const tempDirs: string[] = []; + +async function createVaultRoot(): Promise { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "memory-wiki-migration-")); + tempDirs.push(root); + return root; +} + +function createConfig(vaultRoot: string): ResolvedMemoryWikiConfig { + return { + vaultMode: "isolated", + vault: { path: vaultRoot, renderMode: "native" }, + obsidian: { enabled: false, useOfficialCli: false, openAfterWrites: false }, + bridge: { + enabled: false, + readMemoryArtifacts: false, + indexDreamReports: false, + indexDailyNotes: false, + indexMemoryRoot: false, + followMemoryEvents: false, + }, + unsafeLocal: { allowPrivateMemoryCoreAccess: false, paths: [] }, + ingest: { autoCompile: false, maxConcurrentJobs: 1, allowUrlIngest: false }, + search: { backend: "shared", corpus: "wiki" }, + context: { includeCompiledDigestPrompt: false }, + render: { preserveHumanBlocks: true, createBacklinks: true, createDashboards: true }, + }; +} + +afterEach(async () => { + await Promise.all(tempDirs.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true }))); +}); + +describe("memory wiki source sync migration", () => { + it("removes retired vault metadata files during doctor migration", async () => { + const vaultRoot = await createVaultRoot(); + const metadataDir = path.join(vaultRoot, ".openclaw-wiki"); + const locksDir = path.join(metadataDir, "locks"); + await fs.mkdir(locksDir, { recursive: true }); + await fs.writeFile(path.join(metadataDir, "state.json"), '{"version":1}\n', "utf8"); + await fs.writeFile(path.join(locksDir, "stale.lock"), "stale", "utf8"); + + const provider = createMemoryWikiSourceSyncMigrationProvider(createConfig(vaultRoot)); + await expect(provider.detect()).resolves.toMatchObject({ + found: true, + confidence: "high", + }); + const plan = await provider.plan({} as MigrationProviderContext); + + expect(plan.items.map((item) => item.id)).toContain("memory-wiki-vault-metadata-json"); + + const result = await provider.apply({} as MigrationProviderContext, plan); + const item = result.items.find((item) => item.id === "memory-wiki-vault-metadata-json"); + + expect(item).toMatchObject({ + status: "migrated", + details: { removedStateFile: true, removedLocksDir: true }, + }); + await expect(fs.stat(path.join(metadataDir, "state.json"))).rejects.toMatchObject({ + code: "ENOENT", + }); + await expect(fs.stat(locksDir)).rejects.toMatchObject({ code: "ENOENT" }); + }); +}); diff --git a/extensions/memory-wiki/src/source-sync-migration.ts b/extensions/memory-wiki/src/source-sync-migration.ts index 24db3c3d46c..f2e6d9b994d 100644 --- a/extensions/memory-wiki/src/source-sync-migration.ts +++ b/extensions/memory-wiki/src/source-sync-migration.ts @@ -16,6 +16,14 @@ import { const PROVIDER_ID = "memory-wiki-source-sync"; +function resolveLegacyVaultStatePath(vaultRoot: string): string { + return path.join(vaultRoot, ".openclaw-wiki", "state.json"); +} + +function resolveLegacyVaultLocksDir(vaultRoot: string): string { + return path.join(vaultRoot, ".openclaw-wiki", "locks"); +} + async function legacySourceExists(vaultRoot: string): Promise { const sourcePath = resolveMemoryWikiLegacySourceSyncStatePath(vaultRoot); return await fs @@ -31,6 +39,45 @@ async function legacyLogExists(vaultRoot: string): Promise { .catch(() => false); } +async function legacyVaultMetadataExists(vaultRoot: string): Promise { + const [hasStateFile, hasLocksDir] = await Promise.all([ + fs + .stat(resolveLegacyVaultStatePath(vaultRoot)) + .then((stat) => stat.isFile()) + .catch(() => false), + fs + .stat(resolveLegacyVaultLocksDir(vaultRoot)) + .then((stat) => stat.isDirectory()) + .catch(() => false), + ]); + return hasStateFile || hasLocksDir; +} + +async function removeLegacyVaultMetadata(vaultRoot: string): Promise<{ + removedStateFile: boolean; + removedLocksDir: boolean; +}> { + const statePath = resolveLegacyVaultStatePath(vaultRoot); + const locksDir = resolveLegacyVaultLocksDir(vaultRoot); + const [hadStateFile, hadLocksDir] = await Promise.all([ + fs + .stat(statePath) + .then((stat) => stat.isFile()) + .catch(() => false), + fs + .stat(locksDir) + .then((stat) => stat.isDirectory()) + .catch(() => false), + ]); + if (hadStateFile) { + await fs.rm(statePath, { force: true }); + } + if (hadLocksDir) { + await fs.rm(locksDir, { recursive: true, force: true }); + } + return { removedStateFile: hadStateFile, removedLocksDir: hadLocksDir }; +} + function resolveLegacyImportRunsDir(vaultRoot: string): string { return path.join(vaultRoot, ".openclaw-wiki", "import-runs"); } @@ -88,8 +135,21 @@ export function createMemoryWikiSourceSyncMigrationProvider( const hasSourceSync = await legacySourceExists(config.vault.path); const hasLegacyLog = await legacyLogExists(config.vault.path); const hasLegacyDigests = await legacyMemoryWikiDigestFilesExist(config.vault.path); + const hasLegacyVaultMetadata = await legacyVaultMetadataExists(config.vault.path); const importRunFiles = await listLegacyImportRunJsonFiles(config.vault.path); const items = [ + ...(hasLegacyVaultMetadata + ? [ + createMigrationItem({ + id: "memory-wiki-vault-metadata-json", + kind: "state", + action: "archive", + source: path.join(config.vault.path, ".openclaw-wiki"), + target: "none; Memory Wiki vault metadata is derived from config and SQLite state", + message: "Remove retired Memory Wiki vault state.json and locks directory.", + }), + ] + : []), ...(hasSourceSync ? [ createMigrationItem({ @@ -157,6 +217,7 @@ export function createMemoryWikiSourceSyncMigrationProvider( const found = (await legacySourceExists(config.vault.path)) || (await legacyLogExists(config.vault.path)) || + (await legacyVaultMetadataExists(config.vault.path)) || (await legacyMemoryWikiDigestFilesExist(config.vault.path)) || (await listLegacyImportRunJsonFiles(config.vault.path)).length > 0; return { @@ -180,7 +241,14 @@ export function createMemoryWikiSourceSyncMigrationProvider( continue; } try { - if (item.id === "memory-wiki-source-sync-json") { + if (item.id === "memory-wiki-vault-metadata-json") { + const result = await removeLegacyVaultMetadata(config.vault.path); + items[itemIndex] = { + ...item, + status: "migrated", + details: result, + }; + } else if (item.id === "memory-wiki-source-sync-json") { const result = await importMemoryWikiLegacySourceSyncState({ vaultRoot: config.vault.path, }); diff --git a/extensions/memory-wiki/src/vault.test.ts b/extensions/memory-wiki/src/vault.test.ts index 7029a622d74..75711b0a7e5 100644 --- a/extensions/memory-wiki/src/vault.test.ts +++ b/extensions/memory-wiki/src/vault.test.ts @@ -34,9 +34,12 @@ describe("initializeMemoryWikiVault", () => { await expect(fs.readFile(path.join(rootDir, "WIKI.md"), "utf8")).resolves.toContain( "Render mode: `obsidian`", ); - await expect( - fs.readFile(path.join(rootDir, ".openclaw-wiki", "state.json"), "utf8"), - ).resolves.toContain('"renderMode": "obsidian"'); + await expect(fs.stat(path.join(rootDir, ".openclaw-wiki", "state.json"))).rejects.toMatchObject( + { code: "ENOENT" }, + ); + await expect(fs.stat(path.join(rootDir, ".openclaw-wiki", "locks"))).rejects.toMatchObject({ + code: "ENOENT", + }); }); it("is idempotent when the vault already exists", async () => { diff --git a/extensions/memory-wiki/src/vault.ts b/extensions/memory-wiki/src/vault.ts index ec2afbb6f6c..3076adefc5c 100644 --- a/extensions/memory-wiki/src/vault.ts +++ b/extensions/memory-wiki/src/vault.ts @@ -17,7 +17,6 @@ export const WIKI_VAULT_DIRECTORIES = [ "_attachments", "_views", ".openclaw-wiki", - ".openclaw-wiki/locks", ] as const; type InitializeMemoryWikiVaultResult = { @@ -120,22 +119,6 @@ export async function initializeMemoryWikiVault( withTrailingNewline("# Inbox\n\nDrop raw ideas, questions, and source links here.\n"), createdFiles, ); - await writeFileIfMissing( - rootDir, - ".openclaw-wiki/state.json", - withTrailingNewline( - JSON.stringify( - { - version: 1, - createdAt: new Date(options?.nowMs ?? Date.now()).toISOString(), - renderMode: config.vault.renderMode, - }, - null, - 2, - ), - ), - createdFiles, - ); if (createdDirectories.length > 0 || createdFiles.length > 0) { await appendMemoryWikiLog(rootDir, { type: "init", diff --git a/scripts/check-database-first-legacy-stores.mjs b/scripts/check-database-first-legacy-stores.mjs index 8c0a990e1e4..79ed4cf63a1 100644 --- a/scripts/check-database-first-legacy-stores.mjs +++ b/scripts/check-database-first-legacy-stores.mjs @@ -73,6 +73,8 @@ const legacyStoreMarkers = [ }, { label: "Memory Wiki source sync JSON", pattern: /\bsource-sync\.json\b/u }, { label: "Memory Wiki activity JSONL", pattern: /\b\.openclaw-wiki[/\\]log\.jsonl\b/u }, + { label: "Memory Wiki vault metadata JSON", pattern: /\b\.openclaw-wiki[/\\]state\.json\b/u }, + { label: "Memory Wiki vault lock directory", pattern: /\b\.openclaw-wiki[/\\]locks\b/u }, { label: "Memory Wiki import run JSON", pattern: /\bimport-runs[/\\][A-Za-z0-9._-]+\.json\b/u,