OpenClaw memory plugin

PLUR1BUS

Per-agent LanceDB memory for OpenClaw where every correction is versioned, evidenced and logged, in an append-only store whose scorers must choose which copy to read.

LicenceMIT
Size91,523 lines of JavaScript in index.js and 263 files under lib/, 6,495 more in scripts/, and 106,035 in 482 files under tests/ and test/, 472 of them test files; docs/ excluded, which carries a vendored node_modules copy
Activity1,215 commits on main by 7 author identities, 28 May – 17 September 2026

Carries 6 of 7 rubric mechanisms. Most systems here carry none or one (44%), 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

PLUR1BUS is a memory plugin for OpenClaw, at release 7.12.61, and the first unusual thing about it is that its test suite is larger than the implementation it covers.

The second unusual thing is the ratio of care to surface. openclaw.plugin.json declares fifty top-level configuration groups, covering dreaming, emotional state, persona voice, an Obsidian vault bridge, skill mining, reminders, a semantic lens, conversation reactivation, and a proactive governor. Underneath that is a correction path — lib/safe-update.js, 480 lines — that is more disciplined than most of the dedicated memory systems in this atlas: a content change is refused unless the caller supplies both an update source and a quoted piece of evidence, the replacement row is written and made durable before the old row is marked superseded, and the whole transition is appended to a reconsolidation event log keyed by an idempotency hash.

The most interesting single line in the repository is safe-update.js:398. Before a content change is accepted, the new embedding is compared against the old one and the update is rejected if the cosine distance exceeds 0.45 — a machine refusing to let a "correction" quietly replace a memory with something that means something else. It is a genuinely novel gate, and the one caller outside the automated conflict apply, the user-facing /correct command at index.js:9642, passes skipDriftGate: true — deliberately, with the reasoning written at the call site: the gate throws rather than degrades, a large correction is exactly what a user typing /correct intends, and the confirmation dialog shows the old and new text in full before anything is written. The measured drift is still recorded on the reconsolidation event. A gate that is off by argument is a different thing from a gate that is off by accident, and this is the first.

Where it is strongest: scope. checkAccess (lib/acl-middleware.js:103) denies by default, denies on a missing owner, denies on a conflicting ownership tuple, and is applied as a filter on the read path in three places. The tests assert the denials rather than the permissions.

Where doubt acts, it now acts in two places and is held back in a third on a measurement rather than an omission. scoreNeoRecallItem returns -Infinity for demoted alongside the deletion states, so a record a person has demoted cannot reach the prompt at all; the claim-level epistemic status does the same for invalidated at all three read layers (recall-pipeline.js:180, neo-arch.js:1484, and the SQL clause at db-adapter.js:562). conflict is still a ranking penalty, and the comment that keeps it one is the interesting part — the detector is "an unvalidated LLM", a live probe on 16 August 2026 found 4,017 newest-revision records carrying conflict (2,505 on a single agent) "with no resolve path that clears the status", and a twenty-row sample was not pairwise contradiction. Hard-filtering on that signal would have withheld thousands of records on a flag the system cannot yet clear. The distinction the code draws — withhold on a state a person set, rank on a state a model guessed — is the right one, and it is drawn with the numbers in the source.

Two mechanisms this atlas looks for are present and unusually well-tested. A claim carries a real-world validity window — validFrom/validUntil on the row, tracked separately from createdAt/updatedAt and queryable as-of through a validAt recall parameter, so "where did he work in 2025" is a different query from "what did we record in 2025". And a value-keyed tombstone survives a /forget: a content fingerprint is written to a durable append-only registry and checked as step zero of every capture, so the forgotten sentence cannot be silently re-stored.

2. Mental Model

There are two stores and they hold different kinds of thing.

LanceDB cards are the memories proper — one table per agent, a row per card. A card's life is short to describe: it is active, or it has been superseded by a newer version that names it in previousVersion. Retrieval is unforgiving about this. lib/recall-pipeline.js:176 drops any entry whose status is set and is not active, so a superseded card is not ranked down, it is gone from the read path.

Neo records are the JSONL layer — turn journal, memory candidates, behaviour cards, graph edges, dream diary, episodes — and they carry the epistemics. Each record has a status from NEO_STATUSES (lib/neo-arch.js:65):

["candidate", "active", "promoted", "demoted", "conflict", "pruned", "tombstoned"]

and an origin.trustLevel from NEO_TRUST_LEVELS (lib/neo-arch.js:55), running untrusted → user_asserted → assistant_asserted → tool_observed → validated → curated.

What moves a record between states is a person. /plur1bus memory promote|demote|prune|tombstone <id> (index.js:8248) requires authorization, calls transitionRecordStatus, and appends the transitioned record back to the JSONL. There is no automatic promoter; a candidate becomes promoted because someone typed the command.

The consequence of each state is where the design divides, and the line has moved. scoreNeoRecallItem returns -Infinity for pruned, tombstoned and demoted — three states genuinely withheld, and the third is there because a person set it. conflict stays in the arithmetic below on the reasoning quoted in section 1. Everything between is arithmetic:

const trustBoost = ({ curated: 0.3, validated: 0.25, user_asserted: 0.18,
  tool_observed: 0.18, assistant_asserted: -0.2, untrusted: -0.3 })[item.origin?.trustLevel] ?? 0;
…
const penalties = (item.origin?.role === "assistant" ? 0.2 : 0)
  + (item.status === "demoted" ? 0.35 : 0)
  + (item.status === "conflict" ? 0.3 : 0)
  + (item.stale === true ? 0.15 : 0);

The exits from conflict are authorized and narrow, which is the right shape for a status a model assigns. Two subcommands sit behind the same authorization check as promote/demote/prune/tombstone (index.js:6853). /plur1bus curation resolve <id> keep|drop (lib/curation-resolve.js, dispatched at index.js:8065) moves one record and appends a curation.resolve event. /plur1bus curation drop-injected (lib/drop-injected-conflicts.js, index.js:8082) is the bulk form, and it is bounded twice rather than trusted: previewDropInjected shows the set before applyDropInjected touches it, and the apply path refuses any record whose status !== "conflict" or whose text does not satisfy isInjectedContextText (:104), so the bulk verb cannot reach a record the narrow predicate does not already describe. Neither auto-resolves: a conflict a person never looks at stays a penalty forever, which is the honest cost of leaving an unvalidated detector's output in the ranking rather than in a filter.

So the neo statuses now split three ways rather than two: the deletion states and demoted withhold, conflict and the trust ladder rank, and the split tracks who or what set the flag. A record a person demoted is gone from the read path; a record an LLM detector flagged as contradicting another is ranked down and can still reach the prompt, carrying its status in the rendered line, which is the difference between a model that can weigh the flag and one that cannot see it.

The exception lives on a different axis. Alongside the neo status is a claim-level epistemic status (lib/epistemic-status.js) — untrusted → observed → corroborated → trusted → disputed → invalidated, explicitly orthogonal to who asserted a memory (origin.trustLevel) and to its numeric confidence. Most of its values are a ranking boost (trusted +0.25 … disputed −0.4), but invalidated is a hard filter, dropped on the read path at recall-pipeline.js:180, given -Infinity at neo-arch.js:1484, and excluded in SQL at db-adapter.js:562 (epistemicStatus != 'invalidated'). It withholds rather than ranks, as demoted now does on the neo axis, and the transitions into trusted and invalidated require an authorized actor, so a memory cannot promote or condemn itself. A conservative merge rule (combineEpistemicStatusForMerge) takes the lower of two inputs, so a weakly-trusted memory cannot launder its way up by being merged with a trusted one.

Which copy of a record the scorer sees is itself a design decision here, and it is the one that makes the rest of the vocabulary mean anything. The JSONL stores are append-only event logs: transitionRecordStatus appends a fresh line under the same id rather than replacing the old one, so a record that has moved to demoted exists on disk twice, once in each state. routeNeoRecall (lib/neo-arch.js:1530) deduplicates by id and keeps the newest revision, ordered by updatedAt — the field a transition sets — while preserving first-appearance order so the itemIndex tiebreak below stays stable. The helper that dates a revision, neoRevisionTimeMs (:1523), carries a note on why the existing recordTimeMs will not serve: it reads startTime/createdAt, which are identical across every revision of one record, so it cannot tell two revisions apart. An undated record sorts to -Infinity so any dated revision beats it and the comparison never lands on NaN.

