UNPKG

@tencentdb-agent-memory/memory-tencentdb

Version:

Four-layer local memory system plugin for OpenClaw — auto-captures, structures, and profiles conversational knowledge using local LLM + SQLite vector search (L0→L1→L2→L3 pipeline)

158 lines (127 loc) 5.43 kB
/** * StorageAdapter — adapts IStorageBackend to a fs-like interface * for compatibility with upper-layer code. * * Progressive migration strategy: * Existing L2/L3/Recall code uses `fs.readFile(path.join(dataDir, ...))`. * StorageAdapter provides equivalent method signatures, internally delegating * to IStorageBackend, so existing code only needs to swap the import. * * Eventually, callers may inline IStorageBackend calls directly and * this adapter can be removed. */ import type { IStorageBackend, StorageObject, ListEntry } from "./types.js"; export class StorageAdapter { constructor(private backend: IStorageBackend) {} get type() { return this.backend.type; } // ── fs.readFile replacement ── async readFile(key: string): Promise<string | null> { const obj = await this.backend.getObject(key); if (!obj) return null; return obj.content.toString("utf-8"); } async readFileOrThrow(key: string): Promise<string> { const content = await this.readFile(key); if (content === null) throw new Error(`File not found: ${key}`); return content; } async readFileBuffer(key: string): Promise<Buffer | null> { const obj = await this.backend.getObject(key); if (!obj) return null; return obj.content; } // ── fs.writeFile replacement ── async writeFile(key: string, content: string | Buffer): Promise<void> { return this.backend.putObject(key, content); } // ── fs.appendFile replacement — atomic via backend.appendObject (CR-1 fix) ── /** * Append to a storage object atomically. * * CR-1 fix (2026-05-19): previously implemented as read-modify-write * (readFile + concat + putObject), which lost data under concurrency * (audit/exp1 reproduced 99% loss at 100 parallel writes). Now delegates * to backend.appendObject which uses: * - LocalStorageBackend: POSIX fs.appendFile (O_APPEND atomic) * - CosStorageBackend: COS Append Object API (server-side atomic + 409 retry) */ async appendFile(key: string, content: string): Promise<void> { return this.backend.appendObject(key, content); } // ── fs.readdir replacement ── async readdir(prefix: string, suffix?: string): Promise<ListEntry[]> { const result = await this.backend.listObjects(prefix, { maxKeys: 10000 }); if (!suffix) return result.entries; return result.entries.filter(e => e.key.endsWith(suffix)); } async readdirNames(prefix: string, suffix?: string): Promise<string[]> { const entries = await this.readdir(prefix, suffix); return entries .filter(e => !e.isDirectory) .map((e) => { // Return filename without prefix const name = e.key.startsWith(prefix) ? e.key.slice(prefix.length) : e.key; return name; }); } // ── fs.unlink replacement ── async unlink(key: string): Promise<void> { return this.backend.deleteObject(key); } // ── fs.rm (recursive) replacement ── async rmdir(prefix: string): Promise<void> { await this.backend.deleteByPrefix(prefix); } // ── fs.mkdir (recursive) replacement ── // No-op for object storage (directories are implicit). // For local backend, putObject auto-creates parent dirs. async mkdir(_prefix: string): Promise<void> { // No-op: directories are created implicitly on putObject } // ── fs.access replacement ── async exists(key: string): Promise<boolean> { return this.backend.exists(key); } // ── fs.stat replacement ── async stat(key: string): Promise<{ key: string; size: number; lastModified: number; createdAt: number } | null> { const obj = await this.backend.getObject(key); if (!obj) return null; const lastModified = obj.lastModified?.getTime() ?? Date.now(); return { key, size: obj.size ?? obj.content.length, lastModified, createdAt: lastModified, }; } // ── fs.rename replacement ── async rename(sourceKey: string, destKey: string): Promise<void> { // CR-8 partial fix (2026-05-19): preserve contentType + metadata across rename. // The 3-step (get → put → delete) is still NOT atomic; if the process is killed // between put and delete, both source and dest will exist (data duplication). // A complete fix requires a native renameObject in IStorageBackend (using // POSIX fs.rename for local + COS x-cos-copy-source for remote). Tracked as // long-term work — see audit report H-6 (persona.md backup rotation). const obj = await this.backend.getObject(sourceKey); if (!obj) throw new Error(`Source not found: ${sourceKey}`); await this.backend.putObject(destKey, obj.content, { contentType: obj.contentType, metadata: obj.metadata, }); await this.backend.deleteObject(sourceKey); } // ── fs.copyFile replacement ── async copyFile(sourceKey: string, destKey: string): Promise<void> { // CR-8 partial fix (2026-05-19): preserve contentType + metadata across copy. const obj = await this.backend.getObject(sourceKey); if (!obj) throw new Error(`Source not found: ${sourceKey}`); await this.backend.putObject(destKey, obj.content, { contentType: obj.contentType, metadata: obj.metadata, }); } // ── Direct backend access ── getBackend(): IStorageBackend { return this.backend; } }