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 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 -Infinity3. 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) andrecall-pipeline.js(2,094) dominate;safe-update.js,acl-middleware.js,memory-history.jsandcontradiction-detector.jscarry 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:
- Refuse non-active rows. A superseded memory cannot be updated; the chain only grows at the leaf.
- Validate the ownership tuple before any read or write, requiring the binding that the row's own scope demands.
- Demand evidence.
validateUpdatePatchthrows unless a text or summary change carries bothevidence.updateSourceandevidence.updateEvidence. - 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.
- Demand a new vector. A text change without
patch.vectorthrows: "The embedding must reflect the new content." This closes the failure where a corrected memory keeps ranking under the old text's query. - Gate on semantic drift
(
safe-update.js:395) — cosine distance over 0.45 is rejected outright. - 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. - 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.
validFromandvalidUntil(lib/db-adapter.js:447) record when a claim was true in the world,0meaning "no known bound" rather than the epoch. They are separate fromcreatedAt/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 optionalvalidAtinstant through every chokepoint —isEntryValidAtdoes a left-inclusive/right-exclusivevalidFrom <= validAt < validUntil, pushed down to the vector store as SQL — and thememory_recalltool 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 to0/unknown rather than a fabricated date. - A rejected-value tombstone.
/forgetsoft-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.findBlockingTombstoneForCaptureruns 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-keyedtombstonedneo 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_endthrough 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.jsonland 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.mjsregisters 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 —
neverForgetandmemoryClass: coreare 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 —
/forgetfails if the audit record cannot be written. - One doubt state that withholds. The epistemic
invalidatedstatus is a hard filter at all three read layers, and transitions intotrusted/invalidatedrequire 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.
conflictand the trust ladder remain ranking arithmetic —conflicta 0.3 penalty atlib/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.demotedand the epistemicinvalidatedstate 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
visibilityand 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—typestillmemory,statusdeleted— 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 andrunClassifierreports what it skipped asskippedInactive(lib/jobs/critical-classifier.js:111-138), withtests/critical-classifier-double-push.test.jscovering 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.autoApplyat its defaultfalse,executeActions(lib/jobs/memory-compaction.js:1104) executes nothing and callspersistProposals, 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_forgetis a declared tool with no confirmation exchange, enabled unlesssecurity.allowModelDestructiveMemoryOpsis set to exactlyfalse, 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 withdrewhuman_review; the detail is in section 8. postinstallstill 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:411is 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
neverForgetflag 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
conflictis 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
scoreNeoRecallItemcome 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/validUntilcolumns inlib/db-adapter.js,validAtrecall parameter inindex.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; nopatches/directory exists, andtests/cron-plugin-direct-dispatch-wiring.test.jsasserts 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.jsnow callssafeUpdatewithoutskipDriftGatebehind aconfirm === truecheck, catches the "Semantic drift too high" throw, and returnsreview_onlyinstead of writing — so the automated conflict apply is gated where the confirmed human correction is not./correctkeeps the skip, andindex.js:8995is the only non-testskipDriftGateleft. demotedwithholds. The report recorded the neo doubt states as ranking penalties rather than filters.scoreNeoRecallItemnow returns-Infinityfordemotedalongsideprunedandtombstoned, pinned bytests/neo-demoted-withhold.test.js— "excludes demoted at -Infinity and keeps conflict finite".conflictis 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 carryingconflict(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 atdb-adapter.js:404) adds a real-world validity window (validFrom/validUntil) separate from record time, queryable as-of through avalidAtrecall parameter, with validity caller-supplied rather than guessed. That earnsbitemporal. - 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 earnstombstoneand supersedes the record-keyedtombstonedneo status.d25e101f…stops a correction from reviving a tombstoned card. - A claim-level epistemic status
(
lib/epistemic-status.js) whoseinvalidatedvalue 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 intotrusted/invalidatedrequire 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.