Read against an append-only store, that is not a detail. Keeping the first line seen means scoring the record as it was before the transition, which would apply the active arithmetic to a record a person had just demoted — and a status penalty computed against the wrong copy is not a weakened penalty, it is no penalty. tests/neo-status-transition-dedupe.test.js fixes the arithmetic to a number: active=0.371 against demoted=-0.116 at a live minScore of 0.08, so the two copies fall on opposite sides of the admission threshold.

The rendered line carries the status the penalty was computed from. formatNeoRecallContext emits lane, category, trust, id, score and status on each <memory-record>, which matters because the memory prompt supplement tells the model to prefer active and promoted over conflicting cards — an instruction that can only be followed if the distinction is in the payload.

One state transition is not epistemic at all and is worth naming here because it protects the whole loop. Text that PLUR1BUS itself injected into a prompt — recall blocks, temporal context, status reminders, cron output — is matched against a marker list and refused as a capture candidate (isInjectedContextText, lib/neo-arch.js:194, applied at neo-arch.js:1229 and :1276). Without it, recall output becomes next turn's memory, which the comment above the marker list dates to a performance analysis on 29 May 2026.

Diagram — supersession is dropped at one line of the recall pipeline, demotion, pruning and tombstoning score -Infinity in the neo scorer, and only conflict is ranked down and still injected, so the state a model sets is the one state that ranks rather than withholds
Diagram source
%% caption: supersession is dropped at one line of the recall pipeline, demotion, pruning and tombstoning score -Infinity in the neo scorer, and only conflict is ranked down and still injected, so the state a model sets is the one state that ranks rather than withholds
stateDiagram-v2
  [*] --> candidate: agent_end capture
  candidate --> active: written to LanceDB
  active --> promoted: person runs promote
  active --> demoted: person runs demote
  active --> conflict: contradiction detected
  active --> superseded: safeUpdate writes v+1
  superseded --> [*]: dropped at recall-pipeline.js 176
  promoted --> pruned: person runs prune
  demoted --> tombstoned: person runs tombstone
  pruned --> [*]: score -Infinity
  tombstoned --> [*]: score -Infinity
  conflict --> conflict: ranked down 0.3, still injected
  demoted --> [*]: score -Infinity

3. Architecture

An OpenClaw v6 plugin, loaded from index.js, requiring Node ≥ 22.5 and a running OpenClaw gateway. Nothing else has to be stood up: LanceDB is embedded, the neo store is JSONL on disk, and the caches use the node:sqlite module built into Node rather than a dependency.

  • index.js (13,306 lines) — plugin entry, hook registration, chat commands, and the wiring between every subsystem below.
  • lib/ (263 files, 78,217 lines) — obsidian-control-room.js (3,954), neo-arch.js (3,500), obsidian-bridge.js (2,360) and recall-pipeline.js (2,094) dominate; safe-update.js, acl-middleware.js, memory-history.js and contradiction-detector.js carry the correction path.
  • lib/jobs/ — fifteen background jobs: daily consolidation, garbage collection, conflict resolution, skill mining, critical-push classification, memory compaction, reflection.
  • lib/dreaming/ — light-dream.js, rem-dream.js, dream-narrative.js.
  • lib/setup/feature-cron-plan.js, scripts/setup-feature-crons.mjs — cron registration through the host's public plugin API; see below.
  • lib/providers/local-model-artifacts.js — the pinned local model artifacts.

Storage. LanceDB tables per agent under {baseDbPath}/{agentId}/ (lib/db-adapter.js), so agent isolation is a directory boundary before it is a query filter. Alongside: eleven JSONL files and four JSON files per workspace in the neo store, an optional SQLite embedding cache and LLM result cache, and an optional Obsidian vault the bridge keeps in sync as Markdown.

Retrieval stack. Vector search over LanceDB with a lexical fallback when the embedder is not ready, graph hydration of neighbouring cards (hydrateGraphResults, recall-pipeline.js:965), and two additive passes — a precomputed semantic lens and conversation reactivation — that append to a recall result and, per AGENTS.md, must never replace it. Both are off by default and both carry a 50 ms hard timeout.

Embeddings come from OpenAI or an optional local @huggingface/transformers. A key is not required to store a memory: the adapter falls back to text search when embedding fails. Every text handed to a local Jina model is cut at embedding.local.maxTokens, default 512 and bounded to 32–8,192 (lib/providers/embedding-local-transformers.js:17,:426-430, pinned by tests/jina-v5-nano-embedding.test.js:178,:210-211), and the cap is part of the shared model pool's identity. The reason is in the comment above the check and in the changelog for 7.11.1: both pinned Jina models accept 8,192 tokens, the ONNX runtime holds the attention working set of a batch's longest text times the batch size and never returns it, and in the 7.11.0 lab run one 15,000-character card drove the v3 process to 41 GB and the kernel's OOM killer at card 808 of 1,031, while a single batch of the eight longest cards took the nano model to +30 GB and 117 s; at 512 tokens the same batch costs +0.6 GB and 5 s on nano, +1.1 GB and 22 s on v3. Until 7.11.1 on 5 September 2026 no cap existed, so an installation with long cards and a local Jina embedder could be killed by its own embedding cron.

Deployment and ergonomics

Install is npm install into an OpenClaw installation, and this is where an operator should look before anything else. package.json declares:

"postinstall": "node scripts/setup-feature-crons.mjs || true"

That script registers the plugin's feature crons and nothing else in the host. Since release 7.5.0 on 3 September 2026 it probes OpenClaw's public plugin capabilities — registerGatewayMethod, registerCli and openclaw/plugin-sdk/gateway-runtime — and builds native cron add invocations against the host's own dispatcher (lib/setup/feature-cron-plan.js); a capability that is missing leaves the job unregistered rather than patched in, and the header still states the install contract — it "must NEVER fail an install" and exits 0 whatever happens. Three committed tests pin the retreat: tests/cron-plugin-direct-dispatch-wiring.test.js asserts the source no longer names the dispatch patch or PLUR1BUS_SKIP_HOST_PATCH, tests/host-patch-skip.test.js asserts the flag is gone, and tests/release-750-compat.test.js asserts package.json's files excludes patches/ and that the patch file does not exist. patches/apply-memory-patches.sh, a set of gateway hotfixes applied from a systemd ExecStartPre in one deployment, shipped in no package and referenced by no installer, stayed in the tree until 5 September 2026, when 6025dbb2 deleted it with its directory; the wiring test asserts that neither the file nor patches/ exists (tests/cron-plugin-direct-dispatch-wiring.test.js:13-14).

Local inference is pinned rather than resolved. lib/providers/local-model-artifacts.js freezes each model — E5, Jina v3, the Jina and BGE rerankers, and from 7.11.0 Jina v5 Text Nano — at an immutable Hugging Face revision with every artifact's path, size and SHA-256, checked before Transformers.js sees a file; the two Jina profiles carry license: "CC-BY-NC-4.0" and assertPinnedModelLicenseAccepted refuses to load them without an explicit operator acceptance. BGE is the wizard's recommended reranker since 7.10.0, and Jina v5 Text Nano is the embedding the wizard and the shell installer propose first for new installs since 7.12.0, each with the lab test's reasoning written into the README; a non-interactive or dry run that cannot confirm the CC BY-NC licence falls back to E5 rather than aborting or accepting it silently (scripts/install-memory-system.sh:772-774, PLUR1BUS_ACCEPT_NONCOMMERCIAL_LICENSE=1 to accept), Jina v3 stays selectable for existing installs, and the dashboard's dimension planner shows a migration notice to any install whose current model is not the recommended one (lib/setup/control-ui-plugin-runtime.js:453-459).

The store is human-readable and repairable by hand: JSONL and Markdown for everything except the LanceDB tables, which is a real operational advantage when a background job has done something unexpected. scripts/repair-installed-plugin.mjs and lib/memory-doctor.js exist for when it has.

4. Essential Implementation Paths

Capture — index.js:9537

api.on("agent_end", …) hands the turn to runtimeScheduler.enqueueCapture(agentId, …) with an abort signal. Capture is per-agent queued and runs after the turn ends, so the model's reply is never waiting on it. Background turns are flagged and treated differently, which is what stops a cron-triggered agent run from writing memories about itself.

Correction — lib/safe-update.js:287

