A closed interval cannot be reopened

pi-memory

A Pi extension whose facts carry a validity interval separate from when they were recorded and a five-value standing the storage layer reads through an allowlist — and whose resolve path refuses to move a superseded or contradicted fact back to a live standing, telling the caller to record a new fact instead.

Carries 2 of 7 rubric mechanisms. Most systems here carry none or one (41%), and a dash means the mechanism was not found at this commit — not that the system needed it. Each mark is one LLM reviewer's reading of the code at this commit rather than a run of it — known limits.

  • Tombstone
  • Trust state
  • Bi-temporal
  • Scope enforced
  • Mutation audit
  • Human review
  • Negative evals

1. Executive Summary

pi-memory is "Pi-native temporal memory and hybrid retrieval for agents" — MIT, TypeScript, version 0.1.0, 15,920 lines with 41 test files. It is an extension for Pi plus a standalone daemon that "analyzes changed sources while Pi is closed."

The architecture note in the README is worth quoting for what it declines to do: /memory sync "fingerprints publishers and enqueues work — it does not copy raw source bodies", and "[d]o not install a persistent service unless explicitly authorized." Both are restraint written into the first screen.

The memory model is bitemporal in the way the atlas asks for. A fact carries valid_at and invalid_at — when the thing it states was true — separately from recorded_at, when the store learned it. Both retrieval entry points accept an asOf, parse it strictly ("asOf must be ISO-8601."), default it to now, and filter candidates with factIsValidAt(fact, asOf). The federated path resolves the instant once and passes it down to every engine leg, so a query spanning several scopes reads one consistent moment rather than a different now per leg.

Beside that runs a five-value standing — candidate, supported, needs_review, contradicted, superseded — and the storage layer reads it through an allowlist:

SELECT id FROM facts WHERE scope_id=? AND standing IN ('supported','needs_review','candidate')

An allowlist rather than a denylist, so a standing added next year is withheld by default instead of leaking until somebody remembers to exclude it.

The sentence that makes the whole thing cohere is in resolveFact. Moving a fact to a new standing requires a rationale, which is redacted before it is committed, and a replacement must be a different existing fact. And a fact that is already superseded or contradicted cannot be moved back:

"A closed interval cannot be reopened without losing history; record a new fact instead."

That is the rule that keeps a mutable standing from undoing a temporal record. The standing can move forward; the interval it closed stays closed; the way to say "actually it is true again" is to record that as a new fact with its own validity, which is what a bitemporal store is for.

The capture gate deserves a mention for one detail. It refuses routine chatter — ok, done, lgtm, wip — and tool dumps, and then isNegationOrCorrection deliberately admits corrections and constraints regardless. Corrections are the class most likely to look like noise to a filter and the most expensive to drop, and the module's first line names the failure it is avoiding: "Not a copy of prjct fail-open excess."

2. Mental Model

A fact states something, for an interval, as of a moment it was recorded.

A standing says how the store currently regards it. Two of the five values are terminal.

An asOf is a question about a moment, not about now.

Evidence is what a fact rests on: a content hash, a provenance, an actor, a session and the tool call that produced it.

