@try-works/dsh-memory 0.1.19 → 0.1.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +585 -585
- package/STATUS.md +287 -287
- package/cordis.patch.yml +69 -69
- package/package.json +1 -1
- package/scripts/check-package.mjs +76 -76
- package/src/SYSTEM.template.md +311 -311
- package/src/capture.js +242 -242
- package/src/client/index.js +886 -886
- package/src/config.js +57 -57
- package/src/consolidate-run.js +132 -132
- package/src/consolidate.js +330 -330
- package/src/context.js +88 -88
- package/src/decisions.js +106 -106
- package/src/defaults.js +93 -93
- package/src/digest.js +255 -255
- package/src/entry.js +179 -179
- package/src/eval.js +72 -72
- package/src/files.js +60 -60
- package/src/fixture.js +56 -56
- package/src/forget.js +71 -71
- package/src/freshness.js +65 -65
- package/src/fts.js +166 -166
- package/src/git.js +53 -53
- package/src/history.js +226 -226
- package/src/host.js +866 -866
- package/src/index.js +115 -94
- package/src/inject.js +108 -108
- package/src/layers.js +178 -178
- package/src/ledger.js +19 -19
- package/src/map.js +184 -184
- package/src/models.js +136 -136
- package/src/queue.js +602 -602
- package/src/rank.js +31 -31
- package/src/recover.js +91 -91
- package/src/refresh.js +80 -80
- package/src/remember.js +58 -58
- package/src/rerank.js +90 -90
- package/src/search.js +75 -75
- package/src/select.js +57 -57
- package/src/session.js +60 -60
- package/src/state.js +240 -240
- package/src/store.js +128 -128
- package/src/summarize.js +195 -195
- package/src/system-doc.js +171 -171
- package/src/tasks.js +617 -617
- package/src/telemetry.js +122 -122
- package/src/tools.js +828 -818
- package/src/working.js +288 -288
- package/src/workspace-key.js +68 -68
- package/src/workspace.js +87 -87
package/src/SYSTEM.template.md
CHANGED
|
@@ -1,311 +1,311 @@
|
|
|
1
|
-
# dsh-memory — how this memory system works
|
|
2
|
-
|
|
3
|
-
Written by the plugin itself on every boot, describing the system **as built** rather than as
|
|
4
|
-
planned. It is the manual for the machinery: read it before deciding where a fact belongs, or
|
|
5
|
-
before trusting something found in the store.
|
|
6
|
-
|
|
7
|
-
- Document version: {{documentVersion}}
|
|
8
|
-
- Plugin version: {{version}}
|
|
9
|
-
- Generated: {{generated}}
|
|
10
|
-
- Store root: {{store}}
|
|
11
|
-
|
|
12
|
-
## 1. Why this exists
|
|
13
|
-
|
|
14
|
-
An agent forgets everything between sessions. This system gives it durable memory that (a)
|
|
15
|
-
survives restarts, (b) travels between machines through git, and (c) stays plain markdown a
|
|
16
|
-
human can read, diff and edit.
|
|
17
|
-
|
|
18
|
-
Three intents drive every decision below:
|
|
19
|
-
|
|
20
|
-
1. **The files are the truth.** The store is markdown. The search index, the ledger and every
|
|
21
|
-
cache are derived and may be deleted at any time — which is exactly why they are gitignored.
|
|
22
|
-
If a fact is not in a `.md` file, it is not remembered.
|
|
23
|
-
2. **The model decides, the plugin writes.** Language models judge what matters well and produce
|
|
24
|
-
reliable structure badly. So a model is only ever asked *what* is durable; the plugin owns
|
|
25
|
-
*how* it is written — ids, formatting, routing, indexing. Nothing a model says is trusted to
|
|
26
|
-
be well-formed.
|
|
27
|
-
3. **Memory must not become a junk drawer.** Every stage has a budget and a filter. Capturing
|
|
28
|
-
less and recalling the right thing beats capturing everything.
|
|
29
|
-
|
|
30
|
-
## 2. The layers, and what belongs in each
|
|
31
|
-
|
|
32
|
-
Every layer is **multi-layered**: the named file is an *index* of topics, and the entries live in
|
|
33
|
-
one file per topic under a sibling directory. The vocabulary lives in one place, `src/entry.js`
|
|
34
|
-
(`LAYER_CODES`, `LAYER_FILES`, `LAYER_DIRS`).
|
|
35
|
-
|
|
36
|
-
```text
|
|
37
|
-
{{diagram}}
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
{{layerTable}}
|
|
41
|
-
|
|
42
|
-
Only {{workspaceOnly}} are workspace-only, because a map or a commit log of *one* repository is
|
|
43
|
-
meaningless in a shared layer. `working.md` is per workspace for the same reason. Writing a
|
|
44
|
-
workspace-only layer to `global` is refused rather than silently merged.
|
|
45
|
-
|
|
46
|
-
### Why an index plus topic files, instead of one big file
|
|
47
|
-
|
|
48
|
-
A single append-only file per layer grows without bound: slow to read, impossible to review in a
|
|
49
|
-
diff, useless to grep. Splitting by topic keeps every file small and readable, and the index
|
|
50
|
-
gives a one-screen overview.
|
|
51
|
-
|
|
52
|
-
The index is **regenerated from the topic files after every write**, so it can never disagree
|
|
53
|
-
with what it points at. It is a view, not a second source of truth. Adding a topic file by hand
|
|
54
|
-
is fine — the next rebuild picks it up. Topic slugs preserve Unicode letters, so CJK and other
|
|
55
|
-
non-Latin topics stay distinct files rather than collapsing into `untitled.md`. Id allocation
|
|
56
|
-
spans the whole layer, so `NNNN` is unique per scope and layer rather than per file.
|
|
57
|
-
|
|
58
|
-
## 2b. Memory may correctly write nothing
|
|
59
|
-
|
|
60
|
-
The worker is not required to add a memory every interval or turn. Writing something merely
|
|
61
|
-
to fill a gap pollutes search and creates false context. The relevance filter, the thin-signal
|
|
62
|
-
floor, `NO_DURABLE_CONTENT`, and the DONE-only consolidation reply are all intentional no-op
|
|
63
|
-
paths. A handled digest with zero facts is successful, writes zero entries, and is not retried.
|
|
64
|
-
High signal and low noise are the goal; the common case is that nothing durable changed.
|
|
65
|
-
|
|
66
|
-
## 2c. What capture refuses, and why failures retry
|
|
67
|
-
|
|
68
|
-
Candidate material excludes harness-injected context (runtime snapshots, skill catalogs,
|
|
69
|
-
policy notices, and this plugin's own injected block, which would be a feedback loop).
|
|
70
|
-
Assistant material is the answer block, not private reasoning, and a bare status opener is
|
|
71
|
-
skipped so a research answer arrives as findings. A FAILED summary (empty, unparseable or
|
|
72
|
-
thrown) keeps its material and retries; a deliberate NO_DURABLE_CONTENT settles at once.
|
|
73
|
-
|
|
74
|
-
## 2d. Which model does memory work
|
|
75
|
-
|
|
76
|
-
Each call builds a ladder from the live registry: the configured summaryModel (a preference),
|
|
77
|
-
then the session route, then everything else routable. The first model returning text wins; a
|
|
78
|
-
model returning nothing or throwing is skipped and the same prompt goes to the next rung. The
|
|
79
|
-
winner is remembered so the normal case costs one call, and a model that returned nothing is
|
|
80
|
-
benched for 15 minutes at the end of the ladder rather than dropped. The ladder is capped at
|
|
81
|
-
four models, and if none returns text the call errors so the session is retried.
|
|
82
|
-
|
|
83
|
-
## 2e. Memory belongs to the main agent
|
|
84
|
-
|
|
85
|
-
A subagent READS but does not write. Capture skips subagent sessions, every writing tool
|
|
86
|
-
refuses with an explanation, and the injected context tells a child to report findings to its
|
|
87
|
-
caller. Ownership is the reason: the parent delegated the work and its transcript carries the
|
|
88
|
-
result, so a child-written entry is a second, context-poorer account of the same task.
|
|
89
|
-
|
|
90
|
-
## 3. How a fact gets in — the write path
|
|
91
|
-
|
|
92
|
-
There are two ways in, and the difference matters.
|
|
93
|
-
|
|
94
|
-
**Explicitly**, with `memory_remember` (any layer), `memory_codebase` (map a module),
|
|
95
|
-
`memory_history` (ingest commits). These are deterministic: the plugin allocates the id, stamps
|
|
96
|
-
the date and source, appends the block to the right topic file, regenerates that layer's index,
|
|
97
|
-
and reindexes only the files it touched.
|
|
98
|
-
|
|
99
|
-
**Automatically**, from the session itself:
|
|
100
|
-
|
|
101
|
-
1. **Capture** runs on turn end (throttled by `autoCaptureIntervalMs`, default {{captureInterval}}).
|
|
102
|
-
It projects only the events that arrived since a `lastSeq` watermark, so the work is
|
|
103
|
-
proportional to what is new, not to how long the session has run.
|
|
104
|
-
2. The **relevance filter** decides what may reach a model at all: tool calls collapse to a
|
|
105
|
-
*count* (mechanics are not memory), user turns and assistant *gists* are clipped, bare
|
|
106
|
-
acknowledgements are dropped, and the whole candidate set is trimmed to a hard byte ceiling.
|
|
107
|
-
One exception is deliberate: the file paths a session's tool *calls* point at are kept as
|
|
108
|
-
traversal evidence, because they say where the agent actually worked.
|
|
109
|
-
3. The **summarizer** turns that material into a handful of durable bullets, under standing
|
|
110
|
-
instructions about what is durable and what is not.
|
|
111
|
-
4. **Consolidation** (cadence {{consolidateInterval}}, or on demand) reads pending digests,
|
|
112
|
-
decides what is worth storing and where, and hands the plugin a *decision* — never a file.
|
|
113
|
-
|
|
114
|
-
Steps 2–4 are why this system does not drown: at every stage something is thrown away on
|
|
115
|
-
purpose.
|
|
116
|
-
|
|
117
|
-
### Why the writer, not the model, owns the format
|
|
118
|
-
|
|
119
|
-
An entry is a heading plus a metadata block (`**Id:**`, `**Source:**`, `**Date:**`,
|
|
120
|
-
`**Importance:**`, `**Status:**`, `**Format:**`). If a model were allowed to write that, one
|
|
121
|
-
malformed block would corrupt the ledger, the id sequence and search at once.
|
|
122
|
-
|
|
123
|
-
The model's contract is correspondingly small and line-oriented — one line per fact:
|
|
124
|
-
|
|
125
|
-
```text
|
|
126
|
-
- [<digest id> | global|<workspace:key> | <layer> | <importance>] <topic> :: <text>
|
|
127
|
-
DONE <digest id>
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
This replaced a nested-JSON contract that failed repeatedly in practice: one invalid placeholder,
|
|
131
|
-
one truncated brace, one reasoning preamble or one echoed prompt was enough to lose an entire
|
|
132
|
-
pass. A line protocol parses per line, so a single bad line cannot kill a batch, prose is ignored
|
|
133
|
-
instead of mined, and failure is safe — an unreadable reply is logged and retried, never guessed at.
|
|
134
|
-
|
|
135
|
-
## 3b. Freshness: the entry says how fast it decays
|
|
136
|
-
|
|
137
|
-
Every entry carries its own decay metadata, so staleness is determinable from the file alone —
|
|
138
|
-
it survives a clone because it travels with the text.
|
|
139
|
-
|
|
140
|
-
- `**Volatility:**` — `stable` ({{stableDays}} days: a language, a license, a convention),
|
|
141
|
-
`normal` ({{normalDays}} days: how a subsystem works), `volatile` ({{volatileDays}} days: a
|
|
142
|
-
port, a version in flight, anything that will be untrue soon).
|
|
143
|
-
- `**Verified:**` — when the fact was last *confirmed true*. This is the age basis.
|
|
144
|
-
- `**Valid-until:**` — a hard expiry for facts that genuinely end.
|
|
145
|
-
|
|
146
|
-
Freshness is a property of the entry, not a constant applied to a write date: a language choice
|
|
147
|
-
and a port number do not decay at the same rate, and only the writer knows which is which. When
|
|
148
|
-
volatility is absent, time-bounded language still hints at it, then the normal window applies.
|
|
149
|
-
|
|
150
|
-
`state.md` is the workspace overview — what the repository IS right now. It is replaced per
|
|
151
|
-
section rather than appended, every section carries a `Verified` stamp, and a stale section is
|
|
152
|
-
shown WITH its age in the injected context so it reads as a hypothesis to check rather than a
|
|
153
|
-
fact to trust. Graded verdicts: fresh / aging / stale / unverified.
|
|
154
|
-
|
|
155
|
-
## 3c. Working memory belongs to YOU
|
|
156
|
-
|
|
157
|
-
`working.md` is **your file**, not the repository's and not durable memory. It answers *what am
|
|
158
|
-
I in the middle of*: the task in hand, what you just did, and what comes next. Keep it current;
|
|
159
|
-
nothing else writes there.
|
|
160
|
-
|
|
161
|
-
It is deliberately separate from `state.md`. Those describe different owners: a repository has
|
|
162
|
-
one current state, but several agents may work in it with entirely different intents — and one
|
|
163
|
-
intent may span several repositories. Merging them would lose both meanings.
|
|
164
|
-
|
|
165
|
-
| Section | Behaviour |
|
|
166
|
-
| --- | --- |
|
|
167
|
-
| Task, Next steps, Plan, Blocked by | **Replaced** on write: there is one current task, one current plan. |
|
|
168
|
-
| Recent activity | A **rolling log**, newest last, trimmed oldest-first by entries and characters. |
|
|
169
|
-
|
|
170
|
-
Working memory decays in **hours**, not days, so it is graded on its own scale (fresh / aging /
|
|
171
|
-
stale / not started) and an unrecorded file is called out rather than hidden — silence would
|
|
172
|
-
otherwise read as "no working memory" when it means "nothing written yet".
|
|
173
|
-
|
|
174
|
-
Write it with `memory_working` (a section, or activity notes), or edit the file directly. It is
|
|
175
|
-
yours: read it, edit it, keep it honest. When something there stops changing, promote it to a
|
|
176
|
-
layered entry and let the file go quiet.
|
|
177
|
-
|
|
178
|
-
## 4. How a fact gets out
|
|
179
|
-
|
|
180
|
-
- **Injection** (`agent/pre-step`) puts a bounded context card at the top of a session: the
|
|
181
|
-
active goal, the workspace working-memory head, recent learnings, and the topics already mapped
|
|
182
|
-
for this workspace.
|
|
183
|
-
- **`memory_search`** is the explicit path: lexical FTS5 over every layer file, ranked by
|
|
184
|
-
importance and recency, returning `file:line` citations so a claim can be checked.
|
|
185
|
-
- **The context card** assembles lanes (working, episodes, learnings, codebase) under a token
|
|
186
|
-
budget, with floors and ceilings per lane.
|
|
187
|
-
|
|
188
|
-
Search returns *pointers*, not truth: always eligible to be stale, always checkable.
|
|
189
|
-
|
|
190
|
-
### Optional rerank
|
|
191
|
-
|
|
192
|
-
`memory_search` can ask a model to reorder hits. It is off by default and strictly advisory:
|
|
193
|
-
any failure returns the deterministic order, because search must never depend on a model.
|
|
194
|
-
|
|
195
|
-
## 5. Scope — global versus workspace
|
|
196
|
-
|
|
197
|
-
A **workspace** is one associated local folder or repository. The key is resolved so it is
|
|
198
|
-
identical across machines: explicit input, then remote URL, then folder basename. `memory.json`
|
|
199
|
-
holds the committed registry of keys; `state/associations.json` holds device-local paths, because
|
|
200
|
-
paths differ per machine and must never be committed.
|
|
201
|
-
|
|
202
|
-
Facts that are true *everywhere* — identity, preferences, standing rules, cross-repo lessons — go
|
|
203
|
-
global. Facts tied to one repository go to that workspace. When unsure, ask whether it would be
|
|
204
|
-
misleading in a different repository; if yes, it is a workspace fact.
|
|
205
|
-
|
|
206
|
-
## 6. What is committed, and what is derived
|
|
207
|
-
|
|
208
|
-
| Committed | Derived (gitignored, regenerable) |
|
|
209
|
-
| --- | --- |
|
|
210
|
-
| every `.md` layer file and topic file | `index/` — the FTS5 search index |
|
|
211
|
-
| `SYSTEM.md` (this document) | `state/` — watermarks, associations, tombstones |
|
|
212
|
-
| `memory.json` (workspace registry) | `digests/` — captured session material |
|
|
213
|
-
| `.gitignore` | `logs/` — diagnostics |
|
|
214
|
-
|
|
215
|
-
The line is: **can it be rebuilt from the committed markdown?** If yes it is derived, and it is
|
|
216
|
-
safe to delete. If no, it is committed. Tombstones are the one deliberate exception — they are
|
|
217
|
-
derived state, so a fresh clone re-learns a forgotten fact only if it is re-derived.
|
|
218
|
-
|
|
219
|
-
## 7. Forgetting, and keeping memory honest
|
|
220
|
-
|
|
221
|
-
- **Forget** supersedes an entry in place (history is never deleted) and records a hash-only
|
|
222
|
-
tombstone so the same fact cannot be silently re-added. It is reversible with undo.
|
|
223
|
-
- **Freshness** flags entries that are stale or past their `Valid-until`, and a refresh pass asks
|
|
224
|
-
a model for keep / update / supersede per entry. Superseding keeps the old text.
|
|
225
|
-
- Entries may carry `**Valid-until:**` for facts that genuinely expire.
|
|
226
|
-
|
|
227
|
-
## 8. Models and cost
|
|
228
|
-
|
|
229
|
-
Memory work is extraction, not conversation, so it does not run at the session's reasoning
|
|
230
|
-
budget. `memoryReasoningEffort` (default `{{effort}}`) applies to every memory call, and a model
|
|
231
|
-
that rejects that effort is retried without it rather than failing every write. `summaryModel`
|
|
232
|
-
optionally points memory writes at a cheaper model; empty means the session's own model.
|
|
233
|
-
|
|
234
|
-
Replies are assembled from **text blocks only**. A reasoning model streams its thinking as
|
|
235
|
-
separate blocks, and folding those into the reply prepends prose to an answer that is supposed to
|
|
236
|
-
be parseable.
|
|
237
|
-
|
|
238
|
-
## 9. Cadences, and why they are not on the write path
|
|
239
|
-
|
|
240
|
-
Nothing expensive happens synchronously with a write. Capture, consolidation, refresh, history
|
|
241
|
-
ingest and git sync all run on throttle windows, on the pre-step hook, and on an idle ticker so
|
|
242
|
-
they still run when nobody is chatting.
|
|
243
|
-
|
|
244
|
-
| Cadence | Default |
|
|
245
|
-
| --- | --- |
|
|
246
|
-
| capture per session | {{captureInterval}} |
|
|
247
|
-
| consolidation | {{consolidateInterval}} |
|
|
248
|
-
| freshness refresh | {{refreshInterval}} |
|
|
249
|
-
| commit history ingest | {{historyInterval}} |
|
|
250
|
-
| git pull/push | {{syncInterval}} |
|
|
251
|
-
| local commit debounce | {{commitDebounce}} |
|
|
252
|
-
|
|
253
|
-
Sync failures are never swallowed: a failed pull or push is logged, recorded in `state/sync.json`,
|
|
254
|
-
surfaced by `status()`, reported by `memory_sync`, and leaves a `state/CONFLICT.md` marker that
|
|
255
|
-
the next clean sync clears.
|
|
256
|
-
|
|
257
|
-
## 9b. The scheduled tasks layer
|
|
258
|
-
|
|
259
|
-
`global/tasks/` is not a memory layer and holds no memories. It is a portable backup of the DSH
|
|
260
|
-
scheduled tasks on this machine, and it exists because the schedule store is the only copy of them:
|
|
261
|
-
it is in no git repository, and the host DELETES a task when the session that owns it stops. A task
|
|
262
|
-
can therefore vanish with nothing to observe.
|
|
263
|
-
|
|
264
|
-
One JSON file per task, plus a regenerated `index.md`. A file holds the task's record - id, kind,
|
|
265
|
-
title, prompt, and the selector that says when it runs - and its lifecycle state. The owning session
|
|
266
|
-
and the delivery history are deliberately NOT stored: they are machine-local, and carrying a session
|
|
267
|
-
id to another machine would make a restored task fail, because the session it names does not exist
|
|
268
|
-
there.
|
|
269
|
-
|
|
270
|
-
Because the task files live in this store and are not gitignored, a push carries them to your other
|
|
271
|
-
machines, where they are re-armed on boot.
|
|
272
|
-
|
|
273
|
-
The states, and what each one means:
|
|
274
|
-
|
|
275
|
-
| state | meaning |
|
|
276
|
-
| --- | --- |
|
|
277
|
-
| `active` | armed in the live store, waiting for its next occurrence |
|
|
278
|
-
| `pending` | in the backup, not in the live store; a restore re-arms it |
|
|
279
|
-
| `paused` | held back on purpose. Every automatic pass leaves it alone |
|
|
280
|
-
| `stopped` | was armed, and its owning session stopped, so the host deleted it |
|
|
281
|
-
| `failed` | was due and has still not delivered. The host records no failure of its own, so this is what is observable |
|
|
282
|
-
| `orphaned` | armed, but the session that owns it no longer exists, so it can never fire |
|
|
283
|
-
| `skipped` | captured, but its kind is not one we can re-arm, so a restore refuses it |
|
|
284
|
-
| `done` | delivered, or its recurrence is exhausted |
|
|
285
|
-
|
|
286
|
-
Two cadences, both off the write path. The **backup** runs with the sync cadence and is additive:
|
|
287
|
-
it never deletes, so a stale backup cannot wipe a schedule. The **restore** runs once per process at
|
|
288
|
-
the first real session, because a task needs a live session to own it, and it binds each restored
|
|
289
|
-
task to that session - you are never asked which one. It matches tasks by CONTENT, not by id, since
|
|
290
|
-
the schedule service mints a fresh id on every create.
|
|
291
|
-
|
|
292
|
-
`memory_tasks` reports the whole picture and can `backup`, `restore`, `pause` or `resume` on
|
|
293
|
-
demand. Holding a task back is a decision, and it outranks every automatic pass: a `paused` task is
|
|
294
|
-
never silently re-armed.
|
|
295
|
-
|
|
296
|
-
## 10. Current state of this store
|
|
297
|
-
|
|
298
|
-
{{storeState}}
|
|
299
|
-
|
|
300
|
-
## 11. How to work with this system
|
|
301
|
-
|
|
302
|
-
- **Prefer `memory_search` over guessing.** The store is small and cheap to query.
|
|
303
|
-
- **Write when you learn something durable**, not when you finish a task. A fact that only
|
|
304
|
-
mattered for one turn is not memory.
|
|
305
|
-
- **One fact per entry**, with concrete identifiers: paths, commands, config keys, versions.
|
|
306
|
-
- **Name the topic well.** For `codebase` use the path or subsystem name, so re-discovery updates
|
|
307
|
-
the same topic file instead of fragmenting it.
|
|
308
|
-
- **Say why, not just what.** A decision without its rationale cannot be re-evaluated later.
|
|
309
|
-
- **Supersede, never delete.** History is the point.
|
|
310
|
-
- If this document ever disagrees with the code, **the code wins** — report the drift so the
|
|
311
|
-
document can be regenerated.
|
|
1
|
+
# dsh-memory — how this memory system works
|
|
2
|
+
|
|
3
|
+
Written by the plugin itself on every boot, describing the system **as built** rather than as
|
|
4
|
+
planned. It is the manual for the machinery: read it before deciding where a fact belongs, or
|
|
5
|
+
before trusting something found in the store.
|
|
6
|
+
|
|
7
|
+
- Document version: {{documentVersion}}
|
|
8
|
+
- Plugin version: {{version}}
|
|
9
|
+
- Generated: {{generated}}
|
|
10
|
+
- Store root: {{store}}
|
|
11
|
+
|
|
12
|
+
## 1. Why this exists
|
|
13
|
+
|
|
14
|
+
An agent forgets everything between sessions. This system gives it durable memory that (a)
|
|
15
|
+
survives restarts, (b) travels between machines through git, and (c) stays plain markdown a
|
|
16
|
+
human can read, diff and edit.
|
|
17
|
+
|
|
18
|
+
Three intents drive every decision below:
|
|
19
|
+
|
|
20
|
+
1. **The files are the truth.** The store is markdown. The search index, the ledger and every
|
|
21
|
+
cache are derived and may be deleted at any time — which is exactly why they are gitignored.
|
|
22
|
+
If a fact is not in a `.md` file, it is not remembered.
|
|
23
|
+
2. **The model decides, the plugin writes.** Language models judge what matters well and produce
|
|
24
|
+
reliable structure badly. So a model is only ever asked *what* is durable; the plugin owns
|
|
25
|
+
*how* it is written — ids, formatting, routing, indexing. Nothing a model says is trusted to
|
|
26
|
+
be well-formed.
|
|
27
|
+
3. **Memory must not become a junk drawer.** Every stage has a budget and a filter. Capturing
|
|
28
|
+
less and recalling the right thing beats capturing everything.
|
|
29
|
+
|
|
30
|
+
## 2. The layers, and what belongs in each
|
|
31
|
+
|
|
32
|
+
Every layer is **multi-layered**: the named file is an *index* of topics, and the entries live in
|
|
33
|
+
one file per topic under a sibling directory. The vocabulary lives in one place, `src/entry.js`
|
|
34
|
+
(`LAYER_CODES`, `LAYER_FILES`, `LAYER_DIRS`).
|
|
35
|
+
|
|
36
|
+
```text
|
|
37
|
+
{{diagram}}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
{{layerTable}}
|
|
41
|
+
|
|
42
|
+
Only {{workspaceOnly}} are workspace-only, because a map or a commit log of *one* repository is
|
|
43
|
+
meaningless in a shared layer. `working.md` is per workspace for the same reason. Writing a
|
|
44
|
+
workspace-only layer to `global` is refused rather than silently merged.
|
|
45
|
+
|
|
46
|
+
### Why an index plus topic files, instead of one big file
|
|
47
|
+
|
|
48
|
+
A single append-only file per layer grows without bound: slow to read, impossible to review in a
|
|
49
|
+
diff, useless to grep. Splitting by topic keeps every file small and readable, and the index
|
|
50
|
+
gives a one-screen overview.
|
|
51
|
+
|
|
52
|
+
The index is **regenerated from the topic files after every write**, so it can never disagree
|
|
53
|
+
with what it points at. It is a view, not a second source of truth. Adding a topic file by hand
|
|
54
|
+
is fine — the next rebuild picks it up. Topic slugs preserve Unicode letters, so CJK and other
|
|
55
|
+
non-Latin topics stay distinct files rather than collapsing into `untitled.md`. Id allocation
|
|
56
|
+
spans the whole layer, so `NNNN` is unique per scope and layer rather than per file.
|
|
57
|
+
|
|
58
|
+
## 2b. Memory may correctly write nothing
|
|
59
|
+
|
|
60
|
+
The worker is not required to add a memory every interval or turn. Writing something merely
|
|
61
|
+
to fill a gap pollutes search and creates false context. The relevance filter, the thin-signal
|
|
62
|
+
floor, `NO_DURABLE_CONTENT`, and the DONE-only consolidation reply are all intentional no-op
|
|
63
|
+
paths. A handled digest with zero facts is successful, writes zero entries, and is not retried.
|
|
64
|
+
High signal and low noise are the goal; the common case is that nothing durable changed.
|
|
65
|
+
|
|
66
|
+
## 2c. What capture refuses, and why failures retry
|
|
67
|
+
|
|
68
|
+
Candidate material excludes harness-injected context (runtime snapshots, skill catalogs,
|
|
69
|
+
policy notices, and this plugin's own injected block, which would be a feedback loop).
|
|
70
|
+
Assistant material is the answer block, not private reasoning, and a bare status opener is
|
|
71
|
+
skipped so a research answer arrives as findings. A FAILED summary (empty, unparseable or
|
|
72
|
+
thrown) keeps its material and retries; a deliberate NO_DURABLE_CONTENT settles at once.
|
|
73
|
+
|
|
74
|
+
## 2d. Which model does memory work
|
|
75
|
+
|
|
76
|
+
Each call builds a ladder from the live registry: the configured summaryModel (a preference),
|
|
77
|
+
then the session route, then everything else routable. The first model returning text wins; a
|
|
78
|
+
model returning nothing or throwing is skipped and the same prompt goes to the next rung. The
|
|
79
|
+
winner is remembered so the normal case costs one call, and a model that returned nothing is
|
|
80
|
+
benched for 15 minutes at the end of the ladder rather than dropped. The ladder is capped at
|
|
81
|
+
four models, and if none returns text the call errors so the session is retried.
|
|
82
|
+
|
|
83
|
+
## 2e. Memory belongs to the main agent
|
|
84
|
+
|
|
85
|
+
A subagent READS but does not write. Capture skips subagent sessions, every writing tool
|
|
86
|
+
refuses with an explanation, and the injected context tells a child to report findings to its
|
|
87
|
+
caller. Ownership is the reason: the parent delegated the work and its transcript carries the
|
|
88
|
+
result, so a child-written entry is a second, context-poorer account of the same task.
|
|
89
|
+
|
|
90
|
+
## 3. How a fact gets in — the write path
|
|
91
|
+
|
|
92
|
+
There are two ways in, and the difference matters.
|
|
93
|
+
|
|
94
|
+
**Explicitly**, with `memory_remember` (any layer), `memory_codebase` (map a module),
|
|
95
|
+
`memory_history` (ingest commits). These are deterministic: the plugin allocates the id, stamps
|
|
96
|
+
the date and source, appends the block to the right topic file, regenerates that layer's index,
|
|
97
|
+
and reindexes only the files it touched.
|
|
98
|
+
|
|
99
|
+
**Automatically**, from the session itself:
|
|
100
|
+
|
|
101
|
+
1. **Capture** runs on turn end (throttled by `autoCaptureIntervalMs`, default {{captureInterval}}).
|
|
102
|
+
It projects only the events that arrived since a `lastSeq` watermark, so the work is
|
|
103
|
+
proportional to what is new, not to how long the session has run.
|
|
104
|
+
2. The **relevance filter** decides what may reach a model at all: tool calls collapse to a
|
|
105
|
+
*count* (mechanics are not memory), user turns and assistant *gists* are clipped, bare
|
|
106
|
+
acknowledgements are dropped, and the whole candidate set is trimmed to a hard byte ceiling.
|
|
107
|
+
One exception is deliberate: the file paths a session's tool *calls* point at are kept as
|
|
108
|
+
traversal evidence, because they say where the agent actually worked.
|
|
109
|
+
3. The **summarizer** turns that material into a handful of durable bullets, under standing
|
|
110
|
+
instructions about what is durable and what is not.
|
|
111
|
+
4. **Consolidation** (cadence {{consolidateInterval}}, or on demand) reads pending digests,
|
|
112
|
+
decides what is worth storing and where, and hands the plugin a *decision* — never a file.
|
|
113
|
+
|
|
114
|
+
Steps 2–4 are why this system does not drown: at every stage something is thrown away on
|
|
115
|
+
purpose.
|
|
116
|
+
|
|
117
|
+
### Why the writer, not the model, owns the format
|
|
118
|
+
|
|
119
|
+
An entry is a heading plus a metadata block (`**Id:**`, `**Source:**`, `**Date:**`,
|
|
120
|
+
`**Importance:**`, `**Status:**`, `**Format:**`). If a model were allowed to write that, one
|
|
121
|
+
malformed block would corrupt the ledger, the id sequence and search at once.
|
|
122
|
+
|
|
123
|
+
The model's contract is correspondingly small and line-oriented — one line per fact:
|
|
124
|
+
|
|
125
|
+
```text
|
|
126
|
+
- [<digest id> | global|<workspace:key> | <layer> | <importance>] <topic> :: <text>
|
|
127
|
+
DONE <digest id>
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
This replaced a nested-JSON contract that failed repeatedly in practice: one invalid placeholder,
|
|
131
|
+
one truncated brace, one reasoning preamble or one echoed prompt was enough to lose an entire
|
|
132
|
+
pass. A line protocol parses per line, so a single bad line cannot kill a batch, prose is ignored
|
|
133
|
+
instead of mined, and failure is safe — an unreadable reply is logged and retried, never guessed at.
|
|
134
|
+
|
|
135
|
+
## 3b. Freshness: the entry says how fast it decays
|
|
136
|
+
|
|
137
|
+
Every entry carries its own decay metadata, so staleness is determinable from the file alone —
|
|
138
|
+
it survives a clone because it travels with the text.
|
|
139
|
+
|
|
140
|
+
- `**Volatility:**` — `stable` ({{stableDays}} days: a language, a license, a convention),
|
|
141
|
+
`normal` ({{normalDays}} days: how a subsystem works), `volatile` ({{volatileDays}} days: a
|
|
142
|
+
port, a version in flight, anything that will be untrue soon).
|
|
143
|
+
- `**Verified:**` — when the fact was last *confirmed true*. This is the age basis.
|
|
144
|
+
- `**Valid-until:**` — a hard expiry for facts that genuinely end.
|
|
145
|
+
|
|
146
|
+
Freshness is a property of the entry, not a constant applied to a write date: a language choice
|
|
147
|
+
and a port number do not decay at the same rate, and only the writer knows which is which. When
|
|
148
|
+
volatility is absent, time-bounded language still hints at it, then the normal window applies.
|
|
149
|
+
|
|
150
|
+
`state.md` is the workspace overview — what the repository IS right now. It is replaced per
|
|
151
|
+
section rather than appended, every section carries a `Verified` stamp, and a stale section is
|
|
152
|
+
shown WITH its age in the injected context so it reads as a hypothesis to check rather than a
|
|
153
|
+
fact to trust. Graded verdicts: fresh / aging / stale / unverified.
|
|
154
|
+
|
|
155
|
+
## 3c. Working memory belongs to YOU
|
|
156
|
+
|
|
157
|
+
`working.md` is **your file**, not the repository's and not durable memory. It answers *what am
|
|
158
|
+
I in the middle of*: the task in hand, what you just did, and what comes next. Keep it current;
|
|
159
|
+
nothing else writes there.
|
|
160
|
+
|
|
161
|
+
It is deliberately separate from `state.md`. Those describe different owners: a repository has
|
|
162
|
+
one current state, but several agents may work in it with entirely different intents — and one
|
|
163
|
+
intent may span several repositories. Merging them would lose both meanings.
|
|
164
|
+
|
|
165
|
+
| Section | Behaviour |
|
|
166
|
+
| --- | --- |
|
|
167
|
+
| Task, Next steps, Plan, Blocked by | **Replaced** on write: there is one current task, one current plan. |
|
|
168
|
+
| Recent activity | A **rolling log**, newest last, trimmed oldest-first by entries and characters. |
|
|
169
|
+
|
|
170
|
+
Working memory decays in **hours**, not days, so it is graded on its own scale (fresh / aging /
|
|
171
|
+
stale / not started) and an unrecorded file is called out rather than hidden — silence would
|
|
172
|
+
otherwise read as "no working memory" when it means "nothing written yet".
|
|
173
|
+
|
|
174
|
+
Write it with `memory_working` (a section, or activity notes), or edit the file directly. It is
|
|
175
|
+
yours: read it, edit it, keep it honest. When something there stops changing, promote it to a
|
|
176
|
+
layered entry and let the file go quiet.
|
|
177
|
+
|
|
178
|
+
## 4. How a fact gets out
|
|
179
|
+
|
|
180
|
+
- **Injection** (`agent/pre-step`) puts a bounded context card at the top of a session: the
|
|
181
|
+
active goal, the workspace working-memory head, recent learnings, and the topics already mapped
|
|
182
|
+
for this workspace.
|
|
183
|
+
- **`memory_search`** is the explicit path: lexical FTS5 over every layer file, ranked by
|
|
184
|
+
importance and recency, returning `file:line` citations so a claim can be checked.
|
|
185
|
+
- **The context card** assembles lanes (working, episodes, learnings, codebase) under a token
|
|
186
|
+
budget, with floors and ceilings per lane.
|
|
187
|
+
|
|
188
|
+
Search returns *pointers*, not truth: always eligible to be stale, always checkable.
|
|
189
|
+
|
|
190
|
+
### Optional rerank
|
|
191
|
+
|
|
192
|
+
`memory_search` can ask a model to reorder hits. It is off by default and strictly advisory:
|
|
193
|
+
any failure returns the deterministic order, because search must never depend on a model.
|
|
194
|
+
|
|
195
|
+
## 5. Scope — global versus workspace
|
|
196
|
+
|
|
197
|
+
A **workspace** is one associated local folder or repository. The key is resolved so it is
|
|
198
|
+
identical across machines: explicit input, then remote URL, then folder basename. `memory.json`
|
|
199
|
+
holds the committed registry of keys; `state/associations.json` holds device-local paths, because
|
|
200
|
+
paths differ per machine and must never be committed.
|
|
201
|
+
|
|
202
|
+
Facts that are true *everywhere* — identity, preferences, standing rules, cross-repo lessons — go
|
|
203
|
+
global. Facts tied to one repository go to that workspace. When unsure, ask whether it would be
|
|
204
|
+
misleading in a different repository; if yes, it is a workspace fact.
|
|
205
|
+
|
|
206
|
+
## 6. What is committed, and what is derived
|
|
207
|
+
|
|
208
|
+
| Committed | Derived (gitignored, regenerable) |
|
|
209
|
+
| --- | --- |
|
|
210
|
+
| every `.md` layer file and topic file | `index/` — the FTS5 search index |
|
|
211
|
+
| `SYSTEM.md` (this document) | `state/` — watermarks, associations, tombstones |
|
|
212
|
+
| `memory.json` (workspace registry) | `digests/` — captured session material |
|
|
213
|
+
| `.gitignore` | `logs/` — diagnostics |
|
|
214
|
+
|
|
215
|
+
The line is: **can it be rebuilt from the committed markdown?** If yes it is derived, and it is
|
|
216
|
+
safe to delete. If no, it is committed. Tombstones are the one deliberate exception — they are
|
|
217
|
+
derived state, so a fresh clone re-learns a forgotten fact only if it is re-derived.
|
|
218
|
+
|
|
219
|
+
## 7. Forgetting, and keeping memory honest
|
|
220
|
+
|
|
221
|
+
- **Forget** supersedes an entry in place (history is never deleted) and records a hash-only
|
|
222
|
+
tombstone so the same fact cannot be silently re-added. It is reversible with undo.
|
|
223
|
+
- **Freshness** flags entries that are stale or past their `Valid-until`, and a refresh pass asks
|
|
224
|
+
a model for keep / update / supersede per entry. Superseding keeps the old text.
|
|
225
|
+
- Entries may carry `**Valid-until:**` for facts that genuinely expire.
|
|
226
|
+
|
|
227
|
+
## 8. Models and cost
|
|
228
|
+
|
|
229
|
+
Memory work is extraction, not conversation, so it does not run at the session's reasoning
|
|
230
|
+
budget. `memoryReasoningEffort` (default `{{effort}}`) applies to every memory call, and a model
|
|
231
|
+
that rejects that effort is retried without it rather than failing every write. `summaryModel`
|
|
232
|
+
optionally points memory writes at a cheaper model; empty means the session's own model.
|
|
233
|
+
|
|
234
|
+
Replies are assembled from **text blocks only**. A reasoning model streams its thinking as
|
|
235
|
+
separate blocks, and folding those into the reply prepends prose to an answer that is supposed to
|
|
236
|
+
be parseable.
|
|
237
|
+
|
|
238
|
+
## 9. Cadences, and why they are not on the write path
|
|
239
|
+
|
|
240
|
+
Nothing expensive happens synchronously with a write. Capture, consolidation, refresh, history
|
|
241
|
+
ingest and git sync all run on throttle windows, on the pre-step hook, and on an idle ticker so
|
|
242
|
+
they still run when nobody is chatting.
|
|
243
|
+
|
|
244
|
+
| Cadence | Default |
|
|
245
|
+
| --- | --- |
|
|
246
|
+
| capture per session | {{captureInterval}} |
|
|
247
|
+
| consolidation | {{consolidateInterval}} |
|
|
248
|
+
| freshness refresh | {{refreshInterval}} |
|
|
249
|
+
| commit history ingest | {{historyInterval}} |
|
|
250
|
+
| git pull/push | {{syncInterval}} |
|
|
251
|
+
| local commit debounce | {{commitDebounce}} |
|
|
252
|
+
|
|
253
|
+
Sync failures are never swallowed: a failed pull or push is logged, recorded in `state/sync.json`,
|
|
254
|
+
surfaced by `status()`, reported by `memory_sync`, and leaves a `state/CONFLICT.md` marker that
|
|
255
|
+
the next clean sync clears.
|
|
256
|
+
|
|
257
|
+
## 9b. The scheduled tasks layer
|
|
258
|
+
|
|
259
|
+
`global/tasks/` is not a memory layer and holds no memories. It is a portable backup of the DSH
|
|
260
|
+
scheduled tasks on this machine, and it exists because the schedule store is the only copy of them:
|
|
261
|
+
it is in no git repository, and the host DELETES a task when the session that owns it stops. A task
|
|
262
|
+
can therefore vanish with nothing to observe.
|
|
263
|
+
|
|
264
|
+
One JSON file per task, plus a regenerated `index.md`. A file holds the task's record - id, kind,
|
|
265
|
+
title, prompt, and the selector that says when it runs - and its lifecycle state. The owning session
|
|
266
|
+
and the delivery history are deliberately NOT stored: they are machine-local, and carrying a session
|
|
267
|
+
id to another machine would make a restored task fail, because the session it names does not exist
|
|
268
|
+
there.
|
|
269
|
+
|
|
270
|
+
Because the task files live in this store and are not gitignored, a push carries them to your other
|
|
271
|
+
machines, where they are re-armed on boot.
|
|
272
|
+
|
|
273
|
+
The states, and what each one means:
|
|
274
|
+
|
|
275
|
+
| state | meaning |
|
|
276
|
+
| --- | --- |
|
|
277
|
+
| `active` | armed in the live store, waiting for its next occurrence |
|
|
278
|
+
| `pending` | in the backup, not in the live store; a restore re-arms it |
|
|
279
|
+
| `paused` | held back on purpose. Every automatic pass leaves it alone |
|
|
280
|
+
| `stopped` | was armed, and its owning session stopped, so the host deleted it |
|
|
281
|
+
| `failed` | was due and has still not delivered. The host records no failure of its own, so this is what is observable |
|
|
282
|
+
| `orphaned` | armed, but the session that owns it no longer exists, so it can never fire |
|
|
283
|
+
| `skipped` | captured, but its kind is not one we can re-arm, so a restore refuses it |
|
|
284
|
+
| `done` | delivered, or its recurrence is exhausted |
|
|
285
|
+
|
|
286
|
+
Two cadences, both off the write path. The **backup** runs with the sync cadence and is additive:
|
|
287
|
+
it never deletes, so a stale backup cannot wipe a schedule. The **restore** runs once per process at
|
|
288
|
+
the first real session, because a task needs a live session to own it, and it binds each restored
|
|
289
|
+
task to that session - you are never asked which one. It matches tasks by CONTENT, not by id, since
|
|
290
|
+
the schedule service mints a fresh id on every create.
|
|
291
|
+
|
|
292
|
+
`memory_tasks` reports the whole picture and can `backup`, `restore`, `pause` or `resume` on
|
|
293
|
+
demand. Holding a task back is a decision, and it outranks every automatic pass: a `paused` task is
|
|
294
|
+
never silently re-armed.
|
|
295
|
+
|
|
296
|
+
## 10. Current state of this store
|
|
297
|
+
|
|
298
|
+
{{storeState}}
|
|
299
|
+
|
|
300
|
+
## 11. How to work with this system
|
|
301
|
+
|
|
302
|
+
- **Prefer `memory_search` over guessing.** The store is small and cheap to query.
|
|
303
|
+
- **Write when you learn something durable**, not when you finish a task. A fact that only
|
|
304
|
+
mattered for one turn is not memory.
|
|
305
|
+
- **One fact per entry**, with concrete identifiers: paths, commands, config keys, versions.
|
|
306
|
+
- **Name the topic well.** For `codebase` use the path or subsystem name, so re-discovery updates
|
|
307
|
+
the same topic file instead of fragmenting it.
|
|
308
|
+
- **Say why, not just what.** A decision without its rationale cannot be re-evaluated later.
|
|
309
|
+
- **Supersede, never delete.** History is the point.
|
|
310
|
+
- If this document ever disagrees with the code, **the code wins** — report the drift so the
|
|
311
|
+
document can be regenerated.
|