The most carefully built path in the repository, in order:

  1. Refuse non-active rows. A superseded memory cannot be updated; the chain only grows at the leaf.
  2. Validate the ownership tuple before any read or write, requiring the binding that the row's own scope demands.
  3. Demand evidence. validateUpdatePatch throws unless a text or summary change carries both evidence.updateSource and evidence.updateEvidence.
  4. Check idempotency — a SHA-256 over id, source, evidence and the patched fields, looked up in the reconsolidation event log, so a retried correction is a no-op rather than a second version.
  5. Demand a new vector. A text change without patch.vector throws: "The embedding must reflect the new content." This closes the failure where a corrected memory keeps ranking under the old text's query.
  6. Gate on semantic drift (safe-update.js:395) — cosine distance over 0.45 is rejected outright.
  7. Store the new version first, supersede second (:414, :418). The comment is worth reading in full: storing first means a crash leaves both versions active — "a recoverable fork, never a loss" — whereas superseding first would point the old row at an id that was never written.
  8. Rewrite graph edges onto the new id, then append the event with the action, the source, the evidence, the confidence and the measured drift.

The drift gate's two callers — index.js:8995 and lib/jobs/apply-conflict-resolution.js

/correct <old> to <new> runs a confirmation token exchange, then calls safeUpdate with updateSource: "telegram:/correct", an evidence string, and skipDriftGate: true — the one place in the tree outside a test where the flag is set.

The reasoning sits at the call site rather than in a commit message: /correct is a nonce-confirmed user action, the confirmation dialog shows old and new text in the clear, so high semantic drift there is intended and consented to, and the gate would block a legitimate large correction with an exception rather than a warning. The drift is still computed and written onto the reconsolidation event as semanticDrift, so switching the gate off costs the measurement nothing.

The gate does fire on the other caller, which is the automated path it was written for. applyConflictViaSafeUpdate (lib/jobs/apply-conflict-resolution.js) refuses outright unless opts.confirm === true, then calls safeUpdate without the skip flag; when the gate throws "Semantic drift too high" the apply catches it and returns {ok: false, reason: "review_only"} rather than writing. So a conflict resolution the detector rated high-confidence still cannot rewrite a card that has drifted too far from what it replaces — it is downgraded to something a person must look at. That is the shape a drift threshold wants: skipped where a human has confirmed the exact text, enforced where a job proposes one. Consolidation and dreaming still do not call safeUpdate at all, so the gate guards the conflict path and not those.

The confirmation dialog is the part worth copying. Target resolution is fuzzy — candidates are resolved without a minimum score, and "unambiguous" means only that the top match beats the second by more than 0.15 — while safeUpdate replaces the entire text. A prompt naming an 80-character title cannot tell a user which memory they are about to overwrite, so it renders the stored text and the replacement at 300 characters each. The same value carries into provenance: payload.oldText holds the stored content being replaced rather than the search term that found it, and updateEvidence builds its evidence line from that.

Scope — lib/acl-middleware.js:103

checkAccess(ctx, memory) returns {allowed, reason} and denies with a stable reason code on: no context, no memory, an unknown scope value, a requester with no agent id, an invalid or conflicting ownership tuple, a private row with no owner, a workspace row with no workspace, and a user row whose principal is not a user:v1:<sha256> string. Every path that is not an explicit match is a denial.

It is applied on the read path at lib/db-adapter.js:526, :626 and :646 (query, search, get), at lib/recall-pipeline.js:263, in the shared-memory pool, the wiki command, both dream passes, and the Telegram query and edit commands. filterMemoriesByAcl (:226) is the batch form, with optional violation logging to acl-audit.jsonl.

Note that two scope vocabularies coexist: the ACL's agent-private | workspace | user and the neo store's agent_private | workspace_shared | global_user, reconciled by normalizeNeoScope. Neo records are filtered by isNeoRecordAccessible (lib/neo-arch.js:1398) rather than by checkAccess.

Derived records — dreams, episodes, graph edges and patterns — carry a scope on the row and are mostly read without one. Each append stamps a visibility from the writer's binding or the record's own agent and workspace fields (stampDerivedVisibility, lib/neo-arch.js:1420, wired at :1849-1878), and each of the four readers takes an optional requester filtered through isDerivedRecordAccessible (:1452), which shows an unstamped legacy row only to its owning agent. The filter runs only when a requester is passed. The REM-dream pattern read passes one (lib/dreaming/rem-dream.js:1221); the graph-edge and episode reads in index.js, the edge rewrite in lib/safe-update.js:227, reactivation (lib/conversation-reactivation-recall.js:923) and the pattern match on the recall path (index.js:12551) pass none and read the workspace's whole file.

The dream reader shows the failure direction of a fail-closed ACL, and it is worth recording because it is the opposite of a leak. The REM-dream candidate loader built its scope partition as user or workspace only — never agent-private — so every agent-private candidate was rejected by the partition match. On stores where the week's candidates were all agent-private (measured at 70/70 and 49/49 on two live agents), the job permanently reported too_few_memories and did nothing: a correct filter handed the wrong partition produces zero output rather than an exposure. buildRemPartitions (lib/dreaming/rem-dream.js) now runs every sensible partition, agent-private first, with per-partition dedup and vault files; a committed test asserts the old workspace-only partition returns null candidates against the same LanceDB table. The per-card ACL was also extended to the /critical review surface (lib/critical-review.js), which previously gated only on a destructive-channel check while returning every critical card of the agent.

Status transitions, and a dedupe key that had to be designed

The neo store is append-only JSONL with an id index, and appends are deduplicated. That creates an obvious hazard: transitionRecordStatus (lib/neo-arch.js:1372) returns the same record with a new status, the same id and a fresh updatedAt, so an id-keyed dedupe would silently swallow every promotion. It does not, and the reason is three small key functions at lib/neo-arch.js:2305-2330:

function appendDedupeId(record) {
  if (!record || typeof record !== "object" || !record.id) return "";
  if (record.updatedAt || record.embeddingUpdatedAt) return "";
  return String(record.id);
}

function recordStatusTransitionDedupeKey(record) {
  if (!record || typeof record !== "object" || !record.id || !record.updatedAt) return "";
  const status = normalizeNeoStatus(record.status, "");
  if (!status || status === "candidate") return "";
  return `status:${record.id}:${status}:${record.updatedAt}`;
}

function appendCandidateContentDedupeKey(record) {
  if (!record || typeof record !== "object") return "";
  const statusKey = recordStatusTransitionDedupeKey(record);
  if (statusKey) return statusKey;
  const key = recordDedupKey(record);
  if (!key) return appendDedupeId(record);
  return `content:${stableHash("candidate-content", key)}`;
}

Which key applies depends on the file. Turns, reactions and behaviour cards use appendDedupeId, the default in appendJsonlDedupe (:2334): a fresh record is keyed by bare id, and a record carrying updatedAt gets no key and always appends, so a transitioned behaviour card is never swallowed. The candidates file passes appendCandidateContentDedupeKey (:1828), and that is where the promote, demote, prune and tombstone commands write (index.js:8825-8826). A transition there is keyed on id, new status and updatedAt; a record still in candidate is keyed on a hash of its normalized statement (recordDedupKey, :1655), so a second capture of the same sentence is dropped while a transition is never mistaken for one. The residual gap is millisecond-wide: two transitions to the same status within the same updatedAt collapse into one.

Recall

lib/recall-pipeline.js runs lifecycle filtering, ACL filtering, namespace merge with canonical-content dedupe, importance boost, Jaccard dedupe at 0.78, and graph hydration, emitting a decision trace throughout (lib/recall-decision-trace.js) and a retrieval ledger entry per query. /memory <query> --explain renders the trace back to the user, so "why was this memory shown" is answerable without reading logs.

Tests covering the behaviour

tests/crr-status-filter.test.js is the sharpest one: it constructs a superseded and an active memory with identical text, runs the reactivation selector, and asserts the superseded one is absent from the block that gets injected into the prompt. The header names the regression it locks — reactivation reached the semantic lens without a status filter, so a corrected memory could resurface as current evidence.

5. Memory Data Model