Diagram — validity time and record time are separate columns filtered by a validated asOf, while the standing is read through an allowlist and its two terminal values cannot be reopened
Diagram source
%% caption: validity time and record time are separate columns filtered by a validated asOf, while the standing is read through an allowlist and its two terminal values cannot be reopened
flowchart TB
    CAP["Pi extension records ·<br/>daemon analyses changed sources<br/>while Pi is closed"] --> GATE{"capture gate"}
    GATE -->|"ok · done · lgtm · wip ·<br/>tool dumps"| DROP["refused"]
    GATE -->|"isNegationOrCorrection —<br/>corrections and constraints<br/>admitted regardless"| RED["redactSecrets"]
    GATE -->|"ordinary"| RED
    RED --> F[("facts: statement · standing ·<br/>confidence · valid_at · invalid_at ·<br/>recorded_at · scope_id")]
    EV[("evidence: provenance ·<br/>content_hash · observed_at ·<br/>actor_id · tool_call_id")] --- F
    F --> READ{"activeFacts:<br/>standing IN ('supported',<br/>'needs_review','candidate')"}
    READ -->|"allowlist — a standing added<br/>later is withheld by default"| OUT["read"]
    RES["resolveFact(factId, standing,<br/>rationale, replacementId?)"] --> CHK{"is the fact already<br/>superseded or contradicted?"}
    CHK -->|"yes, and the new standing is live"| REF["throws: 'A closed interval cannot be<br/>reopened without losing history;<br/>record a new fact instead.'"]
    CHK -->|"no"| MOVE["standing moves · rationale required<br/>and redacted · replacement must be<br/>a different existing fact"]
    MOVE --> F
    Q["recall(query, asOf?)"] --> P{"asOf parsed as ISO-8601,<br/>rejected if unparseable,<br/>defaults to now"}
    P --> FED["federated: resolve the instant ONCE,<br/>pass it to every engine leg"]
    FED --> VAL["factIsValidAt(fact, asOf) —<br/>the validity interval, not recorded_at"]
    VAL --> RES2["results carry validAt, invalidAt,<br/>expiredAt AND the standing —<br/>retrieval labels rather than withholds"]

3. Architecture

Area Role
src/storage/migrations.ts The schema: documents, chunks, episodes, evidence, entities, facts
src/storage/projection.ts The read layer, including the standing allowlist
src/engine.ts resolveFact and the reopening guard
src/retrieval/hybrid.ts, federated.ts asOf parsing, candidate selection, validity filtering
src/curation/ The pipeline, validation and publication
src/retention/ Capture gate, consolidation, a value model, bounded GC
src/security/redact.ts Secret redaction on capture and on rationales
src/eval/ Oracles, evidence scoring and a comparison CLI
src/daemon/ Out-of-process analysis of changed sources

4. Essential Implementation Paths

src/engine.ts:358-367resolveFact, four lines of validation and the sentence that explains the model.

src/storage/projection.ts:721-726 — the allowlist, and the scope parameter.

src/retrieval/federated.ts:29-40 — one asOf resolved once and pushed to every leg.

src/retention/capture-gate.ts:1-12 — what it refuses, and the exception for corrections.

5. Memory Data Model

Facts sit on top of evidence rather than replacing it. An evidence row carries an origin, a provenance string, an optional URI, a content hash, an excerpt, an observed-at, an actor, a session and the tool call it came from; an episode_evidence join links episodes to the evidence they rest on, with ON DELETE CASCADE from the episode and a plain reference to the evidence, so deleting an episode does not delete the evidence another episode may also cite.

A fact adds kind, a statement, an optional subject/predicate/object triple, a standing, a confidence, and the three timestamps. Confidence is a float and therefore not a trust state in the atlas's sense; standing is the discrete field, and it is the one the reads use.

6. Retrieval Mechanics

Hybrid lexical, vector and graph retrieval over a scope, federated across engines. Two details are worth borrowing.

asOf is validated rather than coerced — an unparseable value throws instead of silently becoming now — and in the federated path it is resolved once and passed down, which is the difference between "as of last Tuesday" and "as of whenever each leg happened to evaluate".

And the relevance gate reports rather than returns: when nothing clears the threshold the response carries "Insufficient evidence: no candidate meets the default relevance gate." A memory that can say it does not know is a better input to an agent than one that returns its three closest guesses.

The honest limit is what retrieval does with standing. It is threaded into every result — standing: fact.standing beside validAt, invalidAt and expiredAt — rather than used to withhold. So the storage-level allowlist governs activeFacts and publication, and a caller using the hybrid API gets contradicted facts back, labelled. That is a defensible design for an API whose consumer is another program, and it means the label has to be read.

