memoir-cli 3.11.3 → 3.14.0

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 (76) hide show
  1. package/README.md +129 -124
  2. package/bin/memoir-work.js +9 -0
  3. package/bin/memoir.js +72 -8
  4. package/docs/AUDIT-REMEDIATION.md +55 -0
  5. package/docs/CASE_TAPE_AMNESIA.md +39 -0
  6. package/docs/HANDOFF-SECURITY-AUDIT.md +106 -0
  7. package/docs/LOCAL-HANDOFF-VALIDATION.md +129 -0
  8. package/docs/MCP-V2-MIGRATION.md +17 -0
  9. package/docs/PROJECT-HANDOFF.md +255 -0
  10. package/docs/PROJECT-VIEW-DEBUG.md +66 -0
  11. package/docs/PROJECT-VIEW-VALIDATION.md +136 -0
  12. package/docs/RELEASE-3.14-VALIDATION.md +36 -0
  13. package/docs/RELIABILITY-ROLLOUT.md +57 -0
  14. package/docs/RETRIEVAL-INDEX.md +45 -0
  15. package/docs/RETRIEVAL-RESULTS.md +26 -0
  16. package/docs/SPEC.md +684 -0
  17. package/evals/CONTINUITY-PROTOCOL.md +45 -0
  18. package/evals/cases.json +200 -0
  19. package/evals/results/retrieval-2026-09-05.json +5333 -0
  20. package/evals/retrieval-performance.mjs +99 -0
  21. package/evals/run.mjs +87 -0
  22. package/package.json +13 -5
  23. package/src/adapters/index.js +13 -6
  24. package/src/adapters/restore.js +83 -36
  25. package/src/cloud/auth.js +12 -15
  26. package/src/cloud/constants.js +6 -2
  27. package/src/cloud/storage.js +130 -93
  28. package/src/commands/activate.js +43 -9
  29. package/src/commands/cloud.js +56 -5
  30. package/src/commands/consolidate.js +49 -10
  31. package/src/commands/diff.js +2 -2
  32. package/src/commands/doctor.js +3 -3
  33. package/src/commands/forget.js +100 -0
  34. package/src/commands/push.js +164 -161
  35. package/src/commands/recall.js +42 -0
  36. package/src/commands/restore.js +32 -44
  37. package/src/commands/resume.js +15 -164
  38. package/src/commands/session.js +51 -9
  39. package/src/commands/snapshot.js +6 -7
  40. package/src/commands/status.js +23 -1
  41. package/src/commands/upgrade.js +13 -11
  42. package/src/commands/validate.js +16 -0
  43. package/src/commands/view.js +2 -2
  44. package/src/commands/why.js +4 -3
  45. package/src/config.js +9 -40
  46. package/src/context/capture.js +135 -33
  47. package/src/context/handoffs.js +72 -0
  48. package/src/events/summary.js +122 -0
  49. package/src/integrations/setup.js +88 -0
  50. package/src/mcp.js +151 -283
  51. package/src/memory/lexical-index.js +65 -0
  52. package/src/memory/repository.js +16 -0
  53. package/src/memory/scope.js +65 -0
  54. package/src/memory/search.js +598 -0
  55. package/src/memory/store.js +141 -0
  56. package/src/providers/index.js +182 -51
  57. package/src/providers/restore.js +5 -1
  58. package/src/security/encryption.js +34 -60
  59. package/src/security/files.js +155 -0
  60. package/src/session/brief.js +47 -0
  61. package/src/session/inject.js +12 -6
  62. package/src/session/lock.js +39 -118
  63. package/src/session/migrations.js +6 -0
  64. package/src/session/render.js +34 -4
  65. package/src/session/state.js +305 -34
  66. package/src/work/cli.js +64 -0
  67. package/src/work/errors.js +8 -0
  68. package/src/work/server.js +28 -0
  69. package/src/work/setup.js +96 -0
  70. package/src/work/store.js +340 -0
  71. package/src/work/ui/app.js +205 -0
  72. package/src/work/ui/index.html +30 -0
  73. package/src/work/ui/style.css +3 -0
  74. package/src/work/view.js +93 -0
  75. package/src/workspace/tracker.js +84 -332
  76. package/supabase/migrations/202609050001_backup_versions.sql +50 -0
