@zenera/cli 1.1.10 → 1.1.12

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 (65) hide show
  1. package/README.md +69 -15
  2. package/dist/banner.js +69 -47
  3. package/dist/cache.d.ts +98 -0
  4. package/dist/cache.js +301 -0
  5. package/dist/catalog.d.ts +3 -0
  6. package/dist/catalog.js +35 -11
  7. package/dist/commands/cache.d.ts +7 -0
  8. package/dist/commands/cache.js +245 -0
  9. package/dist/commands/check.js +6 -3
  10. package/dist/commands/index.js +5 -0
  11. package/dist/commands/init.js +8 -4
  12. package/dist/commands/key.js +68 -14
  13. package/dist/commands/memory.d.ts +3 -0
  14. package/dist/commands/memory.js +375 -0
  15. package/dist/commands/models.js +17 -1
  16. package/dist/commands/run.js +11 -3
  17. package/dist/commands/sandbox.js +70 -23
  18. package/dist/engine.js +14 -9
  19. package/dist/home.d.ts +2 -2
  20. package/dist/home.js +2 -2
  21. package/dist/keys.d.ts +22 -0
  22. package/dist/keys.js +105 -2
  23. package/dist/lib.d.ts +1 -0
  24. package/dist/lib.js +1 -0
  25. package/dist/liveness.js +11 -0
  26. package/dist/main.js +9 -0
  27. package/dist/podman.js +43 -9
  28. package/dist/projects.js +10 -2
  29. package/dist/resolve.d.ts +4 -0
  30. package/dist/resolve.js +43 -17
  31. package/dist/scaffold.js +22 -1
  32. package/dist/term.d.ts +23 -2
  33. package/dist/term.js +215 -8
  34. package/dist/tui/app.d.ts +10 -0
  35. package/dist/tui/app.js +478 -54
  36. package/dist/tui/theme.d.ts +13 -2
  37. package/dist/tui/theme.js +26 -13
  38. package/dist/tui/wrap.d.ts +92 -0
  39. package/dist/tui/wrap.js +147 -2
  40. package/dist/validate.d.ts +2 -0
  41. package/dist/validate.js +87 -2
  42. package/package.json +2 -2
  43. package/templates/editor/.github/copilot-instructions.md +200 -55
  44. package/templates/editor/.github/prompts/new-agent.prompt.md +5 -2
  45. package/templates/editor/.github/prompts/review-project.prompt.md +5 -0
  46. package/templates/editor/.github/prompts/sync-with-spec.prompt.md +11 -4
  47. package/templates/editor/.github/skills/zen-cli/SKILL.md +3 -0
  48. package/templates/editor/.github/skills/zen-cli/references/faker.md +18 -8
  49. package/templates/editor/.github/skills/zen-cli/references/keys.md +7 -7
  50. package/templates/editor/.github/skills/zen-cli/references/memory.md +113 -0
  51. package/templates/editor/.github/skills/zen-cli/references/rag.md +2 -2
  52. package/templates/editor/.github/skills/zen-cli/references/run.md +3 -1
  53. package/templates/editor/.github/skills/zen-memory/SKILL.md +555 -0
  54. package/templates/editor/.github/skills/zen-rag-docs/SKILL.md +575 -0
  55. package/templates/editor/.github/skills/{api-schema-index → zen-rag-schema}/SKILL.md +4 -4
  56. package/templates/editor/.vscode/settings.json +4 -2
  57. package/templates/parts/exa.prompt.md.tmpl +4 -0
  58. package/templates/parts/exa.spec.md.tmpl +2 -0
  59. package/templates/project/INSTRUCTIONS.md +8 -0
  60. package/templates/project/SPECIFICATION.md.tmpl +47 -0
  61. package/templates/project/agents/prompts/{default.md → default.md.tmpl} +4 -0
  62. package/templates/project/agents.yaml.tmpl +4 -0
  63. package/templates/project/gitignore +7 -1
  64. package/templates/project/scripts/_setup.sh +101 -0
  65. package/templates/project/SPECIFICATION.md +0 -6
