refactor: drop memory wiki vault metadata files

This commit is contained in:
Peter Steinberger
2026-05-08 20:23:35 +01:00
parent 32adc09522
commit d68f543180
6 changed files with 154 additions and 21 deletions

View File

@@ -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`

View File

@@ -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<string> {
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" });
});
});

View File

@@ -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<boolean> {
const sourcePath = resolveMemoryWikiLegacySourceSyncStatePath(vaultRoot);
return await fs
@@ -31,6 +39,45 @@ async function legacyLogExists(vaultRoot: string): Promise<boolean> {
.catch(() => false);
}
async function legacyVaultMetadataExists(vaultRoot: string): Promise<boolean> {
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,
});

View File

@@ -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 () => {

View File

@@ -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",

View File

@@ -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,