package/docs/SPEC.md ADDED
@@ -0,0 +1,684 @@
1
+ # The memoir format, v0.1.2 (draft)
2
+
3
+ **Status:** Draft. Version 0.1.1. Seeking implementations and critique.
4
+
5
+ This document specifies the memoir format: an open, portable, plain-text
6
+ format for an AI assistant's accumulated working context — what it has
7
+ learned, decided, been corrected on, and is in the middle of doing.
8
+
9
+ The format is extracted from a working implementation
10
+ ([memoir-cli](https://github.com/camgitt/memoir), MIT), not designed on a
11
+ whiteboard. Where this document and shipped behavior disagree, that is a bug
12
+ in this document; please file it. Normative words (MUST, SHOULD, MAY) are
13
+ used in the RFC 2119 sense.
14
+
15
+ ---
16
+
17
+ ## 1. Why an open format
18
+
19
+ Every AI vendor is shipping memory, and every one of them is shipping it
20
+ locked in. Memory is the strongest lock-in there is, because it compounds:
21
+ the assistant that knows your preferences, your past decisions and their
22
+ reasons, and the mistakes it must not repeat is worth more every week — and
23
+ none of that accumulates anywhere you can take with you. Email had the same
24
+ problem, and solved it with open formats and protocols (mbox, Maildir, IMAP):
25
+ your mail is yours, in files and mailboxes any client can read, and clients
26
+ compete on experience rather than on holding your archive hostage.
27
+
28
+ The memoir format is that seat for AI working context. It deliberately covers
29
+ *experience*, not just facts: a decision with its rationale and the
30
+ alternative that was rejected; a lesson with the trigger that should fire it
31
+ and the behavior change it demands; the live working set of goals and next
32
+ actions that lets a different tool — or the same tool on a different machine —
33
+ pick up mid-thought. It is plain markdown and JSON on disk, owned by the
34
+ user, versioned however the user versions files. Any tool that can read a
35
+ directory can read it. That is the whole point.
36
+
37
+ ## 2. The store
38
+
39
+ A **memoir store** is:
40
+
41
+ 1. a directory of **entry files** — one entry per file, markdown with YAML
42
+ frontmatter (section 3), and
43
+ 2. a **session state file** — a single JSON document holding the live working
44
+ set (section 5).
45
+
46
+ ```
47
+ <store>/
48
+ memory/
49
+ decision_local-db-choice.md
50
+ lesson_migrations-staging-first.md
51
+ preference_commit-style.md
52
+ fact_staging-endpoints.md
53
+ ...
54
+ session.json
55
+ ```
56
+
57
+ The reference implementation currently keeps these in two places for
58
+ tool-compatibility reasons (entry files where the host assistant reads its
59
+ memory, e.g. `~/.claude/projects/<project-slug>/memory/`; session state at
60
+ `~/.config/memoir/session.json`). The format does not care where the store
61
+ lives. A store MAY be a git repository; the format is designed so that it
62
+ diffs and merges well when it is (section 4.2).
63
+
64
+ Entry filenames SHOULD be lowercase slugs and SHOULD carry the entry type as
65
+ a prefix (`decision_`, `lesson_`, ...), purely for human scanability. The
66
+ filename (without `.md`) is the entry's **link name**: other entries
67
+ reference it with `[[wikilinks]]` in their body text.
68
+
69
+ ## 3. Entry files
70
+
71
+ ### 3.1 File format
72
+
73
+ An entry file is UTF-8 markdown. It SHOULD begin with a YAML frontmatter
74
+ block delimited by `---` lines. Everything after the closing `---` is the
75
+ **body**: free markdown, as long or short as the entry deserves.
76
+
77
+ Plain text is a feature, not a compromise, and implementations MUST NOT
78
+ "upgrade" the store to a database as the canonical form:
79
+
80
+ - **Human-readable.** The user can open, audit, and edit their AI's memory
81
+ with any editor. Memory the user cannot read is memory the user cannot
82
+ trust or correct.
83
+ - **Git-diffable.** One entry per file means a change to one memory is a
84
+ one-file diff. History, blame, review, and rollback come free from tools
85
+ the user already has.
86
+ - **Mergeable.** Text files union across machines with well-understood
87
+ conflict surfaces (section 4). A database file is an opaque blob that can
88
+ only be replaced wholesale — which is exactly the failure mode this format
89
+ exists to avoid.
90
+ - **Durable.** Markdown will be readable in twenty years by tools that have
91
+ never heard of memoir.
92
+
93
+ Implementations MAY build derived indexes (embeddings, full-text, SQLite
94
+ caches) but these are caches: the files are the truth, and a conforming
95
+ implementation MUST be able to rebuild any index from the files alone.
96
+
97
+ The frontmatter is restricted to a simple YAML subset so that it can be
98
+ parsed without a full YAML engine: scalar `key: value` pairs, one level of
99
+ nested mapping, and simple `- item` string lists. Writers MUST NOT emit
100
+ anchors, multi-line block scalars, or deeper nesting in frontmatter; prose
101
+ belongs in the body.
102
+
103
+ ### 3.2 Common frontmatter fields
104
+
105
+ | Field | Requirement | Meaning |
106
+ |---|---|---|
107
+ | `type` | REQUIRED | One of the six entry types (3.3). |
108
+ | `name` | REQUIRED | Human-readable title (or slug) for the entry. |
109
+ | `description` | SHOULD | One line. Indexes and pickers render this; an entry without one is invisible in lists. |
110
+ | `created` | SHOULD | ISO 8601 date or date-time the entry was first written. |
111
+ | `updated` | MAY | ISO 8601 date or date-time of last substantive edit. |
112
+ | `schema_version` | MAY | Integer. Absent means `1`. See section 6. |
113
+ | `project` | MAY | Project this entry is scoped to; absent means global. |
114
+ | `tags` | MAY | List of strings. |
115
+ | `aliases` | MAY | List of strings: other names, nicknames, or phrasings a reader might search for this entry under (e.g. an entry about a "vertical swipe feed" surface: `tiktok`, `reels`, `/tape`). Retrieval SHOULD weight this field above body prose. Concept-level recall in a plain-text format is a write-time job — the writer knows what else the thing is called; a substring search later does not. |
116
+ | `origin` | MAY | Mapping with any of `tool`, `session_id`, `machine_id` — where the entry came from. |
117
+
118
+ Readers MUST ignore fields they do not recognize. Writers MUST preserve
119
+ fields they do not recognize when rewriting an entry. (Unknown fields are how
120
+ the format extends without breaking deployed tools.)
121
+
122
+ ### 3.3 Entry types
123
+
124
+ Six types. A type answers the question "what should a tool *do* with this
125
+ entry when it loads?" — inform (fact), constrain style (preference), prevent
126
+ relitigation (decision), change behavior (lesson), orient (goal), or resume
127
+ work (next_action).
128
+
129
+ Machine-readable versions of every constraint in this section are in
130
+ [`schema/entry.schema.json`](../schema/entry.schema.json).
131
+
132
+ #### 3.3.1 `fact`
133
+
134
+ Something true about the world, the project, or the environment, worth
135
+ loading into future context. Facts are the baseline type: when in doubt, an
136
+ entry is a fact.
137
+
138
+ Extra fields: `source` (MAY) — where the fact was established.
139
+
140
+ ```markdown
141
+ ---
142
+ type: fact
143
+ name: Staging API endpoints
144
+ description: Base URLs and auth mode for the staging environment
145
+ created: 2026-05-14
146
+ project: acme-api
147
+ ---
148
+ Staging runs at `https://staging.api.example.com`, bearer-token auth,
149
+ tokens minted by `scripts/mint-token.sh`. Rate limit is 60 req/min and it
150
+ is enforced. See [[decision_local-db-choice]] for why staging has no
151
+ Postgres.
152
+ ```
153
+
154
+ #### 3.3.2 `preference`
155
+
156
+ How the user wants things done. Preferences are standing instructions:
157
+ tone, formatting, workflow, tooling defaults.
158
+
159
+ Extra fields: `scope` (MAY) — `global` or `project`.
160
+
161
+ ```markdown
162
+ ---
163
+ type: preference
164
+ name: Commit message style
165
+ description: Imperative mood, no trailers, reference the issue number
166
+ scope: global
167
+ ---
168
+ Commit subjects in imperative mood ("Add retry", not "Added retry").
169
+ No generated trailers. Reference the issue as `(#123)` at the end of the
170
+ subject when one exists.
171
+ ```
172
+
173
+ #### 3.3.3 `decision`
174
+
175
+ Something that was decided, recorded so it does not get silently relitigated
176
+ three sessions later. A decision's value is almost entirely in its `why` and
177
+ its `rejected` alternative — "we use Postgres" is trivia; "we use Postgres
178
+ *because X*, and we rejected SQLite *because Y*" is experience.
179
+
180
+ Extra fields:
181
+
182
+ | Field | Requirement | Meaning |
183
+ |---|---|---|
184
+ | `date` | SHOULD | When it was decided (ISO 8601). Merge recency key (4.2). |
185
+ | `why` | SHOULD (strongly — see below) | The rationale. |
186
+ | `rejected` | SHOULD | The alternative considered and rejected, and why it lost. |
187
+ | `hidden` | MAY | `true` marks an absolute tombstone (4.3.1). |
188
+ | `hidden_at` | MUST when `hidden: true` | When it was tombstoned. |
189
+ | `superseded_by` | MAY | Link name of the decision that replaced this one. |
190
+
191
+ `why` and `rejected` are *required-encouraged*: their absence is not a
192
+ validation error (auto-captured decisions legitimately lack them at capture
193
+ time), but interactive writers SHOULD prompt for both, and a decision entry
194
+ without a `why` SHOULD be flagged by validators as a warning. A decision
195
+ without a why is half a decision.
196
+
197
+ > Naming note: the field is `rejected`, not `rejected_alternative` — this
198
+ > matches every deployed store and the reference implementation's session
199
+ > records. The longer name lost to reality.
200
+
201
+ ```markdown
202
+ ---
203
+ type: decision
204
+ name: Local store is SQLite, not Postgres
205
+ description: Single-file embedded DB for the local cache; Postgres rejected
206
+ date: 2026-05-02
207
+ why: Zero-dependency install matters more than concurrent writers for a per-user local cache.
208
+ rejected: Postgres — better concurrency, but a daemon dependency in a CLI install is a support burden we measured and declined.
209
+ ---
210
+ Applies to the local derived cache only; the canonical store stays plain
211
+ files per [[fact_staging-endpoints]] discussion.
212
+ ```
213
+
214
+ #### 3.3.4 `lesson`
215
+
216
+ A behavior correction learned from experience — usually from a mistake. A
217
+ lesson is the format's most valuable and most structured type: it must say
218
+ *when it applies* (`trigger`) and *what to do differently*
219
+ (`how_to_apply`), or it is an anecdote, not a lesson.
220
+
221
+ Extra fields:
222
+
223
+ | Field | Requirement | Meaning |
224
+ |---|---|---|
225
+ | `trigger` | REQUIRED | The situation that should fire this lesson ("before deleting a directory", "when a test needs `$HOME`"). |
226
+ | `how_to_apply` | REQUIRED | The concrete behavior change. |
227
+ | `fired_count` | MAY | Integer ≥ 0. Times this lesson demonstrably fired and changed behavior. |
228
+ | `last_fired` | MAY | ISO 8601 date-time it last fired. |
229
+
230
+ `fired_count` / `last_fired` are the hook for the feedback loop (a later
231
+ phase of this spec's roadmap): once tools report firings, lessons acquire an
232
+ outcome record — which lessons actually prevent repeats, and which are dead
233
+ weight a consolidation pass can retire. Writers MAY maintain them; readers
234
+ MUST NOT require them.
235
+
236
+ The body SHOULD carry the evidence: what happened, what it cost, why the
237
+ rule is what it is. Established convention (predating this spec) renders
238
+ these as bold-label sections — `**Why:**` and `**How to apply:**` — in the
239
+ body; validators MUST accept `how_to_apply` expressed as a
240
+ `**How to apply:**` body section in legacy entries (Appendix A).
241
+
242
+ ```markdown
243
+ ---
244
+ type: lesson
245
+ name: Rehearse destructive migrations on a copy first
246
+ description: Never run a schema migration against the only copy of the data
247
+ trigger: About to run a migration that drops, renames, or rewrites columns
248
+ how_to_apply: Restore the latest backup to a scratch database and run the full migration train there first; only then touch the real one.
249
+ created: 2026-04-11
250
+ ---
251
+ **Why:** A rename-and-backfill migration was run directly against the only
252
+ database; the backfill had an off-by-one and the pre-rename column was
253
+ already gone. Recovery took a day. A five-minute rehearsal on a restored
254
+ copy would have caught it.
255
+
256
+ **How to apply:** Restore latest backup → scratch DB → run the FULL
257
+ migration train → verify row counts → only then run for real.
258
+ ```
259
+
260
+ #### 3.3.5 `goal`
261
+
262
+ What the work is currently *for* — the standing objective that orients
263
+ sessions. Goals are few (the reference implementation caps the live set at
264
+ 3) and change rarely.
265
+
266
+ Extra fields: `set_on` (SHOULD, ISO 8601 — merge recency key), `done_at`
267
+ (MAY — temporal tombstone, same semantics as next_action, 4.3.2).
268
+
269
+ ```markdown
270
+ ---
271
+ type: goal
272
+ name: Ship v1 of the importer
273
+ description: End-to-end import from the three legacy formats, behind a flag
274
+ set_on: 2026-06-01
275
+ ---
276
+ Done means: all three legacy formats round-trip in CI and the flag defaults
277
+ on for new installs.
278
+ ```
279
+
280
+ #### 3.3.6 `next_action`
281
+
282
+ A concrete, resumable piece of work. Next actions are the handoff unit: the
283
+ thing a different machine or session picks up.
284
+
285
+ Extra fields:
286
+
287
+ | Field | Requirement | Meaning |
288
+ |---|---|---|
289
+ | `added` | REQUIRED | ISO 8601 date-time the action was added. The temporal-tombstone comparison (4.3.2) is against this field; without it, completion semantics are undefined. |
290
+ | `done_at` | MAY | ISO 8601 date-time it was completed. Presence marks the entry a temporal tombstone. |
291
+ | `machine_id` | MAY | Machine that added it. |
292
+
293
+ ```markdown
294
+ ---
295
+ type: next_action
296
+ name: Add retry with backoff to the sync client
297
+ description: Sync fails hard on transient 5xx; wrap in 3-try exponential backoff
298
+ added: 2026-06-12T09:30:00Z
299
+ ---
300
+ Touch only `src/sync/client.js`. The failing case is a 503 mid-upload;
301
+ there is a repro in `test-fixtures/sync-503.json`.
302
+ ```
303
+
304
+ ### 3.4 Body conventions
305
+
306
+ - `[[wikilinks]]` reference other entries by link name (filename without
307
+ `.md`). Readers SHOULD resolve them within the store; unresolvable links
308
+ are not an error.
309
+ - Bold-label lines (`**Why:**`, `**How to apply:**`) are established body
310
+ structure for lessons and MAY be used generally. They are convention, not
311
+ syntax: readers MUST NOT require them (except as the legacy fallback in
312
+ Appendix A).
313
+ - Entries SHOULD NOT contain secrets. That rule's enforcement (scanning,
314
+ redaction) is an implementation concern, not a format concern.
315
+
316
+ ## 4. Session state file
317
+
318
+ ### 4.1 Shape
319
+
320
+ The session state file (`session.json`) is a single JSON document: the live
321
+ working set plus a bounded history. It is the "hot" half of the store —
322
+ small, rewritten atomically as a whole, merged structurally (never
323
+ line-by-line). This section describes exactly what the reference
324
+ implementation reads and writes; the machine-readable version is
325
+ [`schema/session.schema.json`](../schema/session.schema.json).
326
+
327
+ ```json
328
+ {
329
+ "version": 1,
330
+ "created_at": "2026-04-18T08:22:36.703Z",
331
+ "updated_at": "2026-08-06T21:10:03.001Z",
332
+ "machines": {
333
+ "<machine-uuid>": { "label": "work-laptop", "last_seen": "2026-08-06T21:10:03.001Z" }
334
+ },
335
+ "current": {
336
+ "goals": [ { "text": "...", "machine_id": "...", "set_on": "..." } ],
337
+ "next_actions": [ { "text": "...", "machine_id": "...", "added": "..." } ],
338
+ "parked_actions": [ { "text": "...", "machine_id": "...", "added": "...", "parked_at": "..." } ],
339
+ "completed_actions": [ { "text": "...", "done_at": "..." } ],
340
+ "completed_goals": [ { "text": "...", "done_at": "..." } ],
341
+ "open_questions": [ { "text": "...", "machine_id": "...", "asked": "..." } ],
342
+ "decisions": [ { "text": "...", "why": "...", "rejected": "...",
343
+ "hidden": true, "hidden_at": "...",
344
+ "machine_id": "...", "date": "..." } ]
345
+ },
346
+ "history": [
347
+ { "date": "...", "machine_id": "...", "summary": "...",
348
+ "files_touched": ["..."], "duration_min": 42, "session_id": "..." }
349
+ ]
350
+ }
351
+ ```
352
+
353
+ Field notes:
354
+
355
+ - `version` — integer schema version of this file (section 6). Note the
356
+ asymmetry with entry files' `schema_version`; both names are kept because
357
+ both are deployed.
358
+ - `machines` — map of stable machine UUID → `{ label, last_seen }`. Machine
359
+ identity is a UUID persisted per machine, paired with a human label
360
+ (hostname) for display. Labels may change; UUIDs MUST NOT.
361
+ - `current.*` — every item carries `text` (the identity key, 4.2) and the
362
+ list's date field (`set_on` / `added` / `asked` / `date`). `machine_id`
363
+ SHOULD be stamped by writers.
364
+ - `completed_actions` — temporal tombstones for `next_actions` (5.3.2):
365
+ `{ text, done_at }`. This list MAY be absent in files written before any
366
+ completion occurred; readers MUST treat absence as empty.
367
+ - `parked_actions` (v0.1.2) — the overflow of `next_actions`. When the live
368
+ list is at its cap, the oldest actions (by `added`) move here rather than
369
+ being discarded. A parked action is still *open*: it MUST be rendered (it
370
+ may be rendered more compactly), it MUST be completable (completing it
371
+ records the same `completed_actions` tombstone), and adding an action
372
+ whose identity is parked MUST return it to `next_actions` rather than
373
+ create a duplicate. `parked_at` records when it overflowed; `added` is
374
+ unchanged and remains the merge and tombstone comparison key. This list
375
+ has its own, larger cap; only when *it* overflows may an action be
376
+ dropped, and an implementation SHOULD log that it did. MAY be absent.
377
+ - `completed_goals` (v0.1.2) — temporal tombstones for `goals`, exactly as
378
+ `completed_actions` are for `next_actions`, compared against `set_on`
379
+ (5.3.2). Without them a retired goal returns on the next merge with any
380
+ replica still carrying it. MAY be absent.
381
+ - `open_questions` — captured questions awaiting an answer. (Draft note:
382
+ there is deliberately no `question` *entry type* in v0.1 — questions have
383
+ so far only proven useful in the live working set. If a portable form
384
+ earns its keep, it becomes a seventh type in a later version.)
385
+ - `history` — bounded roll-up of past sessions, newest first, deduplicated
386
+ by `session_id` when present (v0.1.2; the same session updates its own
387
+ row in place instead of adding one per write), otherwise by
388
+ `(date, machine_id, summary)`.
389
+ - **Bounded lists.** The live lists are capped (reference implementation:
390
+ goals 3, next_actions 8, parked_actions 20, open_questions 5, recent
391
+ decisions 10, completed tombstones 50, history 30). Caps are
392
+ implementation-chosen QUALITY parameters, not format constants — but
393
+ *some* cap is normative: the working set must stay loadable-into-context
394
+ small, with rotation oldest-by-date-out. Rotation out of `next_actions`
395
+ MUST go to `parked_actions`, not to nowhere (v0.1.2): the reference
396
+ implementation silently discarded the oldest action for five months, and
397
+ the loss was only noticed when three live items vanished in one week.
398
+ A cap on the *visible* list is a rendering budget; it is not permission
399
+ to forget. In particular the completed-tombstone retention MUST be large
400
+ enough to outlive any stale replica that might still carry the completed
401
+ item, or completions resurrect (5.3.2).
402
+
403
+ Writers MUST write the file atomically (write temp file, rename) and MUST
404
+ serialize concurrent read-mutate-write cycles (e.g. a lock file). Locking
405
+ only the write is insufficient: two processes can both read the same stale
406
+ snapshot before either writes. The entire read → mutate → write cycle is the
407
+ critical section.
408
+
409
+ Readers encountering unparseable JSON MUST NOT discard the file silently:
410
+ quarantine the original (e.g. copy to a timestamped backup), then degrade to
411
+ a safe empty state.
412
+
413
+ ## 5. Merge semantics (normative)
414
+
415
+ This section is the hard-won part of the format. Every rule here exists
416
+ because its absence produced a real data-loss or data-resurrection bug in
417
+ production. Implementations claiming Full conformance (section 8) MUST
418
+ implement this section exactly.
419
+
420
+ ### 5.1 Identity
421
+
422
+ The identity of a session-list item is its **normalized text**: `text`,
423
+ whitespace-trimmed, case-folded. Two items with the same normalized text are
424
+ the same item. A purged decision tombstone (5.3.1) carries that identity as
425
+ `text_hash` (SHA-256 of the normalized text) instead of the text. The
426
+ identity of an entry file is its link name (filename).
427
+
428
+ ### 5.2 Union, newest wins
429
+
430
+ Merging two replicas (local and remote) of a session list:
431
+
432
+ 1. **Union by identity.** The merged list contains every distinct identity
433
+ from both sides. A merge MUST NOT drop an item merely because the other
434
+ side lacks it — "never clobber." The only way items leave is cap
435
+ rotation (oldest by date field, off the end) and tombstoning. For
436
+ `next_actions` (v0.1.2) the union pools the live *and* parked lists of
437
+ both sides, applies the tombstones, then re-splits: the newest N by
438
+ `added` are live, the rest are parked (keeping any existing `parked_at`).
439
+ A capped union of the live list alone evicts on merge exactly as
440
+ silently as a capped append does.
441
+ 2. **Newest wins per identity.** When both sides carry the same identity,
442
+ the copy with the newer value in the list's date field (`set_on`,
443
+ `added`, `asked`, `date`) wins wholesale. A missing or unparseable date
444
+ compares as the epoch (loses to any real date).
445
+ 3. `machines` merges per-UUID, keeping the entry with the newer `last_seen`.
446
+ 4. `history` unions, deduplicates by `(date, machine_id, summary)`, sorts
447
+ newest-first, and caps.
448
+ 5. `created_at` takes the earlier of the two; `updated_at` the later.
449
+
450
+ Entry files merge by the same principle at file granularity: union of files;
451
+ same filename on both sides resolves by newest (`updated`, falling back to
452
+ `created`), or by the user's VCS if the store is under one. v0.1 does not
453
+ define intra-file three-way merge for entry bodies; that is what git is for.
454
+
455
+ ### 5.3 Tombstones are sticky
456
+
457
+ Deletion under union-merge is the hard problem: plain removal does not
458
+ survive, because any replica still holding the removed item re-unions it
459
+ straight back on the next merge. A conforming implementation MUST therefore
460
+ represent removals as tombstones, and tombstones MUST be *sticky* — they
461
+ propagate through merges. There are two classes, and the difference between
462
+ them is deliberate.
463
+
464
+ #### 5.3.1 Absolute tombstones (`hidden`) — decisions
465
+
466
+ Setting `hidden: true` (with `hidden_at`) on a decision suppresses it
467
+ permanently: hidden decisions are excluded from rendering and lookup but
468
+ retained in the data so the tombstone keeps propagating.
469
+
470
+ **Stickiness rule:** hidden is monotonic. If *any* copy of an identity on
471
+ *either* side of a merge carries `hidden: true`, the merged copy carries
472
+ `hidden: true` (and that copy's `hidden_at`) — **regardless of the date
473
+ comparison in 5.2**. The winner-by-date inherits the tombstone from the
474
+ loser if it lacks one.
475
+
476
+ **Purged form.** A hidden decision MAY additionally have its `text` replaced
477
+ by the literal `[purged]` (and `why` / `rejected` removed) and carry
478
+ `text_hash`: the lowercase hex SHA-256 of the identity it replaces (the
479
+ normalized text, 5.1). This exists for the case where the text itself must
480
+ leave the file — a pasted credential, a client name — not merely leave the
481
+ render. Readers MUST treat a purged tombstone as matching every decision
482
+ whose identity hashes to `text_hash`, and on merge the purged form MUST win
483
+ over any un-purged copy of the same identity regardless of `date`, so the
484
+ redacted text never returns via a stale replica. Implementations MUST NOT
485
+ purge without hiding; the hash is not an identity for a live decision.
486
+
487
+ The date-independence is not an optimization; it is the correctness
488
+ condition. Tombstoning legitimately does not touch the item's `date` (the
489
+ tombstoned copy may well *lose* the newest-wins comparison), and a replica
490
+ that has not yet pulled the tombstone holds an older un-hidden copy that
491
+ would otherwise win or re-union. If hiding were just another field on
492
+ whichever copy has the newer date, every un-synced replica would resurrect
493
+ the item on its next merge, and the same junk would need re-hiding on every
494
+ machine forever. Suppression must be monotonic or it is not suppression.
495
+
496
+ #### 5.3.2 Temporal tombstones (`done_at`) — next actions
497
+
498
+ Completing a next action removes it from `next_actions` AND records
499
+ `{ text, done_at }` in `completed_actions`. Removal alone is insufficient
500
+ for exactly the resurrection reason above — any stale replica (another
501
+ machine, a backup, a long-lived process's in-memory copy) re-unions the
502
+ finished item on merge.
503
+
504
+ **Merge rule.** After the 5.2 union:
505
+
506
+ 1. Union `completed_actions` from both sides by identity, keeping the
507
+ newest `done_at` per identity, capped (newest-first).
508
+ 2. Filter the merged `next_actions`: an action is suppressed if a tombstone
509
+ with its identity exists **and** the action's `added` is not later than
510
+ the tombstone's `done_at`.
511
+
512
+ Unlike `hidden`, this tombstone is *temporal on purpose*: a copy whose
513
+ `added` postdates `done_at` is not a stale straggler — it is a deliberate
514
+ re-add of the same text after completion, and it MUST survive. "Fix the
515
+ flaky test" can legitimately be finished and later added again. Decisions
516
+ get absolute tombstones because a suppressed decision text is junk forever;
517
+ actions get temporal ones because finished work can recur. Implementations
518
+ MUST NOT substitute one class for the other.
519
+
520
+ The same `done_at`-vs-`added`/`set_on` comparison applies to `goal` and
521
+ `next_action` *entry files* that carry `done_at`, and (v0.1.2) to the
522
+ session's `goals` list against `completed_goals`, compared on `set_on`.
523
+
524
+ ### 5.4 Merge invariants (summary)
525
+
526
+ For any replicas A and B:
527
+
528
+ - **No silent loss:** every identity in A ∪ B is in merge(A, B), as a live
529
+ item or a tombstone-suppressed one (up to cap rotation).
530
+ - **Convergence:** merge is commutative and idempotent up to ordering —
531
+ merge(A, B) and merge(B, A) render identically, and merging a replica
532
+ with itself is a no-op.
533
+ - **Monotone suppression:** once hidden everywhere-merged, hidden in every
534
+ future merge; a completed action stays completed against every copy whose
535
+ `added` predates its `done_at`.
536
+ - **Unknown fields pass through (v0.1.2):** a merge MUST carry every key
537
+ under `current` (and every top-level key) that it does not recognise,
538
+ taking the local copy when both sides have one. A merger rebuilt from the
539
+ keys it knows erases whatever a newer writer added — the reference
540
+ implementation lost two goal retirements and a parked action minutes
541
+ after those fields existed, to a push still running the previous version.
542
+ Forward compatibility of the *file* (section 6) is not enough; the merge
543
+ has to be forward compatible too.
544
+
545
+ ## 6. Versioning
546
+
547
+ - Entry frontmatter MAY carry `schema_version` (integer; absent = 1). The
548
+ session file MUST carry `version` (integer). Both are governed by the
549
+ same rules below.
550
+ - This spec is v0.1 and describes data schema version **1**.
551
+ - **Backward:** implementations MUST migrate older versions forward through
552
+ a per-version ladder (a pure step from N to N+1 for each N), filling
553
+ defaults for missing fields. Migration MUST NOT throw on malformed input;
554
+ the worst case is a fresh empty state, with the original quarantined.
555
+ - **Forward (normative — quarantine and degrade, never guess):** an
556
+ implementation encountering a version **newer** than it understands MUST
557
+ NOT attempt to interpret the data. It MUST preserve the original
558
+ (quarantine — e.g. a timestamped backup copy), MUST degrade to safe
559
+ behavior (an empty-but-valid state, or read-only), and SHOULD tell the
560
+ user once to upgrade. A newer schema's fields may have semantics —
561
+ tombstones especially — that an old reader would destroy by "mostly
562
+ understanding" them. Merging is the dangerous path: a degraded read MUST
563
+ contribute nothing to a merge rather than contribute a misreading.
564
+ - Unknown *fields* at a known version are the extension mechanism and are
565
+ NOT a version violation: readers ignore, writers preserve (3.2).
566
+
567
+ ## 7. What this spec does not cover
568
+
569
+ Deliberately out of scope — these are implementations' business, and
570
+ competing on them is the point of an open format:
571
+
572
+ - **Sync transport.** Git, object storage, a vendor cloud, a USB stick.
573
+ The format defines what a correct merge produces, not how bytes move.
574
+ - **Encryption at rest and in transit.** Stores are plain text; protecting
575
+ them (client-side encryption before upload, disk encryption) is an
576
+ implementation/deployment concern.
577
+ - **Cloud services.** Accounts, auth, storage tiers, sharing links.
578
+ - **Capture quality.** What is *worth* remembering, secret scanning,
579
+ consolidation/deduplication passes, relevance ranking, embeddings.
580
+ - **Rendering.** How entries and the working set are surfaced into an
581
+ assistant's context (pinned blocks, system prompts, retrieval) is a
582
+ competitive surface, not a format rule.
583
+
584
+ ## 8. Conformance levels
585
+
586
+ Three levels, cumulative.
587
+
588
+ - **memoir Reader.** Can parse and render a store: frontmatter + body of
589
+ every entry type, and the session file. MUST ignore unknown fields, MUST
590
+ tolerate unknown entry types (render as opaque), MUST honor tombstone
591
+ visibility (never display `hidden` decisions or completed actions as
592
+ live), and MUST apply the forward-version rule (6) on read.
593
+ - **memoir Writer.** Reader, plus can add and edit entries without
594
+ corrupting the store: atomic session writes with a locked
595
+ read-mutate-write cycle, preserves unknown fields on rewrite, stamps
596
+ identity and date fields (`added`, `set_on`, `date`, `machine_id`), and
597
+ never rewrites entries it was not asked to touch.
598
+ - **memoir Full.** Writer, plus implements section 5 exactly: union-by-text
599
+ with newest-wins, both tombstone classes with their stickiness rules, and
600
+ the merge invariants of 5.4.
601
+
602
+ A conformance claim names its level: "memoir Reader (v0.1)".
603
+
604
+ ## 9. Security and privacy considerations
605
+
606
+ A memoir store aggregates exactly the information an attacker would want:
607
+ project internals, decision history, environment facts. Implementations
608
+ SHOULD scan for and warn about secrets before any entry leaves the machine,
609
+ and MUST NOT transmit store contents anywhere without explicit user
610
+ configuration. The format's plain-text nature is a privacy *feature* — the
611
+ user can always audit precisely what their AI knows — and implementations
612
+ should preserve that auditability rather than obscure it.
613
+
614
+ ---
615
+
616
+ ## Appendix A: Legacy dialects (informative)
617
+
618
+ The format formalizes a convention that grew in the wild, and existing
619
+ stores predate it. A survey of one long-lived production store (326 entry
620
+ files, April–August 2026) found:
621
+
622
+ - **Two frontmatter dialects.** About half the files carry `type` as a
623
+ top-level key; the other half nest it as `metadata.type` (often with
624
+ `metadata.node_type: memory`). `originSessionId` likewise appears both
625
+ top-level and nested.
626
+ - **Bare files.** ~6% have no frontmatter at all — plain markdown with an
627
+ `# H1` title.
628
+ - **Legacy type vocabulary.** Deployed values are `user`, `feedback`,
629
+ `project`, `reference` — not the six types of this spec.
630
+ - **Body-borne lesson fields.** Legacy `feedback` entries carry the trigger
631
+ in `description` and the application rule as a `**How to apply:**` body
632
+ section, not as frontmatter keys.
633
+
634
+ Conforming Readers SHOULD apply these compatibility rules when reading
635
+ pre-v0.1 stores, and validators SHOULD downgrade the corresponding errors to
636
+ warnings for legacy-dialect files:
637
+
638
+ | Legacy | Read as |
639
+ |---|---|
640
+ | `metadata.type: X` | `type: X` |
641
+ | top-level `originSessionId` / `metadata.originSessionId` | `origin.session_id` |
642
+ | `type: user` | `preference` |
643
+ | `type: feedback` | `lesson` (trigger ← `description`; how_to_apply ← `**How to apply:**` body section) |
644
+ | `type: reference` | `fact` |
645
+ | `type: project` | no atomic mapping — a project *dossier* aggregates many would-be entries in one file; read as an opaque legacy entry |
646
+ | no frontmatter | opaque legacy entry; `name` ← first `# H1` |
647
+
648
+ Writers MUST NOT emit legacy dialects for new entries. Rewriting legacy
649
+ files into canonical form is an implementation choice (a consolidation
650
+ pass), not an obligation — a Reader must handle both indefinitely.
651
+
652
+ ## Appendix B: Relationship to the reference implementation (informative)
653
+
654
+ memoir-cli (npm) implements: session state exactly as section 4 at
655
+ `~/.config/memoir/session.json` (schema version 1); the merge semantics of
656
+ section 5 in its cross-machine sync (`mergeSessions`); the forward-version
657
+ quarantine of section 6 (`.pre-migration-<ts>` backups, degrade to empty);
658
+ and `memoir validate`, a structural checker for both file kinds against this
659
+ spec. Entry-file writing currently passes agent-authored markdown through
660
+ verbatim — validation, not generation, is where the convention is enforced.
661
+
662
+ ## Appendix C: Changes
663
+
664
+ - **v0.1.2 (2026-09-04)** — additive. `parked_actions`: overflow from the
665
+ live next-action list is parked, rendered and completable, never dropped
666
+ (4.1); merge pools live + parked and re-splits (5.2). `completed_goals`:
667
+ temporal tombstones for goals (5.3.2). `history[].session_id` for
668
+ in-place session rows (4.1). New invariant: unknown fields pass through a
669
+ merge (5.4). Shipped in memoir-cli 3.13.x. Motivation, all first-party:
670
+ three live next-actions silently evicted in one week by a `slice(-8)`;
671
+ a goal set in June still pinned in September because nothing could
672
+ retire it; and the new fields themselves erased by an older build's
673
+ merge on the day they shipped.
674
+ - **v0.1.1 (2026-08-18)** — additive. `aliases` common frontmatter field
675
+ (3.2). Purged form of the absolute decision tombstone: `[purged]` +
676
+ `text_hash`, hash-matched and merge-winning (5.1, 5.3.1). Both shipped in
677
+ memoir-cli 3.12.0 (`memoir_remember aliases:`; `memoir forget [--purge]`).
678
+ Motivation for the first: a real retrieval failure where a shipped surface
679
+ was findable only by the exact words its one memory happened to use. For
680
+ the second: nothing shipped could create an absolute tombstone at all
681
+ (thanks to the agent-memory-atlas review for stating it that plainly).
682
+ - **v0.1 (2026-08-06)** — first public draft. Extracted from memoir-cli
683
+ 3.10.x behavior. Seeking implementations and critique:
684
+ https://github.com/camgitt/memoir/issues