A LanceDB card, from buildUpdateEntry (lib/safe-update.js:80), carries roughly fifty fields. The ones that matter:

  • Identity and lineage — id, versionNumber, previousVersion, supersededBy, status, versionCreatedAt.
  • Provenance — sourceTurnId, sourceMessageRole, sourceTimestamp, sourceUrl, evidenceQuote, updateSource, updateEvidence, reconsolidationConfidence. This is typed provenance in columns, not a metadata blob, and it is the part most systems in this atlas skip.
  • Ownership — agentId, storedBy, workspaceId, workspaceKey, scope, ownerUserId.
  • Dynamics — importance, memoryStrength, halfLifeDays, lastStrengthenedAt, retrievalCount, lastRetrievedAt, replayCount, memoryClass, neverForget, coreMemoryScore.
  • Affect — emotionalValence, emotionalIntensity, emotionalDominant, moodContextAtCapture.

neverForget and memoryClass: "core" are honoured by the garbage collector (lib/garbage-collector.js:87), which is a small thing that many decay implementations forget: a decay curve with no pin will eventually reach the memories the user cared most about.

Two fields the data model previously lacked are now first-class, and both are the value-keyed kind the atlas keeps asking for:

  • A validity window. validFrom and validUntil (lib/db-adapter.js:447) record when a claim was true in the world, 0 meaning "no known bound" rather than the epoch. They are separate from createdAt/updatedAt, and the file header states the separation outright: they are "the REAL-WORLD validity window of a claim … independent of and orthogonal to … System Time." Recall threads an optional validAt instant through every chokepoint — isEntryValidAt does a left-inclusive/right-exclusive validFrom <= validAt < validUntil, pushed down to the vector store as SQL — and the memory_recall tool exposes it directly ("restrict recall to facts valid at this specific point in time … 'where did he work in 2025'"). Historical facts are sibling rows with disjoint windows, not edits to a version chain, so validity time and record time are finally two different questions. Validity is caller-supplied only, never guessed from text: a vague phrase resolves to 0/unknown rather than a fabricated date.
  • A rejected-value tombstone. /forget soft-deletes the row (tombstoneCard, db-adapter.js:702 — status="deleted", epistemicStatus="invalidated") and writes a durable tombstone keyed on a content fingerprint, a SHA-256 of the NFKC-normalized text and never the plaintext (lib/tombstone.js:69), to an append-only registry that survives restore, migration and re-embedding. findBlockingTombstoneForCapture runs as step zero of both capture callsites (index.js:6411, :10667) and refuses a re-store of the forgotten value; corrupt or unreadable registry lines fail closed. This is the value-keyed mechanism the rejected-value tombstone pattern describes, and it supersedes the record-keyed tombstoned neo status as the thing that keeps a forgotten value gone.

What remains absent:

  • A requester on the derived-record reads. Dreams, episodes, graph edges and patterns carry a stamped visibility, but only the REM-dream pattern read passes a requester to the filter; the others read the workspace's whole file (section 4).

6. Retrieval Mechanics

Automatic injection on the turn, plus explicit /memory and plur1bus_recall tool access.

Ranking blends vector similarity, lexical overlap, the importance boost, category lane matching, and the trust and status arithmetic quoted in section 2. Results are deduplicated twice — by canonical content key across namespaces, then by Jaccard similarity at 0.78 — which matters in a system that keeps every version of a memory, because a v3 and a v4 of the same fact are near-identical text.

Two additive passes sit after primary recall and are architecturally constrained rather than merely documented: the semantic lens reads a precomputed index and appends community, bridge and faded memories; conversation reactivation appends a <memory-reactivation> block on an idle gap or after compaction. Both cap their output (three memories, one faded, three open threads), both time out at 50 ms, both fall back to the unmodified base recall, and neither writes anything.

The failure mode to watch is over-recall by construction. Base recall, plus graph hydration of neighbours, plus lens, plus reactivation, plus temporal context, plus emotional state, plus persona voice all target the same prompt. The caps are per-feature and there is no global token budget across them.

lib/temporal-provenance.js is the interesting counterweight and is unusual enough to name. It classifies a recalled memory by age and by whether its content is operational — cron, systemctl, deploy, gateway, migration — and by destructive keywords, then decides whether the agent must verify live before acting on it. A memory that a cron job is disabled is treated as a fact about the past rather than the present after fifteen minutes. That is the right shape for the class of memory that gets an agent into trouble, and no other system in this atlas conditions action on the age of the specific memory being acted upon.

The guard is only as good as the timestamp reaching it, and the timestamp is produced by the mapping layer rather than by the store. Canonical hits from KNOWLEDGE.md carry no createdAt of their own and take the file's mtime as their age, and they are marked authoritative and exempted from the operational guard on the reasoning that a canonical document is the reference something else is verified against. Semantic-lens hits copy createdAt, updatedAt and lastRetrievedAt off the underlying entry, and the reactivation block renders age and freshness rather than omitting them. parseMemoryTimestamp discards a value outside the representable Date range the way it discards a missing one, which keeps buildTemporalProvenance from throwing a RangeError and taking the whole recall rendering with it — the age label is therefore always unknown or <n>[mhd] ago, which is what the reactivation renderer assumes.

7. Write Mechanics

Writes are created by the agent_end capture, by explicit tool and command use, by the Obsidian bridge importing vault edits, and by background jobs.

Conflict handling is split. lib/contradiction-detector.js asks an injected LLM whether two interpretation overlays of the same memory are mutually incompatible and persists findings to contradictions.jsonl; lib/memory-text-contradiction.js and lib/jobs/conflict-resolver.js cover the card text. The output is a conflict status and a listing under /plur1bus curation conflicts — a queue for a person, not a resolution.

Malicious input is filtered at capture. PROMPT_INJECTION_RE (lib/neo-arch.js:106) matches the familiar overrides plus chat-template delimiters, and a turn marked quality.promptInjectionSuspected is excluded from the recallable set at neo-arch.js:1276. The injected-context marker list discussed in section 2 closes the self-capture loop. lib/relevant-memory-context.js prepends a recall safety preamble telling the model that memory content is data.

Operational cost

  • The write path is deferred. Capture runs after agent_end through a per-agent scheduler; the agent never blocks on extraction.
  • The lag before a memory is retrievable is capture-queue depth plus an embedding round-trip, and it is not measured anywhere in the repository. Embeddings are queued through embedding-queue.jsonl and drained by a cron, so a memory can be lexically retrievable before it is vector-retrievable — an interval nothing bounds.
  • Background passes rewrite broad slices of the store. Daily consolidation, memory compaction, memory-dynamics maintenance, GC, skill mining, REM dreaming (weekly) and light dreaming each read and write in bulk, and their token bill scales with the corpus rather than with the day's traffic. Most default off; scripts/setup-feature-crons.mjs registers only those explicitly enabled.
  • Read-path injection is bounded per feature and not in aggregate. Recall blocks, temporal context, reactivation and emotional state each carry their own cap. All of them are injected as a per-turn prefix, which will invalidate a provider's prompt-prefix cache on every turn in which any of them changes.

8. Agent Integration

The plugin registers commands (/memory, /forget, /correct, /state, /enable, /disable, and a /plur1bus namespace covering curation, memory, behaviour, dreaming, skills and reminders), an agent_end hook, gateway start/stop lifecycle hooks, and MCP-style tools for explicit recall.

The model has more agency than the command list suggests, and this is where the 2026-09-19 re-read withdrew human_review. Promotion, demotion and pruning are authorized human commands: isDestructiveAction (index.js:7123) routes them through checkAuth(..., { destructive: true }), which under isAuthorized (lib/security.js:89) demands a userId on the allowlist, or — with no allowlist configured — a private 1:1 chat, refusing groups and unknown chat kinds. That is a real actor check on the channel, and none of those verbs appears on the model's tool surface.

Tombstoning does. api.registerTool (index.js:11136) declares four tools, and one of them is memory_forget (index.js:11574), which resolves a card by id or query and calls tombstoneMemoryWithAudit — no confirmation token, no nonce, no chat. It is scope-checked (checkAccess on every hit, with a deliberately identical "No matching memory found" for a denial so the error is not an existence oracle) but not actor-checked. The only gate is security.allowModelDestructiveMemoryOps, read as !== false (index.js:11139) and so on unless explicitly disabled; tests/config-audit.test.js:424 pins that default. The refusal text the flag unlocks states the problem exactly: "model-facing tool calls do not carry a user-bound authorization context." The repository's own July security scan reached the same finding, and it is still open at this pin.

The second tool, knowledge_update (index.js:11717), drains the knowledge-promotion queue the capture path fills. trackKnowledgePending (index.js:4005) queues a memory for curation into KNOWLEDGE.md; once three are waiting, <knowledge-update-reminder> is injected into the model's own prompt (index.js:3242-3252) telling it to call the tool. A queue whose only drain is the producer, prompted by the producer's own context, is not a review.

