Repository navigation
Description
Context
Roadmap B1 and B2 — see docs/roadmap.md
and the anchor issue #168. The capability exists and is inert: agent/memory.go implements
Remember/Recall/Forget/List against a remote HTTP service, Agent.Memory is declared and read
at the top of Run (agent/agent.go:457), and nothing anywhere calls it — no caller of Remember,
no assignment of Agent.Memory, no memory key in any config file.
docs/memory-decision.md (merged) fixes the v1 write path: one writer, a /remember command the user
types; automatic end-of-turn extraction waits for B3. This issue is the work that decision unblocks.
The two roadmap entries are one unit of delivery on purpose: wiring recall without a store that can hold
anything ships a feature that recalls an empty list forever, and a store without a caller ships a
backend nobody reads. Splitting them creates that in-between state.
What to build
S1 — config and the layer gate.
config.MemoryConfig (enabled, backend, dir, max_entries, max_bytes, recall_limit,
base_url, bank) with defaults enabled: true, backend: "local"; a MemorySpec() resolver in the
shape of TrajectorySpec(); and UserLayer.ResolveMemory, which honours an egress request (a
network backend, or any base_url) only when the user's own ~/.tinycode/config.json asked for it —
a project-local .tinycode/config.json that tries is ignored and the ignore is logged, exactly as
UserLayer.ResolveTrajectoryCapture does for capture: full. config.example.json gains the section
with a $comment saying what leaves the machine.
S2 — the local store.
agent/memory_local.go implementing types.MemoryStore over a JSONL file at
<dir>/<project-hash>.jsonl (default dir ~/.tinycode/memory, 0600), one line per entry:
id= first 16 hex ofsha256(key NUL value), soRememberis idempotent;types.MemorygainsID,SourceandCreatedAt;MemoryStore.Forgettakes the idList
returns (the parameter is renamed; there are no callers to migrate);- bounds:
max_entries(default 2000) andmax_bytes(default 8 MiB), oldest dropped first by
rewriting the file through a temp file and a rename; - recall is lexical — lowercase tokenisation, a small stopword list, tf-idf over the store plus a
recency tiebreak, toprecall_limit(default 5) above a minimum score. No new dependency, and no
pretence of semantics: that is B3; Forgetreally deletes; the Hindsight no-op must not be the model for this backend;- in-process
sync.Mutexserialises writers. Cross-process safety is a single-line append and nothing
more — a compaction that races another process can lose a queued line. That limit is recorded here,
not silently assumed away.
S2b — the store is not a tool path.
tool.SetMemoryRoot plus a refusal in CheckPathAccess, mirroring tool/trajectory_store.go: a
memory the agent can rewrite is not a memory. The default dir lives outside the project root, and a
tool that tries to write it is refused with the same wording as the trajectory and checkpoint stores.
S3 — wiring and commands (its own PR, after this one). S4 (Hindsight contract) and S5 (docs) are
listed in the slices above this issue's first PR and are not part of it.
Acceptance
UserLayer.ResolveMemoryreturnsignored=truefor a project-local egress request, and the merged
config still resolves to the local backend.- The store survives a reopen: write two facts, construct a second instance over the same directory,
Listreturns both. - Remembering the same
(key, value)twice leaves one entry. - Past
max_entries, the oldest entry is gone and the newest are all present. Forget(id)removes exactly one entry, and a reopened store does not return it.- Eight concurrent
Remembercalls all land (no lost line). tool.CheckPathAccessrefuses a write beneath the memory root with the reserved-store wording.go test ./... -count=1,-race,gofmt -l,go vet,staticcheckclean.
Out of scope
Automatic summarisation (see docs/memory-decision.md for the reopen condition), embeddings and vector
search (B3), memory as a model-callable tool (#188), an approval queue, cross-project memory sharing,
and cross-process locking.
Activity
feat(memory): a local store, behind a config section, tools cannot write
docs: record the local memory store and the recounted test total
feat(memory): a local store, behind a config section, tools cannot write
docs: record the local memory store and the recounted test total
feat(memory): wire recall, /remember and /memory
yusiwen commentedon Oct 25, 2026
S1–S3 landed; S4 is what remains.
3986849): thememoryconfig section with the user-layer egress gate,LocalMemory(JSONL per project, bounded,id-deduplicated, lexical recall, real delete),
types.MemorygainingID/Source/CreatedAt, andtool.SetMemoryRootmaking the store unreachable from the tool layer.4eeac93): the compositionroot,
/remember,/memory,/memory forget <id>, the capped and logged recall step, and theinjected block worded as data rather than instructions.
docs/roadmap.mdnow records B1 and B2 asDelivered — [memory] Local store, wired recall, and one explicit writer #293.
decoders read
content/memory_unitswhere the current API returnstext/items, andForgetreports
ErrMemoryDeleteUnsupported. The next increment is a probe against a real server, not adecoder change made from the docs alone.
Measurements:
go test ./... -count=1and-race→ 16 packages ok; 1208 test functions in the mainmodule (
find . -name '*_test.go' -not -path './tuiprobe/*' -print0 | xargs -0 grep -h '^func Test' | wc -l). Recall quality is unmeasured — it is lexical until B3.What v1 deliberately does not do is in
docs/memory-decision.md: no automatic summarisation, so nosecond model call and no store that fills itself.
fix(memory): require a base_url for the hindsight backend
yusiwen commentedon Oct 27, 2026
Closing as delivered. The slices:
3986849): thememoryconfigsection with the user-layer egress gate,
LocalMemory(JSONL, one shard per project, bounded byentries and bytes, id-deduplicated, lexical recall, real delete),
types.MemorygainingID/Source/CreatedAt, andtool.SetMemoryRootmaking the store unreachable from the tool layer.4eeac93): the composition root,/remember,/memory,/memory forget <id>, the capped and logged recall step, and the injectedblock worded as data rather than instructions.
10d1627): the hindsightbackend now requires
base_url, the bank comes from config, and the hard-coded default endpoint isgone from the tree.
README bullet, example config.
S4 moved out, not dropped: #302 owns the Hindsight
adapter's contract (probe against a real deployment, decoder alignment, ids,
Forget, and theOPENAI_API_KEYfallback). The remote backend is reachable by configuration today, but it has neverbeen verified against a served response — that is #302's whole subject.
Measurements, for the record: 1230 test functions in the main module;
go test ./... -count=1and-racegreen at the merge of each PR; recall quality measured separately in #297 (the lexical rankeranswers 0 of 6 paraphrases, the embedding path answers 4–5, and the cross-lingual column is decided by
the model).
docs(roadmap): point B1's remaining work at its own issue