@try-works/dsh-memory 0.1.18 → 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.
Files changed (51) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +585 -585
  3. package/STATUS.md +287 -287
  4. package/cordis.patch.yml +69 -69
  5. package/package.json +1 -1
  6. package/scripts/check-package.mjs +76 -76
  7. package/src/SYSTEM.template.md +311 -311
  8. package/src/capture.js +242 -242
  9. package/src/client/index.js +886 -886
  10. package/src/config.js +57 -57
  11. package/src/consolidate-run.js +132 -132
  12. package/src/consolidate.js +330 -330
  13. package/src/context.js +88 -88
  14. package/src/decisions.js +106 -106
  15. package/src/defaults.js +93 -93
  16. package/src/digest.js +255 -255
  17. package/src/entry.js +179 -179
  18. package/src/eval.js +72 -72
  19. package/src/files.js +60 -60
  20. package/src/fixture.js +56 -56
  21. package/src/forget.js +71 -71
  22. package/src/freshness.js +65 -65
  23. package/src/fts.js +166 -166
  24. package/src/git.js +53 -53
  25. package/src/history.js +226 -226
  26. package/src/host.js +866 -857
  27. package/src/index.js +115 -94
  28. package/src/inject.js +108 -108
  29. package/src/layers.js +178 -178
  30. package/src/ledger.js +19 -19
  31. package/src/map.js +184 -184
  32. package/src/models.js +136 -136
  33. package/src/queue.js +602 -602
  34. package/src/rank.js +31 -31
  35. package/src/recover.js +91 -91
  36. package/src/refresh.js +80 -80
  37. package/src/remember.js +58 -58
  38. package/src/rerank.js +90 -90
  39. package/src/search.js +75 -75
  40. package/src/select.js +57 -57
  41. package/src/session.js +60 -60
  42. package/src/state.js +240 -240
  43. package/src/store.js +128 -128
  44. package/src/summarize.js +195 -195
  45. package/src/system-doc.js +171 -171
  46. package/src/tasks.js +617 -617
  47. package/src/telemetry.js +122 -122
  48. package/src/tools.js +828 -818
  49. package/src/working.js +288 -288
  50. package/src/workspace-key.js +68 -68
  51. package/src/workspace.js +87 -87
@@ -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.