1. Executive Summary
Reasonix is a coding agent whose memory is one Markdown file per fact, designed around the provider's prefix cache. The cached system prompt carries a memory protocol that is byte-identical across projects, plus the bodies of pinned facts; every other fact reaches the model per turn. What is notable is the committed memory benchmark, with a memory-off arm and a column for the tasks memory made worse. What is weak is correction: a fact forgotten mid-session stays in whichever text already carried it, and one of the benchmark's five must-not checks cannot pass.
This report reads the 2.x line. The repository ships
two lines from two branches. studio, the default branch and
the pin here, is 2.x — Reasonix Studio, pre-release and under active
development. main-v2 is 1.x, in maintenance. The two forked
on 12 August 2026 and share the store format and
~/.reasonix (docs/MIGRATING.md). They differ
in how memory reaches the model: 1.x moves the index into a
<session-context> snapshot on the user turn (2b7c6522…),
and 2.x puts the index in no prompt at all.
The constraint is the prefix cache.
BackgroundBlock renders a fixed memoryProtocol
and the pinned bodies. Its doc comment gives the reason the index is
absent: it "changes on every remember, and everything after it in
the prefix … would be re-sent with it"
(internal/state/memory/memory.go:152-192).
Compose folds that block into the system prompt once, at
boot (internal/assembly/boot/memoryset.go:41). Standing
instruction documents ride the turn instead, owed once per session and
again when they change on disk. The package doc in
internal/state/memory/doc.go says all of it folds into the
prefix at boot, which holds only for the protocol and the pinned
bodies.
Correction is text appended after text that stays.
forget archives the file and returns "it no longer
applies". A pinned fact's body stays in the cached prompt for the
rest of the session, and a relevant fact recalled on an earlier turn
stays in that turn. Only a forget from the Memory panel adds a
"disregard its loaded guidance" note to the next turn; the
model's own forget relies on its tool result.
The store is one Markdown file per fact. A record
carries an immutable ID separate from a renameable
Name, a monotonic Revision and a four-value
Type (user, feedback,
project, reference). It also carries an
Activation, a Volatility,
ExpiresAt, LastVerifiedAt, and a
SubjectKey — "which question the fact
answers (project.package_manager); one active value per
scope+subject." A save that claims a subject another active fact
holds is refused, and the error names the holder's id so the model can
write the new answer as its next revision.
The artifact is benchmarks/memorybench.
Fifteen committed tasks named for the questions a memory store should be
asked — mb-contradiction, mb-stale,
mb-conflict, mb-update,
mb-history, mb-distractor,
mb-pin, mb-paraphrase, mb-exact,
plus three mb-v1miss-* regression cases named for a
retrieval miss that shipped. Each is a workspace, a seeded memory
directory, a prompt and a verify.sh.
Two of seven marks. negative_eval rests
on the four satisfiable must-not verifications in that benchmark.
human_review rests on the approval gate over the agent's
own remember and forget calls, which no
approval mode answers. The misses are near-misses. Freshness classifies
age instead of belief. Revision snapshots and a recall audit sit either
side of a mutation event log without being one. The subject key refuses
a second answer without remembering a rejected one, and the project
boundary is a directory rather than a filter.
2. Mental Model
A fact is written by the model, behind a person's approval unless it
is a bounded new project fact, or edited by a person in the Memory
panel. It answers at most one question, its SubjectKey, and
a second fact claiming the same question in the same scope is refused. A
changed answer is a new revision of the one fact, with the old revision
kept under .revisions/.
How a fact reaches the model depends on its Activation.
A pinned body is folded into the cached system prompt at
session start. A relevant fact is retrieved: a BM25 recall
block is appended after the user's text on each turn, and the
memory tool searches, lists and reads the rest. A fact ages
on a clock set by its volatility and reset by explicit verification, and
past ExpiresAt it leaves automatic recall.
The diagram has to show the seam the cache creates. The cached prompt is composed once, so a forget changes the files and the next turn's recall, and leaves the prompt the session started with.
Diagram source
%% caption: the cached prompt carries a fixed protocol and pinned bodies composed at boot; everything else arrives per turn, and a forget changes the store but not the prompt already sent
flowchart TD
W["remember tool<br/>model-written"] --> GATE{"bounded new<br/>project fact?"}
GATE -- "no" --> ASK["waits for a person<br/>in every approval mode"]
GATE -- "yes" --> SUB
ASK -- "approved" --> SUB{"SubjectKey held by<br/>another active fact?"}
ASK -- "denied" --> DROP["not written"]
SUB -- "yes" --> REF["save refused<br/>names the holder's id"]
REF -. "model updates the holder" .-> REV["new revision<br/>old one under .revisions"]
SUB -- "no" --> F[("one fact per file<br/>ID, Revision, SubjectKey")]
REV --> F
PANEL["Memory panel edit"] --> F
F --> ACT{"Activation"}
ACT -- "pinned" --> BOOT["Compose at boot"]
BOOT --> PREFIX[["cached system prompt<br/>fixed protocol + pinned bodies"]]
ACT -- "relevant" --> RECALL["BM25 recall each turn<br/>expired left out"]
RECALL --> TAIL[["recall block after<br/>the user's text"]]
PREFIX --> MODEL["model"]
TAIL --> MODEL
F -. "memory tool: search, list, read" .-> MODEL
FG["forget"] --> ARCH[("archive/")]
ARCH -. "leaves the pinned body in this session's prompt" .-> PREFIXThe dotted arrow at the bottom is the finding. A forget reaches the store at once and the cached prompt only at the next session.
3. Architecture
A single Go kernel and no memory server. Memory lives under
~/.reasonix: a per-project directory under
projects/<slug>/memory, a shared
memory/global, an archive/, and per-fact
.revisions/
(internal/state/memory/store.go:116-117,
store_v2.go:389). Instruction docs are discovered at four
scopes — user, ancestor, project, and a git-ignored
*.local.md override with the highest precedence.
REASONIX.md is the project's own name;
AGENTS.md and CLAUDE.md are read as cross-tool
conventions, and when several exist in one directory all of them load,
each labelled with its source path.
The kernel is reachable from a terminal CLI and TUI, from
reasonix serve over HTTP, from the Reasonix Studio desktop
app, and from editors over ACP. Studio is an Electron shell whose React
renderer calls the same serve routes over loopback
(desktop/README.md), so the Memory panel and the approval
prompt are HTTP routes rather than a separate desktop binary.
4. Essential Implementation Paths
- Compose.
internal/state/memory.ComposefoldsBackgroundBlock— the fixed protocol and the pinned bodies — into the system prompt once, frominternal/assembly/boot/memoryset.go:41. - Per turn.
turnBlocksForprepends the owed<project-instructions>and any<memory-update>notes, andrecallTailappends the recall block after the user's text (internal/session/control/turn_projection.go:47-66,:118-132). - Write.
remember→ the approval gate, orAssessRememberWrite's auto-allow for a bounded new project fact → normaliseType(anything unknown becomesproject, so "a sloppy tool argument never blocks a save") → resolve scope → refuse a heldSubjectKey→ write the file, snapshot the prior revision, rewriteMEMORY.md. - Read.
auto_recall.goranks relevant facts with BM25 and leaves out pinned and expired ones; thememorytool servessearch,listandread. - Forget.
forget.go→Store.Archive(name)→QueueMemory, which the controller answers by reloading the store and queuing nothing (internal/session/control/memory.go:316-328).
5. Memory Data Model
The ID/Name split is the right one:
identity is immutable, the human-facing name can change without breaking
references. Revision is monotonic, a save can require the
expected revision, and every overwritten revision is kept as an
immutable snapshot under
.revisions/<id>/<revision>.md, which
Restore and RestoreArchived bring back as a
new revision (store_v2.go:381, :410,
:438).
SubjectKey is the field to copy. Naming which
question a fact answers, and refusing a second active fact for the
same (scope, subject), converts a contradiction into a
revision. The refusal names the holder's id,
revision and current value, and tells the model to
"update that id instead of creating a second one"
(internal/state/memory/subject.go:64-81). A store that
detects a contradiction by embedding distance still has to decide what
to do; this one declares the key up front and refuses the ambiguity.
Its limit is that it keys on the question, not on the value. An update of the holder is accepted whatever it says, so nothing stops a value an earlier revision replaced from coming back as the next one. After the holder is archived, the key is free and any value claims it.
Keywords is a small good idea: search aliases including
bilingual synonyms, used for recall and "never rendered into the
index", so recall breadth costs no index tokens.
6. Retrieval Mechanics
Two channels, split by Activation. A pinned
body is in the cached system prompt for the whole session, and automatic
recall skips it because it "already ride[s] the stable prefix"
(internal/state/memory/auto_recall.go:342-347). A
relevant fact is ranked with BM25 over its text and
keywords, project facts weighted 1.08 over global ones, and the top hits
under a character budget are appended after the user's text. A global
fact whose id, name, title or SubjectKey matches a project
fact is dropped from the pool, so the project answer shadows the global
one (auto_recall.go:341-395). The index reaches the model
only when the model calls memory list.
TestBlockIsIdenticalWhateverTheStoreHolds holds the
cache property: it builds the static block over an empty store and over
one whose MEMORY.md lists a fact, and fails if they differ
or if the fact's line appears
(internal/state/memory/memory_test.go:43-58).
Freshness gates recall. FreshnessFor classifies a fact
fresh, current, stale or
expired from its Volatility and
LastVerifiedAt. stale multiplies a recall
score by 0.92 and expired — past an explicit
ExpiresAt — is left out of automatic recall, while explicit
search finds it (auto_recall.go:197-204).
Store.Index also drops expired facts, under a doc comment
calling it the index for the cached prefix, and has no caller outside
its tests (index.go:12-28).
That is not a trust state and the mark is withheld.
The four values are a statement about age, not about belief: a stale
fact is old, not doubted, and LastVerifiedAt renews a clock
rather than setting a status. mb-stale and
mb-contradiction are exactly the cases where the
older fact is wrong rather than merely aged, and nothing in the
record can say so.
7. Write Mechanics
Writes are synchronous and explicit, and a pending approval blocks
the turn until a person answers. There is no extraction pass and no
background consolidation on the memory path — the model decides to call
remember, or a person edits a fact in the Memory panel.
A written fact is retrievable on the next turn's recall. A fact
written as pinned, or pinned from /memory,
reaches the cached prompt only at the next session, and
setMemoryActivation says so in its reply.
The subject rule runs at write time. Beyond it the store is
permissive by design: an unknown Type normalises rather
than failing, and the pinned-body budget is unset unless a user
configures memory.pinned_budget_chars.
8. Agent Integration
remember, forget and a read-only
memory tool for the model; a # quick-add and a
/memory command for the human; and a Memory panel in Studio
that lists facts, marks which were used on the last turn, edits,
restores revisions and forgets.
The approval gate is what earns
human_review. Every interactive gate appends Ask
rules for remember and forget and strips both
from session allowlists, so --allowed-tools remember cannot
pre-approve a write
(internal/session/control/approval_orchestration.go:192-202).
Both are RequiresFreshHumanApprovalTool, which refuses the
YOLO bypass and session grants for them
(internal/session/control/approval.go:593-594,
:620-622, :693-700).
TestMemoryApprovalIgnoresAutoApproveTools asserts that a
memory approval under tool auto-approval "must wait for manual
approval", and a headless run refuses every memory call the
auto-allow below does not cover. The HTTP approval route demands the
launch token even with authentication off, because "reaching the
listener does not make a caller the operator"
(internal/frontend/serve/launch_token.go:53-64).
Two limits go with the mark. AssessRememberWrite lets a
bounded, non-sensitive new project or reference fact through with no
prompt; updates, global facts, preferences and feedback ask
(remember_policy.go:26-87). The desktop card for such a
save describes it as happening "without being asked" and offers
a one-click forget
(desktop/frontend-next/src/ui/cards/RememberCard.tsx:6-11).
And the pending request lives in process memory, so a proposal open when
the session ends is dropped, not kept.
Memory is one part of a much larger agent — plan mode, permissions, a workspace sandbox and per-turn checkpoints are the product, and the checkpoints are session state rather than memory.
9. Reliability, Safety, and Trust
The store is well-built for a single trusted user on one machine and
has no mechanism for anything else. Scope is a directory root, so a
project fact cannot reach another project's session — a real boundary,
by path, and a partition rather than a stored key applied on a read, so
scope_enforced is not carried. The global tier deliberately
loads everywhere, and there is no principal, tenant or authentication
concept on the store.
There is no mutation audit. forget archives rather than
deleting and every overwritten revision is snapshotted, which preserves
the states. The model's remember and forget
calls sit in the session transcript as tool calls, and a panel edit,
forget or restore reaches it as a note on the next turn. Nothing in the
store records that a fact was written, refused, archived or restored, by
whom or why.
The recall audit is the near-miss, a good mechanism aimed at
the other half of the problem.
TestComposeEmitsMemoryRecallAudit asserts exactly one
recall audit per composed user turn, and that the audit "must
explain itself" — either it carries hits or it names why recall was
suppressed
(internal/session/control/memory_recall_audit_test.go:21-30).
Beside the served hits it records a shadow ranking from an unserved
BM25F ranker, internal/base/retrieval/v2.go. It is the
retrieval half of the append-only memory
audit pattern, and the rubric puts a mutation record on the other
side of the line.
10. Tests, Evals, and Benchmarks
174 Go test functions in internal/state/memory. No
paper.
benchmarks/memorybench is the reason to read
this repository if you care about evaluation. Fifteen tasks,
each a workspace plus a seeded memory directory plus a prompt plus a
verify.sh, and five verifications pair a required string
with a forbidden one. mb-stale seeds a volatile fact naming
release/0.9 and requires release/1.21 while
forbidding release/0.9. mb-update and
mb-conflict do the same for a revised API base and for a
project region that shadows a global one, and mb-symbol
forbids writing the setting to .env. The forbidden half is
the assertion: the superseded value must not be what the agent
answers with. It is identical on both branches.
mb-contradiction cannot pass. Its
verification is
grep -q "pnpm install" answer.txt && ! grep -q "npm install" answer.txt,
and npm install is a substring of
pnpm install. Any answer that satisfies the first half
fails the second, so the task fails for every agent and every memory
arm, and the paired comparison can never count it helpful or harmful.
The repair is a word boundary, grep -qw, or a pattern
anchored on the preceding character.
Two things make the rest stronger than a store-level negative test.
It asserts on end-to-end agent behaviour, so it catches a
memory that was retrieved correctly and then lost an argument with the
context around it. And three tasks are named mb-v1miss-* —
regression cases preserved from a retrieval miss that shipped, including
a cross-language one and a symbol one.
The harness measures utility, not just recall.
taskExperimentEnv can set
REASONIX_EXPERIMENT_NO_MEMORY=1, which hides the store, the
pinned bodies and recall at load
(internal/state/memory/memory.go:56-62).
memoryUtilitySection(pathA, pathB) pairs the two runs by
task id and reports paired pass counts, a
helpful list, a
harmful list, and the average characters
recall injected (cmd/e2ebench/memorybench.go:60,
:217-270). A benchmark with a column for the tasks memory
made worse, beside its token cost, is the shape the benchmarks page asks for.
What is missing is the result. No memorybench output is committed to
the tree, so what exists here is an instrument rather than a
measurement. A run of it would have shown mb-contradiction
failing in both arms.
I did not run any of it.
11. For Your Own Build
Steal
- Name the question a fact answers.
SubjectKey—project.package_manager— with one active value per scope and subject turns a contradiction into a refused save that names the fact to update. - Keep the index out of the cached prompt. A fixed protocol that names the tools, pinned bodies only, and a test that fails if the saved-fact index reaches the block.
- Write the benchmark as the failure register. Task
classes named
contradiction,stale,distractor,paraphrase,pinare the questions a memory system should be asked, and preservingv1missregressions means a retrieval bug that shipped once cannot ship twice. - Put the forbidden string in the verification.
! grep -q "release/0.9"beside the required one is the cheapest negative retrieval assertion there is, and it works end to end without a test harness. - Measure memory-off. One environment variable and a paired run gives you which tasks memory helped, which it hurt, and what it cost in characters.
- Make the memory approval immune to posture. A write prompt that YOLO, session grants and allowlists cannot answer, with a test that asserts it waits.
Avoid
- A forbidden string that the required one contains.
mb-contradictionforbids a substring of its own answer. Check every must-not pattern against the must-have pattern before trusting a zero. - A retraction that leaves the text in place. A forgotten pinned fact stays in the cached prompt until the next session, and a recalled one stays in its turn. What binds the correction is the model reading a later tool result or note over an earlier prompt.
- Freshness standing in for belief. Four values, one of which withholds, all computed from age. The benchmark's stale and contradiction tasks are cases where the old fact is wrong, and nothing in the record distinguishes wrong from old.
Fit
Take this if you want a coding agent whose memory costs a fixed block
per session and is plain files you can read. Take the benchmark whatever
you build: it is MIT-licensed, portable, and each task needs a
workspace, a seeded memory directory and a shell verification. Fix
mb-contradiction first.
Walk away from the memory design if you need multi-user boundaries, an audit of what changed, or a correction that removes the retracted text from the running session. The first is absent by scope, the second is absent, and the third follows from composing the prompt once for the cache.
12. Open Questions
SubjectKeydeclares which question a fact answers. What would it take for a replaced value to leave a record keyed on the value, so an update cannot quietly restore it?- Does the model honour a tool result saying a fact no longer applies over a pinned body still in its system prompt? No memorybench task forgets a fact mid-run and forbids the answer that used it.
- Freshness and correctness are conflated at the point where it matters most. Would a separate two-value status on top of the freshness clock cost anything the prefix budget cannot afford?
- No memorybench results are committed. What does the helpful/harmful split look like, and how large is the injected-character overhead in practice?
Appendix: File Index
Memory
internal/state/memory/doc.go— the two-layer model; its boot-fold paragraph describes more thanComposefoldsinternal/state/memory/memory.go—BackgroundBlock,memoryProtocol,InstructionsBlockFor,Compose,PrefixCostinternal/assembly/boot/memoryset.go— loads the set and composes the prompt onceinternal/session/control/memory.go,turn_projection.go,input.go— owed instructions,<memory-update>notes and the recall tailinternal/state/memory/store.go,store_v2.go,store_index.go— theMemoryrecord, scopes,MEMORY.md,Archive,.revisionssnapshots andRestoreinternal/state/memory/index.go—Store.Index, called only from testsinternal/state/memory/remember.go,remember_policy.go,forget.go,quickadd.go,queue.go— the write and retract surfacesinternal/state/memory/recall.go,auto_recall.go,recall_index.go,activation.go,freshness.go,subject.gointernal/session/control/approval_orchestration.go,approval.go,approval_bridge.go— the memory approval gateinternal/frontend/serve/memory.go,launch_token.go— the Memory panel's routes and the approval route's token guardinternal/session/control/memory_recall_audit_test.go— one explaining audit per turn
Evaluation
benchmarks/memorybench/tasks/— fifteen tasks, each withtask.toml,verify.sh, a seededmemory/and aworkdir/cmd/e2ebench/memorybench.go— seeding, marker scanning andmemoryUtilitySection's paired memory-off comparison
Docs
docs/SESSION_MEMORY_RETRIEVAL.md— calls instruction files part of the cache-stable prefix, whichComposedoes not put there at this pindocs/MIGRATING.md— the two release lines and what they sharedocs/SPEC.md,docs/ACP.md
Recorded searches
Run at the tree root of the pinned checkout.
git grep -n -E 'sessioncontext|session-context' -- '*.go' ':!*_test.go'— no match; the snapshot envelope is 1.x only.git grep -n -E 'MemorySuggestion|AcceptMemory' -- '*.go' '*.ts' '*.tsx'— no match; the mined-suggestion queue was removed fromstudioby908bc3ec….git grep -n -E 'func Record[A-Za-z]*Memory' -- internal/contract/event— onlyRecordMemoryRecall.git grep -n -E '\.Approve\(|ResolveApproval\(' -- internal/tools internal/runtime internal/state ':!*_test.go'— no match; approvals resolve only from the TUI, ACP and HTTP frontends.git grep -n -i -E 'tombstone|rejected_value|Status ' -- internal/state/memory ':!*_test.go'— no match.git grep -n -E 'go func|time\.NewTicker|time\.Tick\(' -- internal/state/memory ':!*_test.go'— no match.git grep -n -E '[A-Za-z)]\.Index\(\)' -- '*.go' ':!*_test.go'— no match;Store.IndexandindexAtare called from five test files and nowhere else.git grep -n -i -E 'forget' -- 'benchmarks/memorybench/tasks/*/task.toml'— no match.git ls-files benchmarks/memorybench | grep -v -E '^benchmarks/memorybench/tasks/'— nothing; no result file is committed.git grep -n -i -E 'arxiv|bibtex|@article|@misc|doi\.org' -- '*.md' '*.cff'— no match.printf 'pnpm install\n' | grep -c "npm install"—1: the forbidden pattern matches the required answer.
History
2026-09-28 — re-pinned to 025bd17b…,
the tip of studio, the default branch at this reading.
Nothing was rewritten: the previous pin is an ancestor of
main-v2, the 1.x maintenance line, and of the archive
fork's branch; studio forked at 23bdf081…
(12 August 2026) and holds no commit with the old pin's tree. Sections
1–11 are rewritten for the 2.x design, where the index reaches no
prompt. Both marks hold. human_review moved to the approval
gate (section 8): studio
removed the suggestion queue on 17 August 2026, and its gate is one no
mode answers, where 1.x's auto and YOLO modes skipped it. Two claims
were wrong at every pin: SubjectKey refuses a second answer
rather than displacing the first, and mb-contradiction
cannot pass (section 10).
Screened: one auto-run surface, eleven files inside the cooldown;
nothing installed, built or run.
2026-09-19 — re-pinned to 86d2424c…,
48 commits on. Both marks stand. human_review was
producer-tested and its record rewritten, because half of what the
previous record rested on does not support it: the remember
/ forget ask rules raise a confirmation dialog, and
AssessRememberWrite's own doc comment says exactly that —
it decides "whether an interactive host may safely allow a remember
call without a confirmation dialog." A prompt guards against a
slip, not against a decision. What does earn the mark is the other half,
now stated alone: a MemorySuggestion is "generated
read-only from recent local history and only persisted through
AcceptMemorySuggestion", and that method is bound on
*App in the desktop binary's main package
while the model's tools live in internal/. The limit is
recorded with it — the queue does not gate the agent's own
remember writes. negative_eval stands on the
unchanged memorybench, whose mb-contradiction/verify.sh
still pairs a required pnpm install with a forbidden
npm install. Screened again first; a dependency surface was
inside the cooldown, so nothing was installed and no suite was run.
2026-09-15 — e4bfeb67…
— 1,585 commits on, 2026-09-15. Screened on a sparse checkout of the
memory, session-context, control, benchmark and desktop suggestion files
plus the root manifests and .githooks/: one auto-run
surface, one unpinned surface and four dependency surfaces inside the
cooldown; nothing was installed or run. The memory package changed in
ten files, and one change rewrote the report's premise: 2b7c6522…
(2 September) moved the memory index and pinned bodies out of the cached
system prompt into a host-generated session-context snapshot that a
write replaces on the next user turn, and QueueMemory now
drops forget's disregard sentence in favour of that
replacement. Sections 1, 2, 4, 6, 8, 9 and 11 are rewritten for it.
scope_enforced withdrawn: the project boundary is a
directory partition, not a stored key applied on a read.
human_review kept, with the approval gate on
remember and forget added to its record;
negative_eval kept on the unchanged memorybench. Revision
snapshots under .revisions/ are now described; they are not
an event log. Two marks.
2026-08-16 — d95e2510…
— First reading, at 5,487 commits. Screened first: one auto-run surface
(committed .githooks/pre-push, inert unless
core.hooksPath points at it), one build-time execution path
(Makefile), and nine manifests inside the seven-day
cooldown; nothing was installed, built or run. Three marks —
scope_enforced, human_review,
negative_eval — and four near-misses stated in place:
freshness classifies age rather than belief, the recall audit is the
retrieval half of the audit pattern, supersession by
SubjectKey keys on the record, and there is no principal
boundary. No paper, and no memorybench results committed to the
tree.