The critical-review surface grew a bulk form and a claim on the host's dispatch. /plur1bus critical accept all, reject all and several references in one command work every pending review in the authorised scope; a quoted reply to a push — "accept all", "alle ablehnen" — is answered on the host's before_dispatch and before_agent_reply hooks before the agent sees it (index.js:9901-9960), with the references taken from the quoted push's fixed header, the decision from the reply, and the same destructive authorisation as the command; groups are refused and anything ambiguous falls through to the agent. A reject never deletes: markCriticalRejected (lib/db-adapter.js:1112) sets confirmed = 1, type = "note". What the review does not do is hold anything back. confirmed is written by three functions — markConfirmed, markCriticalAccepted, markCriticalRejected (lib/db-adapter.js:1070, :1089, :1112) — and read by exactly one, findUnconfirmedCritical (:971), which builds the pending list. No recall, injection, scoring or dream path consults it; lib/recall-pipeline.js:150 carries the column through and nothing downstream branches on it. An unconfirmed critical card is recalled like any other. And the queue drains itself: autoAcceptStale (lib/jobs/auto-accept-stale-criticals.js:14) marks every card unconfirmed for more than 24 hours as confirmed, and the shipped cron plan registers it daily at 04:50 Europe/Berlin under the same criticalPush switch that creates the cards (lib/setup/feature-cron-plan.js:63-77, :331-335). The push is a notification with an acknowledgement ledger — useful, and not a gate. The operator dashboard is read-only unless controlUi.writeActions is set — off by default, reranker for a runtime switch, all for the embedding target and the re-embedding migration — with a single-use form token per page load and a nonce-bound CSP (lib/setup/control-ui-write.js). Light and REM dream narratives are written into the workspace DREAMS.md inside the host's managed block and announced as a memory.dream.completed event (lib/dreaming/dream-diary.js).

The Obsidian bridge is the second integration and the more unusual one. Memory is mirrored into a vault as Markdown, a person edits or annotates it there, and the bridge syncs changes back — under an explicit stance stated at the top of lib/obsidian-control-room.js: PLUR1BUS stays authoritative, vault text is untrusted input, and apply never mutates memory without explicit approval plus immediate revalidation. Deleting a vault file does not delete a memory; it raises an approval_required_tombstone action (lib/obsidian-bridge.js:1553).

A write into a vault needs more than the bridge's approval flow: lib/obsidian-vault-authority.js binds a confirmation receipt to the agent, the workspace pool and a SHA-256 of the vault's real path under <baseDbPath>/.plur1bus-authority/obsidian-vaults/…, and obsidianServiceMutationPolicy (index.js:6854) requires that receipt plus an action confirmation, with scheduled discovery carrying an explicit plan.

Adapting this to another agent host would be substantial work. The plugin is written against OpenClaw's plugin API, its cron dispatch, its agent workspace resolution and its command registration.

9. Reliability, Safety, and Trust

Strengths:

  • Correction demands evidence — a source and a quote — and refuses a text change without a matching new embedding.
  • Write ordering is reasoned about explicitly, with the crash window named in a comment and resolved in favour of a recoverable fork.
  • Idempotent corrections via a hash checked against the event log.
  • An append-only reconsolidation event log carrying action, source, evidence, confidence and measured drift.
  • A fail-closed ACL on the read path, whose tests assert denials.
  • Recall output cannot become capture input, closing a feedback loop the project traces to a dated performance analysis.
  • Prompt-injection suspicion excludes a turn from recall, rather than only logging it.
  • Age-conditioned action guards for operational memories.
  • Pins survive decay — neverForget and memoryClass: core are honoured by the GC.
  • A repairable store — JSONL and Markdown, plus a doctor and a repair script.
  • The scorer reads the newest revision of an append-only record, so a status a person set is the status the ranking arithmetic uses.
  • A correction dialog that shows what it will overwrite, at 300 characters of old and new text, against a target resolved by fuzzy match.
  • A validity window separate from record time, queryable as-of, so a corrected fact preserves the period the old value was in force rather than erasing it.
  • A value-keyed tombstone that blocks re-capture of a forgotten sentence, keyed on a content fingerprint and never the plaintext, and gated on a binding audit — /forget fails if the audit record cannot be written.
  • One doubt state that withholds. The epistemic invalidated status is a hard filter at all three read layers, and transitions into trusted/invalidated require an authorized actor, so a memory cannot condemn or promote itself.
  • A direct chat resolves to an identity-bound context. resolveSessionOwnerMemoryContext (lib/memory-request-context.js:730) turns a direct-chat session key into the user, channel, account and conversation principal the chat commands see, while main, heartbeat, cron and group sessions keep the agent-and-workspace context; shared workspace and user pools are named from host configuration and never expose the user identifier.
  • Pinned model artifacts — revision, size and SHA-256 per file, and a licence acceptance gate on the CC BY-NC models.

Gaps:

  • Contradiction does not withhold. conflict and the trust ladder remain ranking arithmetic — conflict a 0.3 penalty at lib/neo-arch.js:1503 — so a memory the system records as contradicted can still reach the prompt labelled with its status, moving the decision to the model. demoted and the epistemic invalidated state withhold; the status that flags contradiction does not.
  • The drift gate is skipped on the confirmed human correction and enforced on the automated conflict apply, where exceeding it downgrades the write to a review rather than blocking with an exception.
  • Derived records are read unscoped by most callers. Dreams, episodes, graph edges and patterns carry a visibility and their readers accept a requester, but only the REM-dream pattern read passes one; the pattern match on the recall path (index.js:12551) reads the workspace's whole file.
  • A tombstoned card reached a push. Until release 7.9.2 on 5 September 2026 the classify-recent cron treated rows removed by memory_forget — type still memory, status deleted — as fresh candidates, typed them and named them in a critical push, while the review path read only active rows and could not resolve the references; findRecentUnclassified (lib/db-adapter.js:894) pushes an active-status clause into the LanceDB query and runClassifier reports what it skipped as skippedInactive (lib/jobs/critical-classifier.js:111-138), with tests/critical-classifier-double-push.test.js covering it. The tombstone held at the capture chokepoints and leaked at a read path nobody had listed, which is the shape a value-keyed tombstone's remaining risk takes.
  • Merging is proposal-only, and nothing reads the proposals. With merging.autoApply at its default false, executeActions (lib/jobs/memory-compaction.js:1104) executes nothing and calls persistProposals, which appends one {proposedAt, actions, aclBindings, status: "pending"} line per run to .adaptive-learning/merge-proposals.jsonl (:1002). Searching the tree for that filename finds the writer, three tests that assert the file was written, and two README lines — no reader, no command, no job. The documented intent, "never auto-applies", is met; the unstated half is that the duplicates the daily job detects are never merged by anything, and the ledger grows.
  • The model can tombstone without a person. memory_forget is a declared tool with no confirmation exchange, enabled unless security.allowModelDestructiveMemoryOps is set to exactly false, and the refusal text behind that flag names the reason it should not be: model-facing tool calls carry no user-bound authorization context. This is the finding that withdrew human_review; the detail is in section 8.
  • postinstall still cannot fail by contract, though what it does is now bounded to the host's public API.
  • A prompt-facing value can be produced by four different mapping paths, and correctness has to be established at each one rather than at the store.
  • The feature surface is the risk. Fifty config groups, fifteen background jobs and two dream passes over one memory store means the number of paths that can write to a card is large, and only one of them goes through safeUpdate.

10. Tests, Evals, and Benchmarks

The memory tests need no framework: npm test is node --check on selected modules followed by node --test over tests/ and test/, and the regression files import only node builtins and in-tree modules, so they run without the @lancedb/lancedb install the screen refuses. At the previous pin three such files were executed (27 passing across 7 suites) with a negative control — restoring lib/neo-arch.js from the older pin failed 5 of 7 in the dedup file, confirming the tests discriminate rather than merely pass. At this pin the dependency surface was again inside the seven-day cooldown, so nothing was installed or run; the new behaviour was read from the source and its committed tests.

472 test files under tests/ and test/ — larger than the implementation, and the twelve largest additions since the previous pin are the new subsystems: valid-time.test.js (1,721 lines), epistemic-status.test.js (974), tombstone.test.js (509), and a family of tombstone-* files covering torn writes, the registry cache, scope, query recovery and the forget scripts.