7. Write Mechanics

The extension records; the daemon analyses. /memory sync fingerprints publishers and enqueues work rather than copying source bodies, which keeps the store from becoming a second copy of everything it has seen.

resolveFact is the state-change surface and it asks for three things: the new standing, a rationale, and optionally a replacement. The rationale passes through redactSecrets before it reaches the event log, which is the kind of detail that is easy to skip — a free-text explanation of why a fact was contradicted is exactly where somebody pastes a token.

8. Agent Integration

A Pi extension and a daemon, with the caution about persistent services stated in the README rather than left to the installer. The eval CLI and oracles — final-answer, grounding, evidence-complete, unanswerable — suggest the project measures answer quality rather than only retrieval, and having an unanswerable oracle kind at all is a good sign: it is the case where a memory system is supposed to say nothing.

9. Reliability, Safety, and Trust

Redaction on capture and on rationales, a capture gate, a compact-authority singleton with a digest, an applied-events table, and the reopening guard.

The guard is the one to take away, and it generalises past this project. Any store with a mutable status over a temporal record faces the same question: can a closed interval be reopened? Answering yes quietly loses the history of the closure; answering no forces the honest representation — a new fact, with its own validity, recording that the thing became true again. pi-memory answers no, in one line, with the alternative in the same sentence.

10. Tests, Evals, and Benchmarks

41 test files including curation-safety, capture-gate, compact-authority, compact-promotion and daemon, plus the eval oracles and a comparison CLI.

What is absent is a must-not-retrieve assertion: nothing in the tree seeds a contradicted or superseded fact and asserts it stays out of a retrieval, which is the test that would pin the allowlist against a future refactor moving the filter. Given that the storage query is the single place the rule lives, that test is cheap and load-bearing.

11. For Your Own Build

Refuse to reopen a closed interval. If your status is mutable and your record is temporal, the two will eventually disagree; deciding in advance that terminal means terminal, and that the way forward is a new fact, keeps the history honest. Put the alternative in the error message so the caller knows what to do.

Read the status through an allowlist. A denylist knows only the statuses somebody remembered to add; an allowlist withholds a new one by default, which is the direction you want to fail in.

Validate asOf and resolve it once. Parsing it strictly stops a bad string silently becoming now, and resolving it once before fanning out is what makes a federated as-of query mean anything.

Let the gate admit corrections. A noise filter tuned on "ok" and "done" will also catch "no, it's the other one" unless you exempt it deliberately, and that is the sentence you most wanted to keep.

Redact free-text rationales. The field where someone explains why a fact was wrong is a field where someone pastes the thing that proved it.

12. Open Questions

Whether the hybrid API should withhold contradicted facts rather than label them. The storage layer and the publication path both exclude them; the retrieval API does not, and nothing in the tree records that as a decision either way.

Whether scope_id will move from a parameter to a property of the handle. As a parameter it is the caller's to pass correctly, which is why no scope mark is claimed here.

What the redactor does not catch. The rest of the codebase writes limits down; this module does not, and a pattern-based redactor always has some.

Appendix: File Index

Path What to read it for
src/engine.ts:358-367 The reopening guard, and the alternative in the message
src/storage/projection.ts:721-726 An allowlist read, and the scope parameter
src/retrieval/federated.ts:29-40 One asOf, resolved once, pushed to every leg
src/storage/migrations.ts:137-176 Evidence with provenance, and a fact's three timestamps
src/retention/capture-gate.ts What it refuses, and the exemption for corrections
src/eval/oracles.ts An oracle kind for the answer that should not be given

History

2026-09-164eed6542… — first reading, at a commit dated 15 September 2026. Screened before opening, from a shallow clone: three files scanned, no auto-run surfaces, no build-time execution points, one unpinned surface and two dependency files inside the seven-day cooldown. Nothing was installed, built or run, and the README's caution against installing a persistent service was moot for a read-only reading.