feat: multi-process safety + read-only inspector mode

Filesystem storage now enforces single-writer, many-reader semantics.
A second writer on the same data directory throws at init time with the
holder's PID, hostname, and heartbeat — replacing the previous silent
stale-reads failure mode.

- New: `Brainy.openReadOnly()` — coexists with a live writer, every
  mutation throws clearly.
- New: writer lock at `<rootDir>/locks/_writer.lock` with 10s heartbeat
  and stale-detection (PID liveness + heartbeat freshness).
- New: cross-process flush-request RPC (filesystem-based, no signals)
  so inspectors can force fresh state on demand.
- New: `brain.stats()`, `brain.explain(findParams)`, `brain.health()`
  for operator-facing introspection.
- New: `brainy inspect` CLI with 13 subcommands (stats, find, get,
  relations, explain, health, sample, fields, dump, watch, backup,
  repair, diff), all read-only by default.
- Same-PID re-opens allowed with a warning (preserves test "simulate
  restart" patterns).
- Storage instances passed directly via `storage: new MemoryStorage()`
  are now honoured instead of silently falling through to the
  filesystem auto-detect path.

Brainy + Cortex compose under this model — the lock covers both because
they share `rootDir`, Cortex segments are immutable mmap files, and
MANIFEST updates use atomic-rename.
This commit is contained in:
David Snelling 2026-05-15 11:25:05 -07:00
parent 1bc6a430c7
commit 4fcdc0fef3
12 changed files with 2342 additions and 7 deletions

View file

@ -882,6 +882,52 @@ export interface IntegrationsConfig {
}
}
/**
* Operator-facing summary returned by `brain.stats()`. Designed to fit in a
* terminal screen so an incident responder can read it at a glance: counts,
* mode, lock owner, indexed fields, and index health flags.
*/
export interface BrainyStats {
/** Whether this instance can mutate. `reader` is set by `openReadOnly()` and `asOf()`. */
mode: 'writer' | 'reader'
/** Total entity (noun) count across all types. */
entityCount: number
/** Breakdown by NounType name (`person`, `event`, ...). Zero-count types omitted. */
entitiesByType: Record<string, number>
/** Total relationship (verb) count across all types. */
relationCount: number
/** Breakdown by VerbType name. Zero-count types omitted. */
relationsByType: Record<string, number>
/** Indexed metadata field names known to the metadata index. */
fieldRegistry: string[]
/** Per-index health flags. `true` = index has entries OR no entities exist yet. */
indexHealth: {
hnsw: boolean
metadata: boolean
graph: boolean
}
/** Storage backend info — `backend` is the adapter class name. */
storage: {
backend: string
rootDir?: string
}
/**
* Writer lock metadata if one is currently held on this directory. Present
* for both writer instances (their own lock) and readers inspecting a
* directory with a live writer.
*/
writerLock?: {
pid: number
hostname: string
startedAt: string
lastHeartbeat: string
version: string
rootDir?: string
}
/** The Brainy library version this instance was built against. */
version: string
}
/**
* Brainy configuration
*/
@ -986,6 +1032,31 @@ export interface BrainyConfig {
// - false/undefined (default): Log warning if pending migrations exist, but don't auto-run
// - true: Automatically run pending migrations during init() for small datasets (<10K entities)
autoMigrate?: boolean
/**
* Process role for multi-process safety on filesystem storage.
*
* - `'writer'` (default): acquires an exclusive lock on the storage directory at
* `init()` and refuses to open if another live writer holds the lock. Required
* for any instance that calls `add`/`update`/`delete`/`relate` or any other
* mutation. Released on `close()` and on process exit/SIGINT/SIGTERM.
* - `'reader'`: opens without acquiring the writer lock. Coexists with a live
* writer and with other readers. All mutation methods throw
* `Cannot mutate a read-only Brainy instance`. Use `Brainy.openReadOnly()`
* as a convenience factory.
*
* For cloud storage backends (s3/r2/gcs/azure) the lock is a best-effort
* advisory marker multi-process safety against concurrent writers on
* cloud-backed stores is not yet enforced. See `docs/concepts/multi-process.md`.
*/
mode?: 'writer' | 'reader'
/**
* Bypass the writer-lock check at init even when another writer holds the lock.
* Use only when you know the existing lock is stale and stale-detection
* (PID liveness + heartbeat) cannot prove it. Logs a warning regardless.
*/
force?: boolean
}
// ============= Neural API Types =============