What is covered, by name: ACL call-site adapters and ownership binding, shared-memory recall and the share store, sensitive-read authorization, safe-update data loss, the DB adapter's updateCard data loss, dedupe and status-filter regressions, contradiction detection across four files, the embedding cache, the LLM result cache, cron bootstrap and the direct-dispatch patch, GC's neverForget guard, Obsidian command gating, vault confirmation, review authority, and zero-mutation guarantees.

The negative assertions are real and specific, and the new subsystems widen them. tests/crr-status-filter.test.js asserts a superseded memory must not reach the reactivation block; semantic-lens-status-filter.test.js asserts an invalidated memory is not surfaced by the lens while a trusted one still is, so it proves discrimination rather than blanket suppression. tombstone-e2e.test.js asserts a re-store after /forget returns tombstone_blocked and that /forget fails if its binding audit cannot be written; correct-tombstone-guard.test.js asserts a tombstoned card is not revived by a correction (updateCard call count 0). tombstone.test.js asserts no plaintext lands in the tombstone and that only a committed tombstone blocks — a failed or merely attempted one does not. tests/b13-acl-callsite-adapters.test.js asserts an unbound private row, a conflicting ownership tuple, and a raw user id in place of a canonical principal all fail closed; tests/gc-neverforget-guard.test.js asserts pinned memories are not archived. On the bitemporal side, valid-time.test.js pins the right-exclusive boundary, the BigInt-zero open-window sentinel, and that createdAt/updatedAt are never read as validity bounds.

What is missing is quality measurement. There is no retrieval-quality eval, no benchmark harness, and no committed result for any of it — which for a system whose ranking function sums seven weighted terms means the weights in scoreNeoRecallItem are unvalidated by anything in the repository. No paper, arXiv reference or citation file exists in this tree; the documentation is a 51 KB README, an 87 KB changelog, and a 97 KB how-to-memory-perfect.md.

Three tests pin the removal of the host patch and one pins the classifier's tombstone respect, and they are the right shape — each asserts an absence against the source or the package manifest with a positive control beside it: tests/cron-plugin-direct-dispatch-wiring.test.js, tests/host-patch-skip.test.js, tests/release-750-compat.test.js and tests/critical-classifier-double-push.test.js.

tests/neo-status-transition-dedupe.test.js is worth reading for its shape as much as its subject: it pins the arithmetic to numbers — active=0.371 against demoted=-0.116 at a live minScore of 0.08 — so the assertion is about which side of the admission threshold each copy falls on, not about an ordering that a weight change would silently invert.

The test I would want before trusting this in production is now partly present: tombstone-e2e.test.js asserts a store→forget→re-store is blocked and correct-tombstone-guard.test.js blocks correcting a tombstoned card. What is still not asserted end to end is that every internal compaction and dream write routes through findBlockingTombstoneForCapture — the guard runs at the capture and correct chokepoints, and whether the bulk background passes all pass through it is untested.

11. For Your Own Build

Steal

  • Require evidence for a content change. A source and a quote, validated at the function boundary, turns "the model decided to update this" into a record you can audit later. It costs two required arguments.
  • Require a new embedding with new text. A corrected memory that keeps its old vector ranks under the old query forever, and nothing about it looks wrong.
  • Store the replacement before superseding the original, and write down why. The crash window is real, the comment at safe-update.js:411 is the artifact, and the resulting failure — both versions active — is one a person can fix.
  • Key an idempotency hash on the correction, not the record, so a retried correction cannot fork a version chain.
  • Make recall output unrecapturable. Marker-match your own injected blocks and refuse them as candidates. The failure this prevents is a store that inflates on its own output.
  • Condition action on the age of the specific memory. For operational facts — a service state, a deploy, a cron — "recalled and recent" is a different authorization than "recalled".
  • Design the append-key before making a log append-only. A status change carries the same id as the record it changes; an id-keyed dedupe would eat it silently.
  • Pin memories out of decay. A neverForget flag the GC honours costs one condition and saves the memories a decay curve is worst at keeping.

Avoid

  • Discrete trust states wired to a score. If conflict is a 0.3 penalty, the system cannot refuse to act on a contradiction — it can only be slightly less enthusiastic. Decide which states filter and which rank, and make the filtering ones filter.
  • A safety gate with no live caller. A threshold every caller disables is one nobody is maintaining, and it reads as protection in the schema. The resolution worth copying is the one here: keep the skip where a human confirmed the exact replacement text, enforce it where a job proposes one, and convert the exceeded gate into a review outcome instead of an exception the caller has to catch.
  • Deduplicating an append-only log by first appearance. If a state change appends rather than replaces, the first copy is the pre-change one, and every consumer that keeps it is reading the record as it was before the decision. Pick the newest revision by a field the transition actually sets, and check that the field differs between revisions before relying on it.
  • Instructing a model to weigh a distinction your renderer omits. A prompt supplement that says prefer active over conflicting needs the status in the payload; otherwise it is an instruction the model has no way to follow and no way to report it cannot.
  • Patching your host at postinstall. However well-tested and however necessary, an install step that rewrites another package's shipped code — and cannot fail by contract — is a support burden and a supply-chain surface. The way out here is worth copying too: probe the host's public capabilities, use its own dispatcher, and let a missing capability leave the feature unregistered rather than patched in.
  • Per-feature injection caps with no global budget. Seven bounded features can still fill a context window.

Fit

This suits one specific reader: someone running OpenClaw for themselves or a small team, who wants a memory that grows and is willing to operate it. The feature surface assumes a maintainer who enjoys the surface — dreaming, emotional state, persona voice and an Obsidian vault are not incidental extras, they are most of the product, and someone who wants only "the agent remembers what I told it" will be configuring their way out of features for a while. Defaults help: most of the elaborate machinery ships off.

Walk away if you need multi-tenant guarantees. The read-path ACL is good, but most readers of derived records pass no requester, background jobs write across the store, and the failure mode of a scope gap is unrecoverable. Walk away if you cannot accept a postinstall that registers crons in your host and cannot fail by contract.

The part worth taking whatever you are building is lib/safe-update.js. It is 480 lines, has one dependency on the rest of the system, and is the most complete answer in this atlas to "what does it take to change a memory without losing the old one".

12. Open Questions

  • What is the lag between capture and vector-retrievability under a real embedding cron? Nothing in the tree measures it.
  • Do the seven weights in scoreNeoRecallItem come from measurement or from judgement?
  • Does a consolidation or dream pass resurrect the text of a corrected memory into a new card? The capture and correct chokepoints now consult the content-fingerprint tombstone registry, but whether every bulk background write routes through that check is not asserted.
  • How much does the full feature set inject per turn in aggregate, and at what point do the per-feature caps collectively exceed a sensible budget?
  • Which other read paths besides classify-recent touch cards without the active-status clause? The 7.9.2 fix listed one; nothing in the tree enumerates the rest.
  • How many other prompt-facing fields are produced by more than one mapping path, and is there a check that all of them agree?

