Skip to content

OH MY PM v0.6.2

Latest

Choose a tag to compare

@github-actions github-actions released this 04 Aug 08:47
357f37c

OH MY PM v0.6.2

Project Memory integrity: verification, gap closure, and a supported recovery
path.

One new command (omp memory repair). Everything else behaves exactly as it did
in v0.6.1: no changed flag, output schema, exit code, MCP tool, Project Brain
schema, or Project Memory store format, and no migration is required.

Why this release exists

The original v0.6.2 scope was written as if atomic writes, integrity metadata,
migrations, locking, path confinement, and privacy enforcement all needed
building. They did not — most of them shipped earlier and were genuinely
implemented, not stubbed. Building a second atomic-write layer beside a correct
one would have made the system worse.

So the release was re-scoped to verification and gap closure, and an audit was
performed first, before any implementation:
docs/releases/v0.6.2-integrity-audit.md. That
audit is the authority for what this release contains. It found five real gaps out
of eighteen scope items; the other thirteen were already correct, already proven,
or deliberately out of scope.

The audit's original matrix is preserved unedited, with the closure appended
separately. It records what was believed before the work — which is what makes
it an audit rather than a summary.

The five gaps, and what closed them

G1 — Fault injection could not reach inside the atomic write

writeFileAtomic was one opaque step, so four of the nine crash stages a release
must prove were untestable: before temp write, during temp write, after flush,
before parent sync.

The write was probably correct; nothing proved it. The ordering now lives in
atomic-write.ts, parameterized over its primitives. Production passes real Node
primitives and no hook; tests pass deterministic primitives and a hook that throws
at a named stage. Both run the same ordering logic, so a test cannot pass
against an imitation whose steps happen to differ from production.

Closing this also surfaced a real defect: a failure between temp creation and
rename leaked the temp file, because cleanup was scoped only to the rename step.

G2 — Post-crash recovery was untested, and one test misreported itself

The crash test constructed a fresh in-memory filesystem for its follow-up
commit — a pristine store with no lock file, no staging residue, no partial state
— while its comment claimed it proved the lock was released and a follow-up commit
worked. It proved neither.

This was the most misleading finding in the audit, because the test read as if
crash recovery were covered. The follow-up commit now runs against the same
damaged store
, and asserts the lock is gone, the residue is detectable, and the
next commit succeeds.

That fix exposed a second real defect: a crash after the manifest rename left
staging residue that survived every subsequent identical commit, because the
idempotent early-return skipped cleanup entirely. inspect() reported
abandonedStaging forever.

G3 — Locking was never exercised across real processes

The stale-lock rule (age and dead owner) was correct and is unchanged. But a
single-writer lock's real contract is open(path, "wx") on a real filesystem
between real processes, and every existing test was in-process against an
in-memory port.

Now proven with real spawned subprocesses against the real Node adapter: two
processes cannot both write, a live slow writer is never evicted, a dead stale
owner is reclaimed, acquisition is bounded, and crash residue recovers.

G4 — Path confinement was lexical, and writes were less protected than reads

Containment never consulted the filesystem, so a symlink or junction inside the
data root pointing outside satisfied every check. Reads mitigated this by refusing
to follow a symlink at the leaf; the write path performed no such check.

physical-confinement.ts resolves the physical path before a write, walking the
target one segment at a time and re-checking containment after every step.
Resolving one hop at a time is load-bearing: realpath collapses an entire chain,
so a route that leaves the root and returns — <root>/hop -> <outside>/back -> <root>/real — resolves to a path inside the root and would be accepted, even
though the write physically travels through a directory the store does not govern.

G5 — Corruption could be detected but never isolated or repaired

Detection was strong and deliberately non-destructive. But a user with a corrupt
record had no supported path forward: no quarantine, no preview, no repair.

omp memory repair closes that. See below.

Added: omp memory repair

The invariant the whole design serves:

normal read detects and reports
repair preview scans and proposes
explicit --apply performs bounded mutation
omp memory repair            # scan and propose; changes no byte
omp memory repair --apply    # bounded recovery under the writer lock

Preview and apply are separate store calls, not one call with a flag, so the
read-only path physically cannot reach the writer.

Apply requires explicit intent, takes the same single-writer lock a capture takes,
re-scans under that lock, and refuses a plan whose store fingerprint moved —
before the first write, so a stale plan produces no partial mutation. The
fingerprint is content-derived, never a modification time: mtime granularity is
coarse and platform-dependent, can be moved backwards, and does not change at all
for a same-size in-place edit.

Quarantine is not repair

This distinction is the most important thing in the release. Quarantining a
corrupt record preserves its exact original bytes and makes the rest of the
store readable again. It does not recover that record's meaning. Every output
path reports isolated separately from reconstructed, and there is deliberately
no single "repaired" total — a user whose data is damaged is the last person who
should be told it came back.

The write ordering is the safety argument: read the exact bytes, persist the
payload, persist sanitized metadata, confirm the payload re-reads with a
matching digest
, and only then remove the corrupt live path. No reachable
failure stage leaves the original gone with no readable copy.

What a repair may do, by authority

Class Examples Action
Authoritative records, manifest isolated into quarantine; never rewritten from a guess, never deleted
Derived manifest inventory, chronology rebuilt, but only from records that fully verify
Coordination lock files reclaimed only on age threshold and a dead owner
Residue temp files, abandoned staging removed only when this store's ownership is proven
Recovery evidence quarantine never live, never auto-pruned, never rebuilt

