UNPKG

akm-cli

Version:

akm (Agent Knowledge Manager) — a portable, local-first capability library for AI agents. Discover, load, share, and improve reusable skills, scripts, workflows, and knowledge across any shell-capable coding agent, including Claude Code, OpenCode, and Cur

195 lines (194 loc) • 7.72 kB
// This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. /** * Side-effect-free SQLite snapshots for planning and preview paths. * * A normal SQLite read-only connection can still write lock bytes into an * existing `-shm` file when the source database is in WAL mode. Dry planning * must not do that. This helper copies a stable main/WAL pair and opens that * private copy, so SQLite never attaches to the operator's original * main/WAL/SHM files. * * The copy itself is made by a child process, never by this one: see * {@link copyFileOutsideThisProcess} for why an in-process read of a live * database is unsafe. */ import { spawnSync } from "node:child_process"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { sleepSync } from "../runtime.js"; import { openDatabaseFinalizing } from "./database.js"; const SNAPSHOT_MAX_ATTEMPTS = 8; const SNAPSHOT_BACKOFF_MS = 25; export class SqliteReadSnapshotUnavailableError extends Error { constructor(message) { super(message); this.name = "SqliteReadSnapshotUnavailableError"; } } function pathExists(filePath) { try { fs.lstatSync(filePath); return true; } catch (error) { if (error.code === "ENOENT") return false; throw error; } } function fileFingerprint(filePath) { try { const stat = fs.statSync(filePath, { bigint: true }); return { size: stat.size, mtimeNs: stat.mtimeNs, ctimeNs: stat.ctimeNs }; } catch (error) { if (error.code === "ENOENT") return undefined; throw error; } } function databaseFingerprint(dbPath) { const main = fileFingerprint(dbPath); if (!main) { throw new SqliteReadSnapshotUnavailableError("SQLite snapshot source disappeared while it was being inspected"); } const wal = fileFingerprint(`${dbPath}-wal`); return { main, ...(wal ? { wal } : {}) }; } function fingerprintsEqual(left, right) { const sameFile = (a, b) => a?.size === b?.size && a?.mtimeNs === b?.mtimeNs && a?.ctimeNs === b?.ctimeNs; return sameFile(left.main, right.main) && sameFile(left.wal, right.wal); } /** * Copy `source` to `destination` without opening `source` in THIS process. * * POSIX advisory locks belong to the process, not to the descriptor: when a * process closes ANY descriptor for a file, the kernel drops every lock that * process holds on it, including the SHARED lock a live SQLite connection in * this same process (`akm improve` keeps one on state.db for the whole run) * holds. A peer using an older SQLite (< 3.51) read-write would then see no * reader, take EXCLUSIVE when it closes, and delete the `-wal`/`-shm` this * process is still using. The read therefore happens in a child `cp`, whose * descriptors and locks are its own. Windows locks belong to the handle, so * an in-process copy cannot release another handle's lock there (and there is * no `cp`). * * A source that vanished (a WAL checkpointed away mid-copy) throws an * `ENOENT` error, which {@link openSqliteReadSnapshot} retries. */ function copyFileOutsideThisProcess(source, destination) { if (process.platform === "win32") { fs.copyFileSync(source, destination); return; } const result = spawnSync("cp", ["--", source, destination], { encoding: "utf8", stdio: ["ignore", "ignore", "pipe"], }); if (result.error) { throw new SqliteReadSnapshotUnavailableError(`cannot run cp to copy ${source}: ${result.error.message}`); } if (result.status === 0) return; // `cp` reports its errors as text, so the vanished-source case is recognised // by looking at the source itself. if (fileFingerprint(source) === undefined) { throw Object.assign(new Error(`${source} disappeared while it was being copied`), { code: "ENOENT" }); } const reason = result.stderr.trim() || (result.signal ? `killed by ${result.signal}` : `exit status ${result.status}`); throw new Error(`cp could not copy ${source}: ${reason}`); } /** * Open an isolated copy of an existing SQLite database. * * Returns `undefined` only when the source is absent. Committed WAL frames are * copied beside the private main file; the source SHM is intentionally not * copied because SQLite can safely create one inside the disposable directory. * A live rollback-journal database fails closed because a file-level copy * cannot distinguish its committed and uncommitted pages without attaching to * the source. */ export function openSqliteReadSnapshot(dbPath) { if (!pathExists(dbPath)) return undefined; const snapshotDir = fs.mkdtempSync(path.join(os.tmpdir(), "akm-sqlite-read-")); const snapshotPath = path.join(snapshotDir, "snapshot.db"); let db; try { let copied = false; for (let attempt = 0; attempt < SNAPSHOT_MAX_ATTEMPTS; attempt++) { if (attempt > 0) sleepSync(SNAPSHOT_BACKOFF_MS * attempt); try { if (pathExists(`${dbPath}-journal`)) continue; const before = databaseFingerprint(dbPath); copyFileOutsideThisProcess(dbPath, snapshotPath); if (before.wal) copyFileOutsideThisProcess(`${dbPath}-wal`, `${snapshotPath}-wal`); else fs.rmSync(`${snapshotPath}-wal`, { force: true }); const after = databaseFingerprint(dbPath); if (fingerprintsEqual(before, after) && !pathExists(`${dbPath}-journal`)) { copied = true; break; } } catch (error) { if (error.code === "ENOENT") continue; throw error; } } if (!copied) { throw new SqliteReadSnapshotUnavailableError(`SQLite main/WAL files did not settle after ${SNAPSHOT_MAX_ATTEMPTS} attempts with backoff — ` + "a writer may be continuously active, or a hot rollback journal never cleared"); } db = openDatabaseFinalizing(snapshotPath, { readonly: true, create: false }); const closeSnapshot = db.close.bind(db); let closed = false; const cleanup = (bestEffort) => { if (closed) return; closed = true; process.removeListener("exit", cleanupOnExit); if (bestEffort) { try { closeSnapshot(); } catch { // Process teardown is already committed; keep removing the copy. } try { fs.rmSync(snapshotDir, { recursive: true, force: true }); } catch { // Best-effort process-exit backstop. Normal close still surfaces IO errors. } return; } try { closeSnapshot(); } finally { fs.rmSync(snapshotDir, { recursive: true, force: true }); } }; const cleanupOnExit = () => cleanup(true); process.once("exit", cleanupOnExit); db.close = () => cleanup(false); return db; } catch (error) { try { db?.close(); } finally { fs.rmSync(snapshotDir, { recursive: true, force: true }); } throw error; } }