Appendix: File Index

  • Correction and versioning: lib/safe-update.js, lib/memory-history.js, lib/memory-merge-safety.js.
  • Scope and access: lib/acl-middleware.js, lib/memory-request-context.js, lib/security.js, lib/sql-safety.js.
  • Epistemic states, neo store, injection guards: lib/neo-arch.js, lib/epistemic-status.js.
  • Validity time (bitemporal): lib/valid-time.js, validFrom/validUntil columns in lib/db-adapter.js, validAt recall parameter in index.js.
  • Rejected-value tombstone: lib/tombstone.js, lib/registry-lock.js, scripts/reapply-tombstones.mjs, scripts/repair-tombstones.mjs.
  • Retrieval: lib/recall-pipeline.js, lib/recall-decision-trace.js, lib/semantic-lens-index.js, lib/conversation-reactivation-recall.js, lib/relevant-memory-context.js.
  • Storage adapter: lib/db-adapter.js, lib/multi-namespace-pool.js, lib/shared-memory.js.
  • Contradiction and overlays: lib/contradiction-detector.js, lib/memory-text-contradiction.js, lib/interpretation-overlay.js, lib/overlay-generator.js.
  • Decay, GC and dynamics: lib/memory-dynamics.js, lib/garbage-collector.js, lib/temporal-provenance.js.
  • Human surfaces: lib/obsidian-control-room.js, lib/obsidian-bridge.js, lib/obsidian-mutation-policy.js, lib/obsidian-review-authority.js, lib/critical-review.js, lib/telegram-commands/.
  • Background work: lib/jobs/, lib/dreaming/, lib/runtime-scheduler.js.
  • Cron registration through the host's public API: scripts/setup-feature-crons.mjs, lib/setup/feature-cron-plan.js; no patches/ directory exists, and tests/cron-plugin-direct-dispatch-wiring.test.js asserts it.
  • Critical review: lib/critical-review.js, lib/critical-reply-intent.js, lib/jobs/critical-classifier.js.
  • Local model pinning: lib/providers/local-model-artifacts.js. Dashboard writes: lib/setup/control-ui-write.js. Vault authority: lib/obsidian-vault-authority.js. Dream diary: lib/dreaming/dream-diary.js.
  • Tests cited: tests/crr-status-filter.test.js, tests/b13-acl-callsite-adapters.test.js, tests/gc-neverforget-guard.test.js, tests/safe-update-dataloss.test.js, tests/valid-time.test.js, tests/tombstone-e2e.test.js, tests/correct-tombstone-guard.test.js, tests/semantic-lens-status-filter.test.js, tests/rem-dream-acl-partition.test.js, tests/cron-plugin-direct-dispatch-wiring.test.js, tests/host-patch-skip.test.js, tests/release-750-compat.test.js, tests/critical-classifier-double-push.test.js.

History

2026-10-01 — c381fd57… — audited at the same commit; six marks stand. The dedupe section said a candidate is never deduplicated: appendCandidateContentDedupeKey (lib/neo-arch.js:2322) keys a candidate by a hash of its statement and a transition by id, status and updatedAt, and the section now quotes it with the bare-id appendDedupeId default. The derived-record passages cited a comment, absent at this pin, saying dreams, episodes, edges and patterns carry no scope; they are stamped with a visibility and filtered only when a caller passes a requester, which most do not. The diagram still showed demoted as ranked and injected, against the -Infinity the scorer returns. Anchors in neo-arch.js and recall-pipeline.js re-mapped; the Fit paragraph no longer describes the removed host patch. Read, not run.

2026-09-25 — c381fd57… — census re-measured at the same commit from a depth-1 fetch, read and never run. package.json and openclaw.plugin.json both declare 7.12.61. index.js counts 13,306 lines; lib/ holds 263 files and 78,217 lines; lib/safe-update.js counts 480. The tree at this pin holds 472 *.test.js files, 459 under tests/ and 13 under test/; the trees API gives 436 at 6317fd9, where section 10's figure and the file index's 254 lib/ files came from. The summary, section 10, the file index and section 11 carried figures from earlier pins and now state these. No mark moved.

2026-09-19 — re-pinned to c381fd57…, release 7.12.61, eight commits on. human_review is withdrawn; six marks stand. The previous record read "/correct and /forget require a confirmation token bound to the resolved target, the /critical review surface carries a per-card ACL, and an Obsidian vault mirrors cards for review outside the chat." Each clause is true of the chat surface and none of them survives the producer test, which asks whether a memory waits in a state until an actor the producing agent cannot be resolves it. The confirmation token guards the chat commands; the model's own memory_forget tool (index.js:11574) tombstones a card with no token, gated only by security.allowModelDestructiveMemoryOps, which is read as !== false and pinned to true by tests/config-audit.test.js:424 — and the block message the flag unlocks says why that matters. The /critical ACL is real, but confirmed is read by one function that builds the pending list and by no read path, so nothing is withheld while it is pending, and autoAcceptStale confirms every card older than 24 hours on a daily cron the shipped plan registers under the same switch. The Obsidian vault carries human edits into memory, which is the opposite direction from a review gate. Found in the same pass and now in section 9: merge-proposals.jsonl is written by the compaction job and read by nothing in the repository, so proposal-only merging means the detected duplicates are never merged. The destructive-command actor check in lib/security.js:89 is unusually good and is described rather than marked. Line anchors re-mapped across index.js (12,418 to 13,306 lines) and lib/db-adapter.js. Screened again before reading: six files, no auto-run surface, one build-time execution point, three dependency files inside the cooldown, an agent-addressed instruction file recorded as data. Nothing installed, built or run.

2026-09-16 — 75e1c4c6… — re-read at a commit dated 12 September 2026, 82 commits past the previous pin, at release 7.12.57. All seven marks re-tested and unchanged, and the files they rest on are unchanged with them: lib/epistemic-status.js, lib/tombstone.js, lib/valid-time.js and lib/acl-middleware.js are byte-identical to the previous pin, so the six-value status with its actor-tiered transition matrix, the content-fingerprint tombstone consulted at capture, the validity axis and the ACL gate all stand as described. The growth is storage plumbing rather than mechanism: lib/neo-arch.js gained about a thousand lines of vector sidecar — a versioned index, float32 buffers, batched and lazily-attached reads — alongside stale temp-file cleanup and persisted cap-recheck state. Screened before reading: six files scanned, no auto-run surface, one build-time execution point, one unpinned dependency surface and three dependency files inside the seven-day cooldown. An agent-addressed instruction file was recorded as data. Nothing was installed, built or run.

2026-09-07 — 6317fd99… — re-pinned six commits on, release 7.12.1. The range is the scoped-embedding IPC owner election on macOS (lib/providers/scoped-embedding-ipc.js), two macOS CI workflows, two audit notes and their tests; no file under the recall, correction, tombstone, ACL or reconsolidation paths changed. Seven marks stand on the same evidence. Screened before reading: no auto-run surface, two manifests inside the seven-day cooldown, nothing installed or run.

2026-09-06 — 6025dbb2… — re-pinned at the head of main, three commits past 7.11.0, prompted by the maintainer's comment on the same issue. Screened again: the same postinstall, both manifests inside the seven-day cooldown, one unpinned surface behind the lockfile, an AGENTS.md treated as data; nothing installed or run. The rewritten history, accounted for. The maintainer states that on 5 September 2026 every branch and tag was rewritten with git filter-branch --msg-filter to strip commit trailers carrying session links, that every hash changed, and that the trees are identical commit for commit. Checked from here: the previous pin 3479373f… can be fetched from GitHub by its full hash, its tree is dd8c73c8…, and so is the tree of 533fa93a…, the commit v7.4.0 points at — the code this report read on 17 and 18 August is byte-for-byte the code under the new hash, and that commit's own message is unchanged, its hash differing only because its ancestors' messages do. No commit message on main carries a session trailer. Three commits since the pin, all dated 5 September: 7.11.1 caps every text sent to a local Jina model at 512 tokens after a lab run in which an uncapped 15,000-character card drove the v3 process to 41 GB and the OOM killer — a defect present at 7.11.0 and at every pin before it, described in section 3 with the numbers from the changelog; 7.12.0 makes Jina v5 Text Nano the installer's first proposal for new installs with an E5 fallback when a non-interactive run cannot accept the licence, and adds a migration notice to the dashboard; and 6025dbb2 deletes patches/apply-memory-patches.sh and its directory, turning the wiring test's does not run a host patch into no patch script exists. Marks unchanged at seven; index.js and lib/ outside the provider and setup files are untouched, so every line citation in the body holds.

2026-09-05 — 399bfba7… — re-pinned at release 7.11.0, prompted by the maintainer's request in neoneye/agent-memory-atlas#20 for 7.10.0 (b4138df5…); the head of main is one release on, adding the pinned Jina v5 Text Nano artifacts. The previous pin no longer resolves from the default branch: git cat-file -t 3479373f87dc8f70d460d09ddeb20ffb83355231 fails in a fresh clone, GitHub still serves the commit, and the v7.4.0 tag now points at 533fa93a…, dated two hours after it — the history was rewritten below the tag, so the commit count between pins cannot be stated. Screened again: the same postinstall, both manifests inside the seven-day cooldown, four floating ranges behind the lockfile, an AGENTS.md treated as data; nothing installed or run, and every claim below was read from the source and its committed tests rather than from the issue. Marks unchanged at seven. Every line citation in the body was re-verified and re-mapped, because index.js grew from 10,867 to 12,418 lines. Corrected as stale rather than wrong: the host patch and its skip flag were removed in 7.5.0, and the three tests that pin the removal are named in section 3; the configuration count is fifty; the matrix's "drift gate still has no live caller" contradicted the 17 August entry below and is rewritten. New and described: the classify-recent cron's leak of tombstoned cards into critical pushes, fixed in 7.9.2; the bulk and quoted-reply forms of critical review claimed on the host's dispatch hooks; identity-bound context for direct chats; pinned local model artifacts with a licence gate; the dashboard's opt-in write actions; vault confirmation receipts; dream narratives on the host page. Each row of the issue's table was checked against the file it names and held; the two "still true" lines — conflict at 0.3 (lib/neo-arch.js:1483) and /correct skipping the gate (index.js:8995) — hold too.