Derived state is rebuilt only from verified records. A rebuild that trusted
the existing inventory would re-reference records the same apply just isolated; one
that trusted whatever is on disk would launder corruption into apparently-valid
authoritative state.

An unsupported future store format is reported and never downgraded. A repair
never writes to your project's source files, makes no network request, and is
not exposed as an MCP tool.

Defects found and corrected during the work

Recorded because a release that only lists what it fixed in others is not honest
about itself.

  • A path-spelling mismatch broke convergence. The missing_referenced_record
    finding emitted a shorter relative path than every other target, so the manifest
    rebuild's comparison never matched and an isolated record was never dropped from
    the inventory — the store did not heal. Fixed by making one function the single
    authority for that spelling, so the two cannot drift.
  • A duplication guard was dead code. An "already isolated, skip" branch could
    never run, because isolating a record changes the fingerprint and the stale-plan
    guard always intercepts a retry first. Removed rather than left as an untestable
    guard.
  • An intermittently-failing baseline. An unhandled-rejection race made the
    integrity suite fail 2 of 8 runs, and an unformatted file failed pnpm quality.
    Both were introduced by earlier commits in this same release.

Corrected evidence incidents

Three, kept permanently visible in the audit closure, because the lesson is the
deliverable.

  1. Invalid mutation evidence. A Vitest filter containing | was assumed to be
    a regex. Vitest treats it as a literal substring, so it matched zero files and
    exited 1. The nonzero exit was read as the mutation being caught; nothing had
    run.
  2. Vacuous assertion. An assertion filtered on issue.severity, a property
    that does not exist on the type. The filter matched nothing, so it passed no
    matter how damaged the store was.
  3. Near-miss during the closure. A test claiming to derive the memory
    subcommand count asserted a hardcoded 7. It now derives on both sides — and
    the first attempt to prove that fix by mutation silently failed to apply, which
    is incident 1 all over again. The harness now fails loudly when a mutation does
    not change the file.

The rule this enforces:

a failing command is not mutation evidence
unless the unmutated baseline passes,
the intended tests are discovered,
the mutation changes the intended behaviour,
and targeted assertions fail

A corollary learned here: a flaky baseline is as disqualifying as a failing
one. Evidence gathered against a baseline that fails a quarter of the time is
worthless.

All ten required G5 mutations are killed, each with a verified-green baseline
before and after and targeted named failures.

Performance evidence

Bounded structural baselines, not SLOs — the full performance architecture
remains v0.6.5.

Timing gates on shared CI are coin flips: they fail on a noisy neighbour and pass
on a genuine 10x regression that ran on a quiet machine. So the baselines count
operations at the filesystem port instead, where every byte of real I/O flows.
Proven: each authoritative file is read at most once per scan; a fully-damaged
store costs the same reads as a clean one of equal size; reads grow linearly, not
quadratically; directory visits are constant; a single-finding apply writes at most
four files and removes exactly one; the lock is acquired exactly once with no
polling; a preview performs zero writes, removes, and locks; and 50 quarantined
payloads change neither findings nor fingerprint.

Retention, privacy, and what this release does not add

  • No automatic pruning. No automatic quarantine deletion. No cache eviction
    semantics, because there is no cache. No automatic deletion of authoritative
    records.
  • Privacy. No absolute path, environment value, command line, token, or raw
    record content reaches a finding, a quarantine metadata file, or CLI output.
    Quarantine metadata carries a classification and digests, never the parsed
    content — the content may be exactly what should not be echoed.
  • Not added: Dashboard, cloud sync, telemetry, automatic pruning, automatic
    repair on read, and MCP write tools. Reads never mutate.

Residual limitations

Stated plainly rather than left for a reader to discover.

  • TOCTOU is reduced, not eliminated. Confinement validates immediately before
    each mutating syscall, but a privileged concurrent actor can replace an ancestor
    in that window. Closing it fully needs descriptor-relative operations
    (openat/O_NOFOLLOW per segment) that Node does not expose. The single-writer
    lock and owner-only 0700 modes bound who can realistically win that race.
  • Power-loss durability is not proven. Injecting a stage failure proves the
    algorithm's recovery contract — which states are reachable and whether a retry
    reconciles. Real power-loss durability is bounded by the platform's own fsync
    semantics and cannot be established by a simulated filesystem.
  • Directory fsync is best-effort. Some platforms refuse it; the rename remains
    the commit point.
  • A store with no manifest is not reconstructed. Records may survive, but
    nothing authoritative says which are current, and capture chronology is never
    invented.

Platform evidence

Local runs skip POSIX-only and Windows-only behaviour, so they cannot establish
cross-platform correctness. The project-memory-integrity CI matrix runs the
integrity suites independently on Ubuntu, macOS, and Windows with
fail-fast: false, so a Windows-only path-semantics or process-termination defect
fails visibly rather than being masked by a green Linux job. Audit items 7
(locking) and 8 (path confinement) are marked proven_with_platform_limit for
exactly this reason.

Compatibility

  • Every valid v0.6.1 store remains readable; the store format is unchanged at v2.
  • Migration fixtures still pass. A future store format is refused without rewrite.
  • Existing CLI outputs are unchanged outside the additive repair command.
  • MCP tool count, names, order, schemas, and annotations are unchanged.
  • repair is appended to the closed memory allowlist, so the six v0.3
    subcommands keep their order and timeline stays seventh.

Upgrading

Nothing to do. If you never run omp memory repair, this release changes nothing
about how your store is read or written.