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

160 lines (159 loc) • 7.47 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/. /** * Shared SQLite PRAGMA application + journal-mode resolution (#628). * * Every AKM SQLite opener applies the same opening PRAGMAs: a journal mode, a * 30 s busy_timeout, and (for most) foreign_keys = ON. Historically each opener * hard-coded `PRAGMA journal_mode = WAL`. WAL is impossible on network * filesystems (NFS/SMB) because its `-shm` shared-memory wal-index cannot be * mmap'd over a network mount, so AKM could not run with its data dir on a * network share. * * This module centralises that PRAGMA block behind {@link applyStandardPragmas} * and makes the journal mode configurable via the `AKM_SQLITE_JOURNAL_MODE` * env var (WAL | DELETE | TRUNCATE; default WAL = unchanged behaviour). When the * mode is the WAL default and the data directory is detected to live on a * network filesystem, it auto-falls-back to DELETE with a one-line warning. * * Boundary note: this is a PLAIN module, not a runtime-boundary file. It only * does pure string work plus `db.exec()` (allowed everywhere). The single * runtime primitive it needs — a filesystem-type probe (statfs) — lives in * src/runtime.ts and is injected here, keeping the network-FS classifier a * pure, unit-testable function. * * @module storage/sqlite-pragmas */ import { warn } from "../core/warn.js"; import { statfsType } from "../runtime.js"; const VALID_MODES = new Set(["WAL", "DELETE", "TRUNCATE"]); // One-shot warning guards so a misconfigured env var or a network-FS fallback // each emit AT MOST ONCE per process rather than on every db open. let warnedInvalid = false; let warnedNetworkFallback = false; /** * Resolve a raw `AKM_SQLITE_JOURNAL_MODE` value to a canonical {@link JournalMode}. * * PURE and unit-testable: the raw string is passed in (not read from * `process.env` here). Trims + uppercases; an empty/undefined value yields the * WAL default; a recognised value yields its canonical form; any other * non-empty value warns once and falls back to WAL. Never throws. */ export function resolveJournalMode(raw) { if (raw === undefined) return "WAL"; const normalized = raw.trim().toUpperCase(); if (normalized === "") return "WAL"; if (VALID_MODES.has(normalized)) { return normalized; } warnInvalidJournalModeOnce(raw); return "WAL"; } /** * The single env-reading seam: resolve the configured journal mode from * `process.env.AKM_SQLITE_JOURNAL_MODE`. Read at call time (per open) so tests * that set the env per-case see the right value and we avoid stale-env flakes. */ export function resolveConfiguredJournalMode(env = process.env) { return resolveJournalMode(env.AKM_SQLITE_JOURNAL_MODE); } function warnInvalidJournalModeOnce(raw) { if (warnedInvalid) return; warnedInvalid = true; warn(`[akm] invalid AKM_SQLITE_JOURNAL_MODE=${JSON.stringify(raw)} — using WAL (valid: WAL, DELETE, TRUNCATE)`); } // Known Linux f_type magic numbers for network filesystems. node's statfs // normalises `type` to this numeric f_type magic on all platforms. const FS_MAGIC_NFS = 0x6969; // 26985 — NFS const FS_MAGIC_SMB = 0x517b; // 20859 — older SMB_SUPER_MAGIC const FS_MAGIC_CIFS = 0xff534d42; // 4283649346 — SMB/CIFS const FS_MAGIC_SMB2 = 0xfe534d42; // 4267272514 — SMB2 const FS_MAGIC_FUSE = 0x65735546; // 1702057286 — FUSE (sshfs + many network FUSE mounts) const NETWORK_FS_MAGICS = new Set([ FS_MAGIC_NFS, FS_MAGIC_SMB, FS_MAGIC_CIFS, FS_MAGIC_SMB2, // FUSE is a judgment call: it backs BOTH network mounts (sshfs) and local // mounts (some encrypted/overlay FS). Treating it as network falls back to // DELETE — conservative-but-safe (DELETE works everywhere; the only cost is // losing WAL concurrency). An operator can always force WAL via the env var. FS_MAGIC_FUSE, ]); /** * PURE classifier: is `fsType` a known network-filesystem magic number? * Returns false for `undefined` (probe failed/unsupported) and for local * magics (ext4 0xEF53, btrfs, xfs, tmpfs, apfs, …). Unit-testable with * injected magic numbers — no real mount required. */ export function isNetworkFilesystem(fsType) { if (fsType === undefined) return false; return NETWORK_FS_MAGICS.has(fsType); } /** How long a statement waits for a lock before failing with SQLITE_BUSY. */ export const SQLITE_BUSY_TIMEOUT_MS = 30_000; /** * The standard set for a READ-ONLY handle: just `busy_timeout`. A read-only * connection cannot run the rest (journal_mode and foreign_keys are write * operations), but SQLite's default timeout of 0 makes reads fail INSTANTLY * under writer contention — which matters in the DELETE/TRUNCATE journal modes * the network-filesystem fallback and `AKM_SQLITE_JOURNAL_MODE` can select, * where readers do block. */ export function applyReadonlyPragmas(db) { db.exec(`PRAGMA busy_timeout = ${SQLITE_BUSY_TIMEOUT_MS}`); } /** * Apply AKM's standard opening PRAGMAs to `db`, in order: * 1. `journal_mode` = the configured mode (with WAL→DELETE network-FS fallback) * 2. `busy_timeout = 30000` * 3. `foreign_keys = ON` (unless `opts.foreignKeys === false`) * 4. `synchronous = FULL` (only in a rollback-journal mode — DELETE/TRUNCATE) * * Returns the effective {@link JournalMode} so callers/tests can assert it. * * `synchronous = FULL` is set explicitly only in DELETE/TRUNCATE so durability * intent is clear: rollback journals need FULL for crash-durability across * power loss, whereas WAL is durable at NORMAL. SQLite's default synchronous is * already FULL when unset, so this never changes WAL-default behaviour — the * WAL path emits no `synchronous` pragma, exactly as before. */ export function applyStandardPragmas(db, opts = {}) { let mode = resolveConfiguredJournalMode(opts.env); // Network-FS fallback only fires for the WAL default and only when we have a // directory to probe. An explicitly-requested DELETE/TRUNCATE is never // overridden, and a failed/unsupported probe (undefined) keeps WAL. if (mode === "WAL" && opts.dataDir) { const probe = opts.fsTypeProbe ?? statfsType; if (isNetworkFilesystem(probe(opts.dataDir))) { mode = "DELETE"; warnNetworkFallbackOnce(opts.dataDir); } } // PRAGMAs must run before any DDL or DML. busy_timeout is applied FIRST so a // journal-mode change that must reclaim a leftover `-wal` file (WAL→DELETE on // reopen of an unclean WAL db, e.g. after a crash) can wait out a transient // lock instead of failing immediately with SQLITE_BUSY. For the WAL default // this is a no-op (WAL→WAL changes nothing), so byte-identical behaviour is // preserved. db.exec(`PRAGMA busy_timeout = ${SQLITE_BUSY_TIMEOUT_MS}`); db.exec(`PRAGMA journal_mode = ${mode}`); if (opts.foreignKeys !== false) { db.exec("PRAGMA foreign_keys = ON"); } if (mode !== "WAL") { db.exec("PRAGMA synchronous = FULL"); } return mode; } function warnNetworkFallbackOnce(dataDir) { if (warnedNetworkFallback) return; warnedNetworkFallback = true; warn(`[akm] network filesystem detected at ${dataDir} — WAL unsupported, using DELETE journal mode`); }