2026-08-18 — 3479373f… — same pin, three mechanisms added after a re-read prompted by the upstream author. The authorized exits from conflict were missing from this report: /plur1bus curation resolve and /plur1bus curation drop-injected sit behind the same authorization gate as the status transitions (index.js:6853), and the bulk form is doubly bounded — a preview before the apply, and a refusal of any record that is not status === "conflict" and does not satisfy isInjectedContextText (lib/drop-injected-conflicts.js:104). Derived dream records stamp a visibility and rem-dream.js hands its reader a requester triple, which the scoping row now says. Each was read at this pin against index.js and lib/, not taken from the report of it.

2026-08-17 — 3479373f… — re-pinned at release 7.4.0, 49 commits past the previous pin. Screened again: the same postinstall host patch, both manifests inside the seven-day cooldown, four floating ranges behind the lockfile; nothing installed or run, and the new behaviour was read from the source and its committed tests. Marks unchanged at all seven and now carrying evidence records. Two published criticisms are corrected, both in the direction that understated the system.

  • The drift gate has a live caller. This report said a threshold whose only caller disabled it had no live consumer. lib/jobs/apply-conflict-resolution.js now calls safeUpdate without skipDriftGate behind a confirm === true check, catches the "Semantic drift too high" throw, and returns review_only instead of writing — so the automated conflict apply is gated where the confirmed human correction is not. /correct keeps the skip, and index.js:8995 is the only non-test skipDriftGate left.
  • demoted withholds. The report recorded the neo doubt states as ranking penalties rather than filters. scoreNeoRecallItem now returns -Infinity for demoted alongside pruned and tombstoned, pinned by tests/neo-demoted-withhold.test.js — "excludes demoted at -Infinity and keeps conflict finite". conflict is deliberately left finite, and the reason is written at the call site with its numbers: the detector is "an unvalidated LLM", a 16 August 2026 live probe found 4,017 newest-revision records carrying conflict (2,505 on one agent) "with no resolve path that clears the status", and a twenty-row sample was not pairwise contradiction. The split the code now draws — withhold on a state a person set, rank on a state a model guessed — is a better answer than closing both.

Also new: PLUR1BUS_SKIP_HOST_PATCH=1 is honoured by scripts/setup-feature-crons.mjs and scripts/install-memory-system.sh, with its own test, so the install can complete "without writing into the OpenClaw dist tree" — the patch is still the default, and the objection now has a switch rather than a fork. A global injection budget lands at index.js:12249 (recall.globalInjectMaxChars, default 17,000 characters across the joined prompt block). Tombstone coverage extends across the bulk writers, with tests/tombstone-bulk-writers.test.js and tests/light-dream-injection-guard.test.js asserting that a dream rewrite cannot resurrect forgotten text. index.js is 10,867 lines, lib/ 61,178, across 361 test files.

One fact about the repository is worth recording plainly. docs/superpowers/specs/2026-08-17-atlas-remaining-gaps-design.md is a design document titled "Remaining Atlas Gaps" that cites this report by URL and by pin (b550a2d8, v7.3.0) as its input, lists what a prior pull request closed — including "Neo demoted ranked instead of withholding" — and plans eight further workstreams against it, one of which is to "document the Atlas objection at the patch callsite". Every claim above was verified against the code and its tests rather than against that document.

2026-08-15 — b550a2d8… — re-pinned at release 7.3.0 ("audit fixes, epistemic status, bi-temporal memory"). Screened again before reading: the same postinstall host patch, both manifests inside the seven-day cooldown, four floating ranges with the lockfile present; nothing was installed or run, and the new behaviour was read from the source and its committed tests. Three new load-bearing modules close two gaps and add a hard-filtering trust state:

  • Bi-temporal memory (lib/valid-time.js, columns at db-adapter.js:404) adds a real-world validity window (validFrom/validUntil) separate from record time, queryable as-of through a validAt recall parameter, with validity caller-supplied rather than guessed. That earns bitemporal.
  • A value-keyed tombstone (lib/tombstone.js) writes a content-fingerprint denial record on /forget — SHA-256 of the normalized text, never the plaintext — to an append-only registry that survives restore, migration and re-embedding, checked as step zero of every capture (index.js:6411, :10667) and gated on a binding audit. That earns tombstone and supersedes the record-keyed tombstoned neo status. d25e101f… stops a correction from reviving a tombstoned card.
  • A claim-level epistemic status (lib/epistemic-status.js) whose invalidated value hard-filters on the read path (recall-pipeline.js:157, neo-arch.js:1464, db-adapter.js:562) — the first doubt-adjacent state in the system that withholds rather than ranks. Transitions into trusted/invalidated require an authorized actor, and a conservative merge rule refuses to launder a weak memory up to a higher tier.

Two scope fixes land in the same release. 09a5254c… repairs the REM-dream candidate loader, whose scope partition was built as user/workspace only and so rejected every agent-private candidate — measured disabling the dream job entirely on two live agents (70/70 and 49/49 candidates agent-private); buildRemPartitions now runs agent-private first. 90ced8cb… adds a per-card ACL to the /critical review surface (lib/critical-review.js), which had gated only on a destructive-channel check. index.js is 10,289 lines, lib/ 57,345, across 339 test files. No paper or citation file exists in the tree.

2026-08-11 — 3efedcd4… — second reading the same day, at release 7.2.6, twelve commits past the first pin. Screened again before reading: 0 auto-run surfaces, the same postinstall, both manifests inside the cooldown; nothing was installed. Three regression files were executed with node --test and pass (27 tests); restoring lib/neo-arch.js from the previous pin on a scratch copy fails 5 of 7 in the dedup file.

A published claim was wrong, and wrong in the system's favour. This report stated that a record marked conflict or demoted was ranked down by 0.3 and injected anyway. The penalty was not being applied at all. The JSONL stores are append-only, so transitionRecordStatus appends a second line under the same id; routeNeoRecall deduplicated by first appearance and therefore scored the pre-transition copy, with its active status. 20cf0fe7… changes the deduplication to keep the newest revision by updatedAt, preserving first-appearance order so the tiebreak stays stable. The error in this report was one of mechanism rather than of outcome — the observable behaviour was as described — and it came from reading the scorer without reading what the scorer is handed.

The same commit renders status on each <memory-record> line, closing a gap this report did not find: the memory prompt supplement instructs the model to prefer active and promoted over conflicting cards, and the template emitted lane, category, trust, id and score but not status, so the instruction asked for a distinction the payload did not carry. The reading that would have caught it is one this report did not perform — checking every distinction a prompt instruction demands against the fields the renderer actually emits.

It also corrects /correct. The confirmation dialog named an 80-character title while safeUpdate replaced the full text against a fuzzily-resolved target, and payload.oldText carried the user's search term rather than the stored content, so updateEvidence recorded the query instead of the value it replaced. Both are fixed, and skipDriftGate is retained with its rationale written at the call site. That answers the open question this report carried about why the gate is disabled.

35852e8e… repairs the timestamps the operational guard depends on: canonical KNOWLEDGE.md hits carried no age and, containing operational keywords, permanently demanded live verification; they now take the file mtime and are marked authoritative and exempt. A probe over the live namespaces is recorded in that commit as 25,550 rows with none missing createdAt, placing the defect in the read path's mapping layer rather than in the store.

The unreferenced plur1bus/ directory described in the previous entry is deleted. Its index.js imported ./lib/categorize.js, which never existed inside that directory, so the copy could not have run.

2026-08-11 — 241aac28… — first reading, at release 7.2.3. Screened before reading: 0 auto-run surfaces, 1 build-time exec (postinstall runs scripts/setup-feature-crons.mjs, which patches the host OpenClaw dist directory), 1 unpinned manifest, and both package.json and package-lock.json changed inside the seven-day cooldown; nothing was installed and nothing was executed.