@@ -0,0 +1,555 @@
1
+ ---
2
+ name: zen-memory
3
+ description: How agent memory is organised and how to configure it in `agents.yaml` — the `memory:` block, per-agent `access`/`sees`/`writes`/`autoRecall`, the four `memory_*` tools, kinds and relations, how recall ranks and stitches a subgraph, and how to design a memory strategy for a project. Includes what to commit and what never to, how to keep a working file under `/memory` and re-run it later, and how to put memory in front of a `zen rag schema` or `zen rag docs` index so a search that already succeeded once is not paid for again.
4
+ ---
5
+
6
+ # Memory
7
+
8
+ Memory is what an agent knows that nobody wrote down for it. A skill is
9
+ authored, reviewed and versioned; a memory is **produced by a run** and read by
10
+ the runs that follow. Both are knowledge, and they are not interchangeable —
11
+ anything authoritative belongs in a skill, where a human owns it.
12
+
13
+ It is a **graph**, not a list of rows. A node is one thing worth keeping — a
14
+ request, a plan, a fact, a script, a call that was made — and an edge says how
15
+ one led to another. That shape is the point: a script an agent wrote is useless
16
+ without the request that asked for it and the endpoint it calls, and a store
17
+ that can only return a ranked list leaves the joining to the model, every time.
18
+
19
+ It lives with the **project**, not the session. A memory that died with the
20
+ session would be a cache.
21
+
22
+ ## Turning it on
23
+
24
+ Two keys, and the second one is what actually enables anything.
25
+
26
+ ```yaml
27
+ memory: # where the graph lives, and what vectorises it
28
+ dir: memory
29
+ embedding: main
30
+
31
+ agents:
32
+ - name: auditor
33
+ memory: true # this agent gets the tools and auto-recall
34
+ ```
35
+
36
+ That is the whole feature working the obvious way: the agent can search, load
37
+ and commit, and it recalls automatically before answering new user input.
38
+
39
+ The top-level block is optional — an agent that says `memory: true` gets the
40
+ defaults without one. A project that mentions memory nowhere opens no store and
41
+ creates no directory.
42
+
43
+ ### `memory:` — the graph itself
44
+
45
+ | Field | Type | Default | Meaning |
46
+ | ----------- | ------------- | --------------- | ----------------------------------- |
47
+ | `dir` | path | `memory` | Relative to the project root |
48
+ | `embedding` | embedding ref | the top-level | The vectoriser recall searches with |
49
+ | `kinds` | name[] | the seven below | **Replaces** the node vocabulary |
50
+ | `relations` | name[] | the four below | **Replaces** the edge vocabulary |
51
+
52
+ **Set `embedding`.** Without one, recall falls back to term overlap: it finds a
53
+ memory phrased the way the query happened to be phrased and misses the rest,
54
+ which is the failure mode that makes people conclude memory does not work. The
55
+ width is learned from the first response rather than declared, and changing the
56
+ model afterwards is **refused** rather than silently mixing two vector spaces —
57
+ so choose it before the graph has anything in it.
58
+
59
+ ```yaml
60
+ embeddings:
61
+ main: openai:text-embedding-3-small
62
+ memory:
63
+ embedding: main
64
+ ```
65
+
66
+ `zen memory stats` says `embedding none — recall falls back to term overlap`
67
+ when this was skipped, and a vector count below the node count means some nodes
68
+ were committed before an embedder existed.
69
+
70
+ ### `agents[].memory` — who may see and write what
71
+
72
+ ```yaml
73
+ agents:
74
+ - name: auditor
75
+ memory:
76
+ access: read-write
77
+ sees: [audit]
78
+ writes: [audit]
79
+ autoRecall: { limit: 3 }
80
+ ```
81
+
82
+ | Field | Type | Default | Meaning |
83
+ | ------------ | -------------------------- | --------------- | -------------------------------------------- |
84
+ | `access` | `read`/`read-write`/`full` | `read-write` | Which of the four tools this agent gets |
85
+ | `sees` | name[] | `[]` | Private slices it may read, on top of `*` |
86
+ | `writes` | name[] | `[*]` | Labels it may commit under |
87
+ | `autoRecall` | boolean or `{ limit }` | `true`, limit 5 | Recall before a turn that follows user input |
88
+
89
+ `memory: true` is shorthand for all four defaults.
90
+
91
+ ## The four tools
92
+
93
+ They are **not** listed in an agent's `tools:`. They are derived from the
94
+ binding and appear when it exists, so `access` is the only thing that decides
95
+ which of them the model is offered:
96
+
97
+ | Level | Tools |
98
+ | ------------ | ------------------------------- |
99
+ | `read` | `memory_search`, `memory_load` |
100
+ | `read-write` | the above, plus `memory_commit` |
101
+ | `full` | the above, plus `memory_forget` |
102
+
103
+ | Tool | Does |
104
+ | --------------- | -------------------------------------------------------------------------- |
105
+ | `memory_search` | Ranks, then stitches. Returns an outline of a subgraph and a legend of ids |
106
+ | `memory_load` | Reads whole nodes by id. **The only call that counts as use** |
107
+ | `memory_commit` | Writes a subgraph — nodes and the edges between them — in one transaction |
108
+ | `memory_forget` | Removes nodes, their vectors and their file bytes together |
109
+
110
+ Search and load are split on purpose. Search hands back context the model never
111
+ asked for; counting that as use would poison recency ranking. Commit is one
112
+ call because a remembered thing is a subgraph, and building it with three calls
113
+ leaves the graph half-formed when the model stops early.
114
+
115
+ The system prompt already explains all of this to the agent — how to read a
116
+ recollection, when committing is worthwhile, why correction is a new node. That
117
+ text is **derived from `access`**, so an agent that cannot write is never told
118
+ how to. Do not restate it in a prompt; state project-specific policy instead.
119
+
120
+ ## How it is organised
121
+
122
+ ### The vocabulary
123
+
124
+ Seven kinds and four relations, both closed sets, offered to the model as enums.
125
+ That is what keeps a graph built by a language model still queryable a month
126
+ later.
127
+
128
+ | Kind | Is |
129
+ | ------------ | -------------------------------------------------------------- |
130
+ | `task` | a request that started a piece of work |
131
+ | `plan` | an approach that worked, worth reusing on a similar request |
132
+ | `fact` | a durable truth about the environment or the domain |
133
+ | `snippet` | a short piece of code or configuration, too small to be a file |
134
+ | `file` | an artifact kept whole under `/memory` and re-runnable |
135
+ | `operation` | an external call that was made — an endpoint, command or query |
136
+ | `preference` | a standing instruction from the user that applies to every run |
137
+
138
+ | Relation | Reads as | Effect on recall |
139
+ | ------------ | --------------------------------------------------------- | ------------------------------- |
140
+ | `PRODUCED` | the source led to the target being made (task → artifact) | spine — pulled in first |
141
+ | `CALLS` | the source invokes the target | spine |
142
+ | `SUPERSEDES` | the source replaces the target; the target is stale | spine, and **hides** the target |
143
+ | `INFORMED` | the source was context the target was built from | background — dropped first |
144
+
145
+ Override the vocabularies only if the defaults genuinely do not fit the domain.
146
+ A wider vocabulary is a vaguer one, and a kind nothing else understands is a
147
+ kind no other agent can search for. `kinds` **replaces** the built-in set, with
148
+ one exception: `preference` is always available, because the engine itself reads
149
+ that kind to build the system prompt.
150
+
151
+ ### Every node
152
+
153
+ | Field | Why it matters |
154
+ | ------------------------ | ----------------------------------------------------------- |
155
+ | `text` | the only thing ranked, and the only thing embedded |
156
+ | `kind` | filters a search, and decides how it renders |
157
+ | `audience` | who may see it. `['*']` is everyone; denied by default |
158
+ | `createdAt` | provenance, and what `newer_than` filters on |
159
+ | `lastUsedAt`, `useCount` | bumped by `memory_load` alone — the input to recency |
160
+ | `revision` | optimistic concurrency, via `expected_revision` on a commit |
161
+ | `file` | the remembered bytes, if any |
162
+
163
+ ### How recall actually works
164
+
165
+ 1. **Rank.** The query is embedded and compared against node texts. Anything
166
+ scoring below a fixed floor of `0.15` is noise and is dropped. Superseded
167
+ nodes are excluded, and so are `preference` nodes unless asked for by kind.
168
+ 2. **Order by recency, but do not admit by it.** Relevance alone clears the
169
+ threshold; decay by `lastUsedAt` only breaks the tie afterwards. Memory that
170
+ keeps proving useful surfaces ahead of memory written once and never read —
171
+ without anything ageing out of reach however well it matches.
172
+ 3. **Stitch.** Breadth-first from every seed at once, ignoring edge direction,
173
+ because a memory is as often reached from the artifact back to the request as
174
+ the other way.
175
+ 4. **Spend the budget in tiers**: seeds, then the spine (`PRODUCED`, `CALLS`,
176
+ `SUPERSEDES`), then `INFORMED` context. A tight budget therefore degrades by
177
+ dropping background rather than by truncating the answer.
178
+
179
+ Defaults: 5 seeds, 2 hops, 25 nodes. `memory_search` takes `limit`, `max_hops`,
180
+ `max_nodes`, `kinds` and `newer_than` to move them.
181
+
182
+ What comes back is a `<memory-recollection>` block: an indented outline where
183
+ the indentation _is_ the graph, each line reading `score kind id` or
184
+ `→relation kind id`, with each node's text under it **clipped to 160
185
+ characters**. That clip is a design constraint on how you write a node — see
186
+ below.
187
+
188
+ ### On disk
189
+
190
+ ```
191
+ <project>/memory/
192
+ ├── manifest.json written LAST — a half-written memory reads as empty
193
+ ├── graph.json nodes and edges, parsed whole on open
194
+ ├── vectors.f32 raw little-endian float32, a quarter the size of JSON
195
+ ├── vectors.json the ids those vectors belong to
196
+ ├── files/ remembered bytes, one per node that has any
197
+ └── .lock held while a run is writing; a dead pid's lock is stale
198
+ ```
199
+
200
+ It is project state, not source, and it is not committed. The directory is
201
+ mounted **read-only** at `/memory` in the agent's namespace.
202
+
203
+ ## Designing a memory strategy
204
+
205
+ ### The test for what to remember
206
+
207
+ **Would the next run otherwise have to work this out again?** That is the whole
208
+ question. A script that ran, an interface that behaved differently from its
209
+ documentation, a call sequence that turned out to be necessary — yes. The
210
+ request restated, progress notes, anything unverified, anything a search already
211
+ returned — no.
212
+
213
+ The second test is **would it still be true next month**. Memory has no expiry
214
+ and no reviewer. A fact about the environment survives; a fact about today's
215
+ data does not, and committing one is how a graph becomes confidently wrong.
216
+
217
+ ### Write the text for the question, not for the answer
218
+
219
+ Recall ranks a node's `text` against **the user's next message**, because that
220
+ is what auto-recall embeds. So the text has to be phrased the way somebody will
221
+ ask for it, not the way the system that produced it names it.
222
+
223
+ | Instead of | Write |
224
+ | --------------------------------- | ------------------------------------------------------------------------------- |
225
+ | `POST /v2/subs/{id}:cancel — 204` | `Cancelling a subscription: POST /v2/subs/{id}:cancel, 204, no body` |
226
+ | `fix for the 409` | `A duplicate customer email returns 409, not 422 — retry is wrong` |
227
+ | `script.py` | `Weekly invoice reconciliation script — reads billing CSV, writes a diff table` |
228
+
229
+ Front-load the identifying facts: the first 160 characters are what appears in
230
+ a recollection, and everything after that costs a `memory_load` to see.
231
+
232
+ ### The shape of a good commit
233
+
234
+ One call, several nodes, and the edges that explain them:
235
+
236
+ ```
237
+ task "reconcile invoices against the ledger for a month"
238
+ PRODUCED → file the script that did it
239
+ INFORMED ← fact "ledger exports use minor units; invoices use decimals"
240
+ file
241
+ CALLS → operation "GET /v2/invoices?created[gte]= — cursor paging"
242
+ ```
243
+
244
+ Committed separately, a later run finds a script with no idea why it exists, or
245
+ a fact with nothing that acts on it. Committed together, one hit on any of them
246
+ brings the rest.
247
+
248
+ **Never edit a memory to correct it.** Commit the new one and link it to the old
249
+ with `SUPERSEDES`. Recall follows the edge forward and hides what it points at,
250
+ so the wrong answer stops being served without being destroyed — and the reason
251
+ a thing changed is often the useful part. `zen memory ls --stale` is the list of
252
+ corrections that have happened.
253
+
254
+ A new node whose text is a near-duplicate (cosine ≥ 0.97) of one already there
255
+ is **folded into it**, and the existing id comes back under the model's ref, so
256
+ the links still land on the memory that was already present.
257
+
258
+ ### Audiences: one graph, masked — not one graph each
259
+
260
+ Every node carries an `audience`; `sees` is the set an agent reads. `*` is the
261
+ public slice and is always included, so a binding can only ever **widen** what
262
+ an agent sees, never hide the common ground. An invisible node is
263
+ indistinguishable from a missing one, deliberately, so a mask cannot be probed
264
+ by id.
265
+
266
+ Decide audiences before you decide agents. The useful question is not "which
267
+ agent wrote this" but "which agent would be misled by this":
268
+
269
+ | Situation | Configuration |
270
+ | ------------------------------------------------- | ----------------------------------------- |
271
+ | Everything is common ground | `memory: true` everywhere. Do this first |
272
+ | A specialist accumulates jargon nobody else needs | `sees: [x]`, `writes: [x]` on that agent |
273
+ | A reviewer should read a slice but not add to it | `sees: [x]`, `writes: []`, `access: read` |
274
+ | A summariser should never write at all | `access: read` |
275
+ | One curator may delete | `access: full` on exactly one agent |
276
+
277
+ When `writes` names exactly one label, the tool schema omits the field and
278
+ applies it — there is nothing to choose and nothing for the model to get wrong.
279
+ Neither `sees` nor `writes` is ever a tool parameter: an agent that could name
280
+ its own audience could read another agent's slice just by asking.
281
+
282
+ Keep `access: full` rare. Forgetting is almost never the right correction, and a
283
+ model that can delete will occasionally tidy.
284
+
285
+ ### Auto-recall
286
+
287
+ On by default, because an agent that has to remember to go looking mostly does
288
+ not. It fires **after new user input and after a hand-off** — not before every
289
+ call, because recalling on every turn costs tokens and defeats prompt caching
290
+ for a marginal gain. A turn following a tool result does not re-recall.
291
+
292
+ | Setting | For |
293
+ | ---------------- | ------------------------------------------------------------------------ |
294
+ | `true` (default) | Almost everything |
295
+ | `{ limit: 2 }` | A cheap model, or a graph big enough that five seeds is a page |
296
+ | `{ limit: 8 }` | A research agent where breadth beats prompt size |
297
+ | `false` | An agent that should decide for itself when to look — it keeps the tools |
298
+
299
+ `autoRecall: false` is right for a tool-driven agent whose first turn is
300
+ mechanical, and for anything where the user's message is a payload rather than a
301
+ question.
302
+
303
+ ### Preferences
304
+
305
+ A `preference` node is a standing instruction — "always report findings as a
306
+ table", "use ISO dates". These are **not recalled by similarity**, because they
307
+ are not about the request in front of the agent: an instruction on how to report
308
+ is nowhere near a question about gateway rules in embedding space, and a search
309
+ would never surface it.
310
+
311
+ So they are listed rather than ranked, and rendered into the system prompt with
312
+ their ids:
313
+
314
+ ```
315
+ <memory-preferences>
316
+ - report findings as a table with a severity column [01JD9Q7X8N2K4M6P8R0T2V4W6Y]
317
+ </memory-preferences>
318
+ ```
319
+
320
+ They land in the cached prefix, so they cost once per run rather than once per
321
+ turn, and the id is what lets a model replace one with `SUPERSEDES` instead of
322
+ quietly ignoring it. They are left out of ordinary recall on purpose — they are
323
+ in the prompt already.
324
+
325
+ Commit one only when the user's own words generalise: "always", "from now on",
326
+ "I prefer", "never". A single request being fulfilled is not a standing
327
+ instruction, and a graph full of false preferences is a system prompt nobody
328
+ wrote.
329
+
330
+ ## Remembering files, and running them again
331
+
332
+ The workspace is disposable — it may be a container about to be discarded — so a
333
+ reference to a path in it dangles the moment the run ends. `memory_commit` takes
334
+ a `file` argument on a node instead, and copies the **bytes** out:
335
+
336
+ ```json
337
+ {
338
+ "nodes": [
339
+ {
340
+ "ref": "s",
341
+ "kind": "file",
342
+ "text": "Invoice reconciliation script — reads the billing CSV, writes a diff table",
343
+ "file": "reconcile.py"
344
+ },
345
+ { "ref": "t", "kind": "task", "text": "reconcile invoices against the ledger for March" }
346
+ ],
347
+ "edges": [{ "from": "t", "to": "s", "relation": "PRODUCED" }]
348
+ }
349
+ ```
350
+
351
+ The path is the one the **file tools** use, resolved the same way. The copy
352
+ lands at `<project>/memory/files/<id>.<ext>` and appears to the agent as:
353
+
354
+ ```
355
+ /memory/01JD9Q7X8N2K4M6P8R0T2V4W6Y.py
356
+ ```
357
+
358
+ | Rule | Value |
359
+ | --------- | ---------------------------------------------------------------- |
360
+ | Size cap | 2 MiB. Over it, keep the artifact and commit a `snippet` instead |
361
+ | Inlined | under 32 KiB — `memory_load` returns the contents |
362
+ | Larger | `memory_load` returns the path; open or run it |
363
+ | Mount | `/memory`, **read-only** |
364
+ | Extension | preserved, because it decides how the file runs and renders |
365
+
366
+ Reusing one is the point of keeping it:
367
+
368
+ ```
369
+ run_command python3 /memory/01JD9Q7X8N2K4M6P8R0T2V4W6Y.py
370
+ ```
371
+
372
+ Which means the file has to be **self-contained**. A script that reads
373
+ `../data/latest.csv` or imports from the workspace will not run next week; one
374
+ that takes its inputs as arguments will. If it needs a credential, it should
375
+ read it from the environment and say so in the node text.
376
+
377
+ Nothing writes to `/memory` through the file tools, so every file under it has a
378
+ node explaining what it is and the graph cannot acquire orphans. To change a
379
+ remembered file: copy it into the workspace, edit it there, commit the new one
380
+ and link it with `SUPERSEDES` to the old.
381
+
382
+ | Commit as | When |
383
+ | --------- | ------------------------------------------------------------------------- |
384
+ | `file` | It is runnable, or it is the artifact itself: a script, a config, a query |
385
+ | `snippet` | It is a few lines that only make sense read: a request body, a fragment |
386
+ | `fact` | It is a sentence. A file containing one sentence is a fact in a costume |
387
+
388
+ ## Memory in front of a `zen rag` index
389
+
390
+ This is the highest-value thing memory does in a project that has an index, and
391
+ it is entirely a matter of **what you commit**.
392
+
393
+ ### The division of labour
394
+
395
+ | | A schema/docs index | Memory |
396
+ | --------------- | --------------------------------------------------------------- | --------------------------------- |
397
+ | Holds | the API or the documents, completely | what earlier runs worked out |
398
+ | Authority | the source of truth | a record, never a substitute |
399
+ | Completeness | exact and countable | a ranked, masked slice |
400
+ | Freshness | as fresh as the last `index` | as stale as it was allowed to get |
401
+ | Cost of a query | one embedding round trip (`search`), or nothing (`list`/`grep`) | already in the prompt |
402
+
403
+ So: **never copy index content into memory.** A schema pasted into a node goes
404
+ stale silently and the index already answers in milliseconds; a documentation
405
+ passage quoted into a node outlives the release it was true for and will look
406
+ exactly as authoritative when it is wrong.
407
+
408
+ What memory should hold is the **route** — the resolution, not the source:
409
+
410
+ | Commit | Kind | Because |
411
+ | --------------------------------------------------------------------------------- | ------------------ | --------------------------------------------------------------------------------- |
412
+ | The endpoint that turned out to be the answer, and its shape | `operation` | A `search_api` costs a round trip and returns five ranked guesses; this names one |
413
+ | The sequence of calls that was necessary | `plan` | No ranking finds an ordering. It is not in the document |
414
+ | Where the answer lives in the docs — document name, heading, line range | `fact` | `zen rag docs show` reads it back verbatim and current |
415
+ | A convention the index cannot state — auth, casing, base path, the error envelope | `fact` | It is not in the graph, and it is wrong to re-derive it every run |
416
+ | Where the index itself is | `fact` | `-d` or `$ZEN_SCHEMA_DB`. An agent cannot guess a path |
417
+ | A working request body, or a script that drives the API | `snippet` / `file` | The thing you would otherwise write again |
418
+ | That a search returned nothing, and `grep` confirmed absence | `fact` | Proving a negative twice is pure waste |
419
+
420
+ ### Why it makes search cheaper
421
+
422
+ Auto-recall fires **before the model's first tool call of the turn**. So the
423
+ recollection arrives before the decision to search is made — and a recalled
424
+ `operation` node naming `POST /v2/subs/{id}:cancel` removes the search
425
+ entirely, rather than making it faster. When the recollection is only close, it
426
+ still narrows: the model reaches for `list_api --name` or `grep_api` instead of
427
+ `search_api`, which are local, exact, and cost nothing.
428
+
429
+ Phrase these nodes accordingly. The next question will arrive as _"how do I
430
+ cancel someone's subscription?"_, not as `cancelSubscription`, so the node text
431
+ must contain the human phrasing **and** the identifiers. Put the question first
432
+ and the answer second.
433
+
434
+ ### Keeping it honest against re-indexing
435
+
436
+ An index is rebuilt when the source changes; memory is not. Two habits keep the
437
+ two from drifting:
438
+
439
+ 1. **Name the version in the node text** — "Billing API v2", "acme\_4.1.0 docs".
440
+ A recollection about a version the project no longer uses is then visibly
441
+ about the wrong thing rather than invisibly wrong.
442
+ 2. **Supersede after a re-index.** When a route changes, commit the new
443
+ `operation` node with `SUPERSEDES` pointing at the old one. `zen memory ls
444
+ --kind operation` is the review list.
445
+
446
+ And state the rule in the project's own skill for the index: **verify against
447
+ the index before acting on a recalled route.** Recall is a shortcut past the
448
+ search, not past the truth. One `list_api --name` or `zen rag docs show` is
449
+ cheap and settles it.
450
+
451
+ ### The project skill is where this is written down
452
+
453
+ An index used by a project needs a skill in that project describing it — see
454
+ `zen-rag-schema` and `zen-rag-docs`. Memory policy belongs in the same skill,
455
+ because it is the same subject:
456
+
457
+ ```md
458
+ - Recalled routes are a shortcut, not authority. Confirm with `list_api --name`
459
+ before writing a call against one.
460
+ - After solving something the index made you work for, commit it: the
461
+ `operation` you ended on, linked to the `task` that asked and any `fact` you
462
+ had to discover. Phrase the text as the question, not as the operation id.
463
+ - Never commit schema text or documentation passages. Commit the pointer —
464
+ document, heading, line range — and read it back with `zen rag docs show`.
465
+ ```
466
+
467
+ ## Inspecting and repairing it
468
+
469
+ ```
470
+ zen memory [stats|ls|show|export|forget] [args] [options]
471
+ ```
472
+
473
+ `zen memory export --open` is the one to reach for: one self-contained HTML
474
+ page with every node, the graph as a diagram, and the remembered file's actual
475
+ content in the detail pane. It reads **unmasked** — when the mask is what is
476
+ wrong, the hidden part is exactly the part you need — and it contacts no model,
477
+ so inspection is free and offline.
478
+
479
+ | Question | Command |
480
+ | ---------------------------------------- | -------------------------------- |
481
+ | Is memory even on, and is it embedded? | `zen memory stats` |
482
+ | What has this project learned? | `zen memory export --open` |
483
+ | What is in one private slice? | `zen memory ls --audience audit` |
484
+ | What has been corrected? | `zen memory ls --stale` |
485
+ | What files are being kept? | `zen memory ls --files` |
486
+ | Why was that recalled? | `zen memory show <id>` |
487
+ | That should never have been written down | `zen memory forget <id>` |
488
+
489
+ `zen inspect` answers the other half: a run report shows the recollection block
490
+ exactly as the model received it, which is how you tell "memory had nothing"
491
+ apart from "memory had it and the model ignored it".
492
+
493
+ ## A worked configuration
494
+
495
+ ```yaml
496
+ embeddings:
497
+ main: openai:text-embedding-3-small
498
+
499
+ memory:
500
+ dir: memory
501
+ embedding: main
502
+
503
+ agents:
504
+ - name: triage # front door: reads everything, writes the common ground
505
+ memory: true
506
+
507
+ - name: api-worker # does the work; keeps its own routes and scripts
508
+ memory:
509
+ access: read-write
510
+ sees: [api]
511
+ writes: [api]
512
+ autoRecall: { limit: 8 }
513
+
514
+ - name: reporter # formats the answer; must not invent memories
515
+ memory:
516
+ access: read
517
+ autoRecall: false
518
+
519
+ - name: curator # the only thing that may delete
520
+ memory:
521
+ access: full
522
+ sees: [api]
523
+ ```
524
+
525
+ ## Review checklist
526
+
527
+ - [ ] `memory.embedding` is set, and was set before the graph had content.
528
+ - [ ] Every agent that should learn has a `memory:` binding — the top-level
529
+ block alone enables nothing.
530
+ - [ ] `access: full` appears at most once.
531
+ - [ ] `sees`/`writes` exist because an agent would be **misled** by the other
532
+ slice, not merely because it does not need it.
533
+ - [ ] `autoRecall: false` on any agent whose user message is a payload.
534
+ - [ ] Nothing authoritative is expected to live here — that is a skill's job.
535
+ - [ ] The project's index skill says to verify a recalled route.
536
+ - [ ] `zen memory export --open` after a week of use, to see what it actually
537
+ learned rather than what you hoped.
538
+
539
+ ## When it goes wrong
540
+
541
+ | Symptom | Cause |
542
+ | ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
543
+ | The `memory_*` tools are missing | No `memory:` on the agent. They come from the binding, never from `tools:` |
544
+ | `zen memory` says the project has none | Neither a `memory:` block nor an agent binding — nothing is opened and no directory is made |
545
+ | Recall finds a memory only when reworded | No embedder. `zen memory stats` says `embedding none` |
546
+ | Recall finds nothing after changing the embedder | Refused rather than mixed — a manifest records the model. Re-embed or change it back |
547
+ | Vectors fewer than nodes | Some nodes were committed with no embedder; they are only reachable by term overlap |
548
+ | The graph fills with restated requests | The commit rule is not in a prompt or skill. State the "would a later run redo this" test |
549
+ | A wrong memory keeps coming back | It was edited instead of superseded, or superseded in the wrong direction — the **new** node is the source |
550
+ | An agent cannot see a node you can | Its `audience` is not in that agent's `sees`. Invisible and missing are the same thing, on purpose |
551
+ | "this agent cannot remember files" | The agent has no workspace, so a path cannot be resolved. Commit without `file` |
552
+ | A remembered file will not run | It is not self-contained — it referenced the workspace it was written in |
553
+ | Recollections are large and unhelpful | `autoRecall.limit` is too high, or node texts describe answers rather than questions |
554
+ | The model recalls a route the API no longer has | Nothing re-checks memory against a rebuilt index. Supersede, and require verification |
555
+ | A second run of the same project refuses to start | The directory lock. A lock whose process is gone is stale and is taken over |