mx 0.1.230

A Swiss army knife for Claude Code and multi-agent toolkits
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
#import "lib.typ": *

#page-header(
  "Memory",
  "Knowledge graph with SurrealDB-backed persistent memory."
)

The memory subsystem is the largest command surface in mx. It provides a
persistent knowledge graph backed by SurrealDB (embedded SurrealKV or networked
WebSocket), with categories, tags, resonance levels, embeddings for semantic
search, relationships between entries, and a wake ritual for identity bootstrap.

Every entry in the graph has a unique ID (prefixed `kn-`), a category, a title,
body content, optional tags, a resonance level (1--10+), and timestamps. Entries
can be linked via typed relationships, anchored to each other by embedding
similarity, and surfaced through keyword or semantic search.

#note[The database schema is applied automatically on every connection, in both
embedded and network mode. All schema statements are idempotent
(`IF NOT EXISTS` / `UPSERT`), so no manual setup is required. Set `MX_SKIP_SCHEMA=1` to skip
auto-apply in environments with restricted DB permissions. Run `mx migrate` to
explicitly apply the schema (it ignores `MX_SKIP_SCHEMA`).]

== Table of contents

- #link(<adding>)[Adding entries]
- #link(<dedup>)[Write-boundary deduplication]
- #link(<reading>)[Reading entries]
- #link(<updating>)[Updating entries]
- #link(<deleting>)[Deleting entries]
- #link(<wake>)[Wake system]
- #link(<embeddings>)[Embeddings and anchoring]
- #link(<relationships>)[Relationships]
- #link(<seeding>)[Seeding]
- #link(<health>)[Health and statistics]
- #link(<export>)[Export]
- #link(<reinforcement>)[Reinforcement]
- #link(<metadata>)[Metadata management]
- #link(<sessions>)[Session tracking]

// ═══════════════════════════════════════════════════════════════════════
// ADDING ENTRIES
// ═══════════════════════════════════════════════════════════════════════

== Adding entries <adding>

#command(
  "mx memory add",
  [Create a new entry in the knowledge graph. At minimum, provide a category
  and title (or a `--type` for ephemeral facts, which auto-routes the category
  and generates a title from content).],
  flags: (
    ([`--category`],   [`string`], [Category name (run `mx memory categories list` for valid names). Required unless `--type` is provided.]),
    ([`-t, --title`],  [`string`], [Entry title. Required unless `--type` is provided.]),
    ([`--content`],    [`string`], [Inline content. Conflicts with `--file`.]),
    ([`-f, --file`],   [`path`],   [Read content from a file. Also accepts `--content-file`.]),
    ([`--tags`],       [`string`], [Comma-separated tags.]),
    ([`-a, --applicability`], [`string`], [Comma-separated applicability contexts.]),
    ([`-p, --project`], [`string`], [Source project ID.]),
    ([`--source-agent`], [`string`], [Source agent ID. Defaults to `MX_CURRENT_AGENT` env var.]),
    ([`--source-type`], [`string`], [Source type: `manual`, `ram`, `cache`, `agent_session`. Default: `manual`.]),
    ([`--entry-type`], [`string`], [Entry type: `primary`, `summary`, `synthesis`. Default: `primary`.]),
    ([`--session-id`], [`string`], [Session ID to associate with this entry.]),
    ([`--ephemeral`],  [`flag`],   [Mark entry as ephemeral.]),
    ([`-d, --domain`], [`string`], [Domain/subdomain path.]),
    ([`--content-type`], [`string`], [Content type: `text`, `code`, `config`, `data`, `binary`. Default: `text`.]),
    ([`--private`],    [`flag`],   [Mark as private (only visible to owner). Shorthand for `--visibility private`.]),
    ([`--visibility`], [`string`], [Set visibility: `public` or `private`.]),
    ([`--owner`],      [`string`], [Explicit owner. Defaults to `source_agent` or `MX_CURRENT_AGENT` if private.]),
    ([`--resonance`],  [`int`],    [Resonance level (1--10, or higher for transcendent).]),
    ([`--resonance-type`], [`string`], [Resonance type: `foundational`, `transformative`, `relational`, `operational`, `ephemeral`, `session`.]),
    ([`--wake-phrase`], [`string`], [Wake phrase for memory ritual verification.]),
    ([`--wake-phrases`], [`string`], [Multiple wake phrases (comma-separated).]),
    ([`--wake-order`], [`int`],    [Custom wake order (lower = earlier in sequence).]),
    ([`--anchors`],    [`string`], [Comma-separated bloom IDs this entry connects to.]),
    ([`--type`],       [`string`], [Fact type for ephemeral knowledge: `decision`, `insight`, `person`, `quote`, `thread_opened`, `commitment`, `thread_closed`. Auto-routes category and sets `resonance_type=ephemeral`.]),
    ([`--session`],    [`string`], [Session to link fact to via EXTRACTED_FROM relationship. Requires `--type`.]),
    ([`--thread-id`],  [`string`], [Thread ID for `thread_closed` operations. Requires `--type`.]),
    ([`--no-auto-anchor`], [`flag`], [Skip automatic anchor generation.]),
    ([`--no-embed`],   [`flag`],   [Skip synchronous embedding generation on write.]),
    ([`--allow-duplicate`], [`flag`], [Write through even if an identical (same session, same owner) entry already exists this session. Without this flag, a recased/repunctuated near-duplicate is skipped (exit 0) rather than written -- see #link(<dedup>)[Write-boundary deduplication] below.]),
    ([`--json`],       [`flag`],   [Output as JSON.]),
  ),
  examples: (
    "mx memory add --category recipe --title \"Retry with backoff\" \\\n  --content \"Use exponential backoff with jitter...\" \\\n  --tags \"reliability,networking\" --source-agent whistledown",
    "mx memory add --category discovery --title \"SurrealDB needs explicit NS\" \\\n  --content \"Always set namespace before queries\" \\\n  --resonance 7 --resonance-type operational",
    "# Ephemeral fact (auto-routes category, generates title)\nmx memory add --type decision \\\n  --content \"Chose Typst over mdBook for docs\" \\\n  --session abc-123",
    "# Content from file\nmx memory add --category ingredient -t \"API reference\" -f api-notes.md",
  ),
)

#tip[When `--type` is provided, `--category` and `--title` become optional. The
fact type routes to an appropriate category and generates a title from the
content automatically.]

#command(
  "mx memory add-batch",
  [Add multiple entries in a single invocation from a JSONL file or stdin.
  Each line is a JSON object whose fields match `mx memory add` arguments
  (`category`, `title`, `content`, `tags`, `source_agent`, `type`, etc.).
  The store is opened once and the ~435 MB embedding model is loaded once for
  the whole batch, so a 30-entry bulk caller pays one cold-load instead of
  thirty. Malformed lines are skipped and reported at the end; one bad entry
  does not abort the rest (partial-success semantics). A duplicate-skip
  (#link(<dedup>)[write-boundary dedup]) is NOT a malformed-line failure --
  it counts toward "already saved," not "failed." Exits non-zero only when a
  line was malformed or the write itself errored; exits 0 when every line
  either wrote or was a legitimate duplicate-skip.

  *Recommended delivery:* pass a JSONL file via `--file` when calling through
  a `sudo` wrapper (e.g. the hearth `_secret-exec` proxy) — a file path
  survives wrapper layers where a piped stdin may not.],
  flags: (
    ([`-f, --file`], [`path`],  [Path to a JSONL file (one JSON object per line). Reads from stdin when omitted.]),
    ([`--no-embed`], [`flag`],  [Skip embedding for the whole batch. Defers to the next `mx memory embed --all` run (e.g. a nightly cron). Keyword and tag search are unaffected; only `--semantic` search misses un-embedded entries until then.]),
  ),
  examples: (
    "# Stdin pipe\nprintf '{\"category\":\"insight\",\"title\":\"T1\",\"content\":\"C1\",\"source_agent\":\"soren\"}\\n{\"type\":\"decision\",\"content\":\"chose Rust\",\"source_agent\":\"soren\"}\\n' \\\n  | mx memory add-batch",
    "# File (safer through sudo wrappers)\nmx memory add-batch --file /tmp/pocket-entries.jsonl",
    "# Skip embedding — defer to nightly embed --all\nmx memory add-batch --file entries.jsonl --no-embed",
  ),
)

#note[Each JSONL line for `add-batch` is a self-describing payload: include all
per-entry fields (`category`, `title`, `content`, `source_agent`, `tags`,
`private`, `resonance`, `type`, etc.) on each line. There are no batch-wide
field overrides except `--no-embed`. This design supports heterogeneous batches
(facts, person nodes, summaries, blooms) in a single invocation --- which is
the primary batch-write use-case. Per line, `allow_duplicate: true` mirrors
`--allow-duplicate` on the single-add path -- see below.]

// ═══════════════════════════════════════════════════════════════════════
// WRITE-BOUNDARY DEDUPLICATION
// ═══════════════════════════════════════════════════════════════════════

=== Write-boundary deduplication <dedup>

All four new-entry write funnels -- `mx memory add` (standard), `mx memory
add --type` (fact-routing), and both lines of `mx memory add-batch`
(standard and `--type`) -- check a normalized `dedup_hash` of `title+body`
(lowercase, ASCII punctuation and common Unicode punctuation stripped --
smart quotes, en/em dash, horizontal ellipsis -- whitespace collapsed)
against the writing agent's OWN entries in the SAME session AND SAME
CATEGORY before writing. Category is enforced by scoping the candidate
query, not by folding it into `dedup_hash` itself -- identical title+body
re-filed under a different category is a distinct fact (a different shelf),
never a dedup candidate, so it always lands as its own entry.
A regenerated duplicate that differs only by case, punctuation, or
whitespace -- the common shape when an LLM re-derives the same fact across
turns -- is detected and the write is SKIPPED rather than landing as a
second row. Title and body are hashed as a length-prefixed pair, so a
duplicate that shifts the split point (e.g. a sentence folded into the
title one turn, split title+body the next) is still caught.

A skip is treated as *success, not failure*: it means the content is already
durably saved, and there is nothing further to do.

- *Plain mode:* `Already saved as kn-XXXX (identical entry this session — nothing to do).`
  Deliberately distinct from the success line (`Added entry: kn-XXXX`) --
  a caller regexing on `Added entry: (kn-\S+)` should switch to `--json`
  rather than assume this line matches that pattern.
- `--json` *mode:* `{"id": "kn-XXXX", "skipped": true, "duplicate_of": "kn-XXXX", "status": "already_persisted", "note": "..."}`,
  with clean JSON on stdout (no other text) and exit code `0`. `id` mirrors
  every success payload's `id` key so a caller reading `.id` unconditionally
  never gets a silent `null` on a skip.
- *`add-batch` per-entry line:* `[N] Already saved: kn-XXXX (title) — identical this session, no action` --
  never the bare words "skipped" or "duplicate" -- and the batch summary
  carries its own `K already saved (no action needed)` category, separate
  from `added` and `failed`. `add-batch` has no `--json` mode, so this is
  stdout text only.
- Pass `--allow-duplicate` (single add) or `allow_duplicate: true` (per JSONL
  line) to force the write through when a re-add is deliberate. *Caveat:* on
  the fact-routing paths (and on the standard path when `title` is
  byte-identical to an existing entry's), the write's id is derived from
  `path_hint:title` (`generate_id`), so a byte-identical forced re-add
  overwrites the SAME row in place (new `updated_at`, same id) instead of
  creating a second entry -- `--allow-duplicate` only produces a genuinely
  distinct row for a recased/reworded near-duplicate, not an exact repeat.
  This is accepted, not fixed: perturbing the id on a forced write was
  considered and rejected (it would make `--allow-duplicate` non-idempotent
  in a different, more surprising way). If you need a second row for
  byte-identical content, vary the title.
- On a candidate-lookup failure (a transient DB read blip), the gate FAILS
  OPEN: it warns to stderr (`--json` mode also gets a
  `"dedup_lookup_warning"` field) and proceeds with the write rather than
  aborting it. A dedup mechanism that already tolerates a check-then-write
  race between two concurrent writers tolerates a lookup blip the same way
  -- the write always wins.

#note[*Coverage boundary, read this before assuming full coverage:* the gate
catches recased/repunctuated re-adds within the SAME session, SAME owner,
and SAME category. It does NOT catch: cross-session regenerations (a fact
re-derived in a later wake under a different `session_id` -- *open question,
not yet confirmed:* the ~20 double-write pairs (W342–W445) that motivated
this feature have not been checked against this scope; if most of them are
cross-session rather than same-session, this fix closes only a minority of
the motivating class, and owner-scoped matching with a bounded time window
would be the sharper mechanism -- flagged for follow-up, not resolved
here), reworded duplicates (different words, same meaning -- e.g. "shipped"
vs "published"), a public+owned entry vs. a later public+unowned entry with
identical content (owner is a hard AND-conjoined predicate, so these two
visible-to-everyone rows never dedup against each other even though both
are public), identical title+body filed under a DIFFERENT category
(category is a hard AND-conjoined predicate too -- a re-file under a new
category is treated as a distinct fact, deliberately, never a duplicate),
identical title+body with DIFFERENT tags (tags are edge-modeled, not a
scalar field on the row, and are deliberately excluded from the dedup
identity entirely -- a tag-only-differing re-add still dedupes, same as any
other identical-content re-add), or writes with no `session_id` at all
(dedup is bypassed entirely when `session_id` is absent -- a bypass signal
is emitted on EVERY funnel: `"dedup": "bypassed_no_session"` in `--json`
mode on the standard single-add path (mutually exclusive with the stderr
note -- json mode never prints both), a stderr note on all four funnels in
every other mode, so the bypass is never silent). Keep marking back
captured entries; this is a store-boundary backstop, not a substitute for
careful write discipline.]

#note[*In-process, best-effort -- not a database-level uniqueness guarantee.*
The dedup index is read-then-write within a single `mx` process invocation
(TOCTOU: two concurrent `mx` invocations can still both write the same
normalized content). It is NOT wired to the legacy `content_hash` field
(title-only, import-time change detection, unenforced) and there is no
`DEFINE INDEX ... UNIQUE` backing it at the schema level -- a persisted
`dedup_hash` plus a compound unique index would close the TOCTOU gap and
make the "store-boundary" framing literally true, at the cost of mapping a
constraint violation to a skip; this is a design option for a future PR, not
implemented here -- and, per finding 1, that unique index must key on
`(session, owner, category, dedup_hash)`, not just `(session, owner,
dedup_hash)`, to match the same category scope this fix adds to the
in-process gate. The candidate query has no result limit, so a very
long-lived session pays an O(n) rehash of every row ever written to its
`(session, owner, category)` group on every future write in that group --
bounded in practice (memory sessions aren't usually thousands of rows), a
follow-up if it ever isn't.]

#note[*The write boundary confirms content existence under a claimed
owner.* A `duplicate_of` hit tells the caller "an entry with this exact
normalized content already exists under this owner" -- bounded to the
pre-existing owner-claim authz (a caller can already assert their own owner
scope; this exposes no more than that), but it is a new confirmation surface
worth naming, and it is sharper on `add-batch`: each JSONL line supplies its
own `source_agent`/`owner`, so one batch invocation can probe many
owner+content combinations at once. When more than one pre-existing entry
under a group already shares a hash (only possible from data written before
this gate existed), `duplicate_of` reports whichever one iteration order
happens to surface last -- not the earliest or a canonical row.]


// ═══════════════════════════════════════════════════════════════════════
// READING ENTRIES
// ═══════════════════════════════════════════════════════════════════════

== Reading entries <reading>

=== Shared filter flags

Several read commands (`search`, `list`) share a common set of filter
flags. These are documented once here and referenced below.

#table(
  columns: (auto, auto, auto),
  table.header([*Flag*], [*Type*], [*Description*]),
  [`-c, --category`], [`string`], [Filter by category (comma-separated).],
  [`--json`],          [`flag`],   [Output as JSON.],
  [`--mine`],          [`flag`],   [Show only your private entries.],
  [`--include-private`], [`flag`], [Include private entries (requires matching owner).],
  [`--min-resonance`], [`int`],    [Minimum resonance level. Filtered on the time-decayed *effective* resonance (see note below on the basis divergence with `wake`).],
  [`--max-resonance`], [`int`],    [Maximum resonance level. Filtered on the time-decayed *effective* resonance.],
  [`--has-wake-phrase`], [`flag`],  [Filter to entries WITH a wake phrase.],
  [`--missing-wake-phrase`], [`flag`], [Filter to entries WITHOUT a wake phrase.],
  [`--has-anchors`],   [`flag`],   [Filter to entries WITH anchors.],
  [`--missing-anchors`], [`flag`], [Filter to entries WITHOUT anchors.],
  [`--has-resonance-type`], [`flag`], [Filter to entries WITH a resonance type.],
  [`--missing-resonance-type`], [`flag`], [Filter to entries WITHOUT a resonance type.],
  [`--limit`],         [`int`],    [Limit number of results.],
  [`--tags`],          [`string`], [Filter by tags (comma-separated, matches any).],
  [`--exclude-tags`],  [`string`], [Comma-separated tag prefixes to drop (e.g. `tier/` excludes every entry tagged `tier/<anything>`). Specify once; comma-separate multiple prefixes -- repeating the flag is a parse error. Same parsing and prefix-match semantics as `wake-fetch`'s `--exclude-tags`.],
)

#note[*Visibility default:* `list` and `search` run *public-only* by default.
Your own private entries are omitted unless you pass `--include-private` (or
`--mine`, which shows only your private entries). When a public-only query
matches private entries you own but hides them, `mx` prints a best-effort
*stderr* nudge --- see the note below on the hidden-private hint. (`wake`, by
contrast, includes owned-private blooms in its cascade.)]

#note[*Hidden-private hint:* When `MX_CURRENT_AGENT` is set and a public-only
`list`/`search` query matches private entries you own, `mx` writes a hint to
*stderr* pointing at `--include-private`, e.g.:

```
note: 3 private entries of yours matched but are hidden; use --include-private to see them
```

The hint is *stderr only* --- it never touches `stdout`, `--json` output, or the
exit code, and any error computing it is swallowed silently. It fires only in
the public-only case: passing `--include-private` or `--mine` silences it (they
already show your private entries), and it is *suppressed under `search
--semantic`* because the underlying count uses the keyword (BM25) predicate,
which does not agree with vector similarity.]

#note[*Resonance basis divergence (Issue \#404):* `list`/`search --min-resonance`
and `--max-resonance` filter on the time-decayed *effective* resonance, whereas
`wake --min-resonance` filters on the *raw* stored value. The same numeric
threshold can therefore admit an entry under `wake` but exclude it under
`list`/`search` once decay is applied. `foundational` and `transformative`
resonance types are decay-exempt, so they filter identically under both. This
divergence is intentional for now; an explicit `--resonance-basis raw|decayed`
flag is tracked in \#404.]

#note[`--exclude-tags` takes a comma-separated list of tag prefixes. Any entry
carrying at least one tag that prefix-matches at least one supplied value is
dropped -- useful for excluding whole tag namespaces. Empty segments from
trailing commas are ignored. Pass the flag once; repeating it (e.g.
`--exclude-tags a --exclude-tags b`) is a parse error, not a silent override.]

#command(
  "mx memory show",
  [Display a single entry by ID.],
  flags: (
    ([`--json`],         [`flag`], [Output as JSON.]),
    ([`--content-only`], [`flag`], [Output only the body content (useful for piping).]),
  ),
  examples: (
    "mx memory show kn-abc123",
    "mx memory show kn-abc123 --content-only | pbcopy",
  ),
)

#command(
  "mx memory list",
  [List entries, optionally filtered by category, tags, resonance, and other
  shared filter flags.],
  flags: (),
  examples: (
    "mx memory list -c recipe",
    "mx memory list -c discovery,decree --min-resonance 5",
    "mx memory list --missing-wake-phrase --limit 20",
    "# Exclude a whole tag namespace\nmx memory list --category person --exclude-tags 'tier/'",
  ),
)

#note[`list` accepts all shared filter flags documented above, including the
public-only visibility default and the hidden-private stderr hint.]

#command(
  "mx memory search",
  [Search entries by keyword or semantic similarity. Keyword search is the
  default; add `--semantic` to use vector embeddings.],
  flags: (
    ([`--semantic`], [`flag`], [Use semantic (vector) search instead of keyword search.]),
    ([`--activate`], [`flag`], [Activate all returned results: resets `last_activated` (decay clock) and increments `activation_count`. Marks results as intentionally consumed rather than just browsed.]),
  ),
  examples: (
    "mx memory search \"retry pattern\"",
    "mx memory search \"how to handle timeouts\" --semantic",
    "mx memory search \"agent bootstrap\" -c recipe,method --limit 5",
    "# Search and activate results (mark as consumed)\nmx memory search \"retry pattern\" --activate",
    "# Exclude a whole tag namespace from keyword search\nmx memory search \"onboarding\" --exclude-tags 'tier/'",
    "# Exclude a whole tag namespace from semantic search\nmx memory search \"onboarding\" --semantic --exclude-tags 'tier/'",
  ),
)

#note[`search` accepts all shared filter flags, including the public-only
visibility default and the hidden-private stderr hint (the hint is suppressed
under `--semantic`). Semantic search requires entries to have embeddings
generated via `mx memory embed`.]

#note[With `--semantic`, `--exclude-tags` exclusion happens in two phases:
unchunked entries are excluded inside the candidate-set query itself (SQL
`WHERE`), while chunked entries are filtered on the Rust side after
hydration, since `embedding_chunk` has no direct tag edge to filter on in
SQL. Either way, excluded entries are dropped before the final sort and
truncate, so `--limit` returns a full page as long as the search stays
within its scan budget (`limit * 50`, minimum 1000 chunk candidates). If
that budget is exhausted before enough non-excluded results are found --
which can happen when excluded tags dominate the ranked candidates -- the
result set may come back short, and a warning is logged when that
happens.]

#tip[By default, search does not activate results -- browsing is not the same as
engagement. Use `--activate` when you are intentionally consuming the results
(e.g., loading context for a task), not just exploring.]

#command(
  "mx memory recent",
  [List recent ephemeral facts with decay. By default shows only ephemeral
  entries from the last 10 days. Use `--all-types` to surface all resonance
  types.],
  flags: (
    ([`--days`],           [`int`],    [Number of days to look back. Default: `10`.]),
    ([`--json`],           [`flag`],   [Output as JSON.]),
    ([`--resonance-type`], [`string`], [Filter by resonance type. Defaults to ephemeral only when `--all-types` is omitted.]),
    ([`--all-types`],      [`flag`],   [Surface all resonance types instead of ephemeral only.]),
    ([`--sort`],           [`enum`],   [Sort order: `chronological` (default) or `resonance` (highest first).]),
    ([`--limit`],          [`int`],    [Maximum number of results. Default: `100`.]),
  ),
  examples: (
    "mx memory recent",
    "mx memory recent --days 30 --all-types --sort resonance",
    "mx memory recent --resonance-type foundational --limit 10",
  ),
)


// ═══════════════════════════════════════════════════════════════════════
// UPDATING ENTRIES
// ═══════════════════════════════════════════════════════════════════════

== Updating entries <updating>

#command(
  "mx memory update",
  [Update an existing entry. Supports replacing content entirely, appending,
  prepending, find-and-replace, and modifying any metadata field. Content
  mutation modes are mutually exclusive.],
  flags: (
    ([`-t, --title`],     [`string`], [Update the title.]),
    ([`--content`],       [`string`], [Replace content entirely (inline).]),
    ([`-f, --file`],      [`path`],   [Replace content entirely from file.]),
    ([`--append-content`], [`string`], [Append text to end of existing content.]),
    ([`--append-file`],   [`path`],   [Append content from file to end.]),
    ([`--prepend-content`], [`string`], [Prepend text to start of existing content.]),
    ([`--prepend-file`],  [`path`],   [Prepend content from file to start.]),
    ([`--find`],          [`string`], [Find text in content (requires `--replace`).]),
    ([`--replace`],       [`string`], [Replace text found by `--find`.]),
    ([`--replace-all`],   [`flag`],   [Replace all occurrences (with `--find`/`--replace`).]),
    ([`--nth`],           [`int`],    [Replace only the Nth occurrence (1-indexed).]),
    ([`--category`],      [`string`], [Update category.]),
    ([`--tags`],          [`string`], [Replace all tags (comma-separated).]),
    ([`--add-tag`],       [`string`], [Add a single tag to existing tags.]),
    ([`--remove-tag`],    [`string`], [Remove a specific tag.]),
    ([`-a, --applicability`], [`string`], [Update applicability (comma-separated, replaces all).]),
    ([`--content-type`],  [`string`], [Update content type.]),
    ([`--resonance`],     [`int`],    [Update resonance level (1--10+).]),
    ([`--resonance-type`], [`string`], [Update resonance type.]),
    ([`--anchors`],       [`string`], [Replace all anchors (comma-separated bloom IDs).]),
    ([`--add-anchor`],    [`string`], [Add a single anchor.]),
    ([`--remove-anchor`], [`string`], [Remove a specific anchor.]),
    ([`--wake-phrase`],   [`string`], [Update wake phrase.]),
    ([`--wake-phrases`],  [`string`], [Replace all wake phrases (comma-separated).]),
    ([`--add-wake-phrase`], [`string`], [Add a single wake phrase.]),
    ([`--remove-wake-phrase`], [`string`], [Remove a specific wake phrase.]),
    ([`--wake-order`],    [`string`], [Update wake order. Use `'-'` to clear.]),
    ([`--private`],       [`flag`],   [Mark as private (shorthand for `--visibility private`).]),
    ([`--visibility`],    [`string`], [Change visibility: `public` or `private`.]),
    ([`--owner`],         [`string`], [Update owner (only valid when visibility is private).]),
    ([`--session-id`],    [`string`], [Update session ID (for retrofitting entries with wrong or missing session linkage).]),
    ([`--force`],         [`flag`],   [Force dangerous visibility changes (e.g., making blooms public).]),
    ([`--no-auto-anchor`], [`flag`],  [Skip automatic anchor generation.]),
    ([`--no-embed`],      [`flag`],  [Skip synchronous embedding generation on write.]),
    ([`--json`],          [`flag`],   [Output as JSON.]),
  ),
  examples: (
    "mx memory update kn-abc123 --title \"Better title\"",
    "mx memory update kn-abc123 --add-tag reliability",
    "mx memory update kn-abc123 --find \"old text\" --replace \"new text\"",
    "mx memory update kn-abc123 --append-content \"\\n\\nUpdate: confirmed working\"",
    "mx memory update kn-abc123 --resonance 8 --resonance-type foundational",
  ),
)

#command(
  "mx memory edit",
  [Find-and-replace shortcut. Equivalent to
  `mx memory update <id> --find ... --replace ...` with a simpler interface.],
  flags: (
    ([`--find`],        [`string`], [Text to find in content. Also accepts `--old`.]),
    ([`--replace`],     [`string`], [Replacement text. Also accepts `--new`.]),
    ([`--replace-all`], [`flag`],   [Replace all occurrences (default: error if multiple matches).]),
    ([`--nth`],         [`int`],    [Replace only the Nth occurrence (1-indexed).]),
    ([`--no-auto-anchor`], [`flag`], [Skip automatic anchor generation.]),
    ([`--no-embed`],    [`flag`],   [Skip synchronous embedding generation on write.]),
    ([`--json`],        [`flag`],   [Output as JSON.]),
  ),
  examples: (
    "mx memory edit kn-abc123 --find \"old pattern\" --replace \"new pattern\"",
    "mx memory edit kn-abc123 --old \"v1\" --new \"v2\" --replace-all",
  ),
)

#command(
  "mx memory append",
  [Append content to the end of an entry's body. Shortcut for
  `mx memory update <id> --append-content ...`.],
  flags: (
    ([`--content`], [`string`], [Content to append (omit to read from stdin).]),
    ([`-f, --file`], [`path`],  [Read content from file. Also accepts `--content-file`.]),
    ([`--no-auto-anchor`], [`flag`], [Skip automatic anchor generation.]),
    ([`--no-embed`], [`flag`],  [Skip synchronous embedding generation on write.]),
    ([`--json`],    [`flag`],   [Output as JSON.]),
  ),
  examples: (
    "mx memory append kn-abc123 --content \"\\n\\nAdditional note here.\"",
    "mx memory append kn-abc123 -f addendum.md",
  ),
)

#command(
  "mx memory prepend",
  [Prepend content to the start of an entry's body. Shortcut for
  `mx memory update <id> --prepend-content ...`.],
  flags: (
    ([`--content`], [`string`], [Content to prepend (omit to read from stdin).]),
    ([`-f, --file`], [`path`],  [Read content from file. Also accepts `--content-file`.]),
    ([`--no-auto-anchor`], [`flag`], [Skip automatic anchor generation.]),
    ([`--no-embed`], [`flag`],  [Skip synchronous embedding generation on write.]),
    ([`--json`],    [`flag`],   [Output as JSON.]),
  ),
  examples: (
    "mx memory prepend kn-abc123 --content \"IMPORTANT: \"",
  ),
)

#command(
  "mx memory restore",
  [Restore entry content from a backup. Use `--list` to see available backups
  before restoring.],
  flags: (
    ([`--list`], [`flag`], [List available backups instead of restoring.]),
    ([`--no-auto-anchor`], [`flag`], [Skip automatic anchor generation.]),
    ([`--no-embed`], [`flag`], [Skip synchronous embedding generation on write.]),
    ([`--json`], [`flag`], [Output as JSON.]),
  ),
  examples: (
    "mx memory restore kn-abc123 --list",
    "mx memory restore kn-abc123",
  ),
)


// ═══════════════════════════════════════════════════════════════════════
// DELETING ENTRIES
// ═══════════════════════════════════════════════════════════════════════

== Deleting entries <deleting>

#command(
  "mx memory delete",
  [Remove an entry from the knowledge graph.],
  flags: (
    ([`--json`], [`flag`], [Output as JSON.]),
  ),
  examples: (
    "mx memory delete kn-abc123",
  ),
)


// ═══════════════════════════════════════════════════════════════════════
// WAKE SYSTEM
// ═══════════════════════════════════════════════════════════════════════

== Wake system <wake>

The wake system provides identity bootstrap for agents. It retrieves
high-resonance entries ("blooms") and presents them through a cascade that
reconnects the agent to its knowledge. The default output is a plain-text
cascade; a token-based ritual flow is available for programmatic use.

#command(
  "mx memory wake",
  [Wake up with resonant identity cascade. Retrieves high-resonance blooms
  and presents them in the requested format.],
  flags: (
    ([`-l, --limit`],   [`int`],    [Number of blooms to return. Default: `20`.]),
    ([`--min-resonance`], [`int`],   [Minimum resonance threshold -- get ALL blooms >= this value (overrides `--limit`). Filtered on the *raw* stored resonance, unlike `list`/`search`, which use the decayed *effective* value (Issue \#404).]),
    ([`-d, --days`],    [`int`],    [Include memories activated in last N days. Default: `7`.]),
    ([`--no-activate`], [`flag`],   [Do not update activation counts.]),
    ([`--begin`],       [`flag`],   [Start token-based wake ritual. Returns first bloom and session token.]),
    ([`--bloom-id`],    [`string`], [Bloom ID for `--respond`.]),
    ([`--respond`],     [`string`], [Submit your one guess for this bloom. Maximum 2000 characters.]),
    ([`--session`],     [`string`], [Session token for chained ritual (required with `--respond`).]),
    ([`--wake`],        [`int`],    [Wake number recorded on every guess row (with `--begin`). Optional; mx keeps no counter of its own. Must be positive. Refused when another ritual already logged guesses under it.]),
    ([`--force-wake`],  [`flag`],   [Begin under a `--wake` number another ritual already logged guesses under.]),
    ([`--model`],       [`string`], [Model identifier recorded on every guess row (with `--begin`). Optional; mx cannot discover it.]),
    ([`--include-excluded`], [`flag`], [Include entries tagged `archive` or `wake-exclude`, which are kept out of the wake set by default.]),
  ),
  examples: (
    "# Default wake -- top 20 blooms, text output\nmx memory wake",
    "# All blooms with resonance >= 7\nmx memory wake --min-resonance 7",
    "# Token-based ritual (for non-TTY / programmatic use)\nmx memory wake --begin --wake 7 --model some-model-id\nmx memory wake --bloom-id kn-abc --respond \"your guess\" --session tok-xyz",
  ),
)

#note[`MX_CURRENT_AGENT` must be set for wake to function. The wake system
reads blooms ordered by resonance and wake order.]

=== Wake modes

- *Default* (`mx memory wake`): plain text cascade output, blooms listed with titles and content.
- *Token-based* (`--begin`, `--respond`): stateless chained ritual for non-interactive environments. Start with `--begin`, then loop with `--respond` using the returned session token and bloom ID.

=== The ritual flow

Each chunk gets exactly one guess, made from the title alone, after which the
entry is shown. There is no hint ladder and no second attempt.

`--respond` returns `status: "shown"` with a `bucket` of either `unhinted` (the
guess string-matched one of the chunk's phrases, and the tool had given no
hint) or `revealed` (it did not). `match` carries the mechanical facts:
`kind` is `exact` (identical to a phrase after trimming), `close` (matched
only after normalizing case, punctuation or spelling) or `none`, and
`phrase_index` says which phrase matched. The bucket names what the tool did,
not what the responder knew.

The begin response and every respond payload echo `wake` and `model`. `--begin`
refuses a wake number another session already logged guesses under (pass
`--force-wake` to proceed anyway) and adds a `warnings` entry when the number is
below the highest the agent has logged, or more than one above it.

A step that is already logged is answered from the log rather than judged
again: the response carries `replayed: true`, the *logged* `guess`, `bucket`
and `match`, and the current token and `next`. The first guess counts.

A caller whose response was lost retries with the token it spent and gets that
response again, with `replayed: true` and the token it never received — however
far the lost call moved the ritual, including when it judged nothing: a lost
`bloom_missing` or `chunk_truncated` is answered with the same status (without
`bloom`). Only the last call can be resumed this way. A token older than that is
refused with a pointer to the last one that worked, and the pointer is true: the
token from the last successful response is always either current or resumable.

The final response carries a summary of bucket counts split by phrase source
(`authored`, `derived`, `auto`, `unphrased`), the number of chunks walked, and `unjudged` —
the steps where no guess was judged. `chunks` always equals the bucket totals
plus `unjudged`, so the difference never has to be worked out by subtraction.

Two statuses judge no guess, write no row, and omit `bucket`, `guess` and
`match`: `chunk_truncated`, when the entry shrank past the session's chunk
cursor, and `bloom_missing`, when the entry was deleted mid-ritual. Both still
advance the step, so the token the caller spent stops verifying, and both are
counted in `unjudged`.

The ritual is walked by the agent that began it: `MX_CURRENT_AGENT` must equal
the session's agent, and a caller naming no agent is refused as well.

A guess is refused outright, with no row written and no advance, when it is
longer than 2000 characters (counted in characters, and never truncated) or
carries no alphanumeric content at all. A refused call can simply be retried
with the same session token.

A session created by a version of mx older than the one-guess ritual is refused
rather than continued. A completed session is kept, with `completed_at` set.

Naming the wrong entry is refused with `{status: "error", error:
"invalid_bloom_id", message, expected_id?}` on stderr and exit code 1, like
every other error.

=== The guess log

Every `--respond` that judges a guess writes one `wake_guess` row, keyed by
session and step, in the same transaction as the session advance: the agent, wake number, model, entry, chunk, both positions
in the sequence, the title as shown, the guess, the phrases it was matched
against, the match, the bucket, and the SHA-256 of the chunk text. If the
transaction fails, the respond call fails and neither the row nor the advance
lands.

Guess rows are scoped to the writing agent and are not reachable through
`mx memory export`, which reads the knowledge table only.

=== Scoring and reading the guess log

`mx memory wake-log` scores the log and reads it back. Every command works on
the calling agent's rows only (`MX_CURRENT_AGENT`, required) and filters by
agent in every query.

#command(
  "mx memory wake-log score",
  [Score every pending guess row (`scored_at` unset, including rows written
  before scoring existed). Loads the embedding model once, and not at all when
  nothing is pending. Its entire stdout is `{"status":"scored","rows":N,"skipped":M}`:
  no similarity value, guess or title, so a ritual driver can run it as its
  last step. A row that fails to score stays pending, counts in `skipped`, and
  is retried next run. If the model cannot be loaded the command exits non-zero
  with the error on stderr, prints nothing on stdout, and leaves every row
  pending.],
  flags: (),
  examples: ("mx memory wake-log score",),
)

Per row it stores the guess embedding and:

- *Axis A* (tunable; never identity evidence). `sim_phrase`: the best cosine
  similarity to the row's own `authored` phrase snapshot (none for `derived`,
  `auto` or `unphrased`). `sim_content`: the best similarity to the entry's stored vectors
  under the same embedding model, the mean-pooled entry embedding and every
  embedding chunk. `sim_title`: similarity to the title, with the ritual's own
  `(Part N/M)` suffix removed.
- *Axis B*. `sim_prior`: similarity to the centroid of the 5 most recent
  earlier scored guesses on the same entry and the same shown title (suffix
  included), under the same embedding model, from other sessions; `prior_n` is
  how many there were. `sim_prior_null`: the mean similarity to the same kind
  of centroid for up to 10 other entries, those with the most recent history.
  The signal is the gap between the two, never `sim_prior` alone. The same
  three values are also stored over prior rows answered by the same `model_id`
  (`*_same`) and by a different known one (`*_cross`); a row with no `model_id`
  gets none of those. Axis B is empty on the first ritual: there is no history.
  The 5 and the 10 are defaults with no data behind them yet.

Rows are scored oldest first, each written before the next one is computed, so
one run over many rituals stores what a run after every ritual would have.
A skipped row is the exception: rows scored while it was pending are computed
without it, and are not recomputed when it scores later.

Authored-phrase vectors are cached in `wake_phrase_embedding`, keyed by a
SHA-256 of the embedding model and the whitespace-normalized phrase, with no
phrase text stored, so a later ritual embeds only each row's guess and title.
A cache that cannot be read or written costs only time: the row still scores.

#note[*Known limitation of `sim_content`:* ritual chunks are cut by byte
threshold and embedding chunks by token count. They do not line up, so
`sim_content` is measured against the whole entry, not the specific ritual
chunk the guess was made on. For single-chunk entries this makes no
difference.]

#command(
  "mx memory wake-log report",
  [The Goodhart text, then bucket counts and both axes (medians) for one ritual.
  Bucket counts are split by phrase source, with an `unphrased` column for
  later chunks prompted with no phrase. An `authored` row past the first chunk,
  logged before \#469, is marked `leaked` (a `leaked` field in JSON, after the
  bucket in text), counted in its own column instead of `authored`, and left
  out of `sim_phrase`. Terminal only; see below.],
  flags: (
    ([`--wake`], [`int`],  [Wake to report. Default: the most recent wake with scored rows.]),
    ([`--out`],  [`path`], [Write to this file instead (JSON if it ends in `.json`, text otherwise) and print only its path.]),
  ),
  examples: ("mx memory wake-log report", "mx memory wake-log report --wake 7 --out wake-7.json"),
)

#command(
  "mx memory wake-log bloom",
  [One entry across rituals, newest first: wake, date, model, position, guess,
  bucket and the five similarity fields. The view for retuning a phrase.
  Terminal only.],
  flags: (
    ([`<ID>`],    [`string`], [Entry ID (the `kn-` prefix is optional).]),
    ([`--limit`], [`int`],    [Maximum rows. Default: `20`.]),
    ([`--out`],   [`path`],   [Write to this file instead and print only its path.]),
  ),
  examples: ("mx memory wake-log bloom kn-abc",),
)

#command(
  "mx memory wake-log wake",
  [Every row of one ritual in sequence order. Terminal only.],
  flags: (
    ([`<N>`],   [`int`],  [Wake number.]),
    ([`--out`], [`path`], [Write to this file instead and print only its path.]),
  ),
  examples: ("mx memory wake-log wake 7",),
)

Every report starts with this text, and `wake-log report --help` carries it:

#quote[Axis A rises by construction when wake phrases are retuned toward logged
guesses. It measures how well the phrases fit what the model says. It is a
tuning dial for phrase authoring and must never be reported as evidence of
identity or memory. Axis B is the only number here that phrase edits cannot
raise. Axis B is evidence of stable reaching, not of remembering.]

`--begin`, `--respond` and their replays never carry an axis value.

#note[*The terminal-only rule.* The scores must never reach the context of the
model that made the guesses: a model that can see its score can play to it,
and one that can read its own earlier guesses will repeat them. So `report`,
`bloom` and `wake` print to stdout only when stdout is a terminal. Anywhere else
they refuse with an error naming the rule, before reading anything, unless
`--out FILE` is given. A model's shell tool is not a terminal; a human shell
is. An `--out` path that already exists must be a regular file, so
`/dev/stdout` cannot route the output back onto a pipe. An `--out` that names
the process's own stdout or stderr is refused too, since a model's shell tool
may capture them in a file. In the text layout every control character and
bidi control in a stored string is shown escaped (`\n`, `\u{1b}`,
`\u{202e}`), so a guess cannot drive or reorder the terminal.

Be plain about what this guard is: it stops accidental and casual leakage,
which is the realistic failure. It does not stop a process that deliberately
fakes a terminal or reads back a file written with `--out`. The rest is
convention: these three commands are not run during a ritual session, and
logged guesses and scores are never quoted into any text a model reads. The
tool cannot enforce that.]

=== Wake set exclusion

Entries tagged `archive` or `wake-exclude` are kept out of every cascade layer
by default, and `--begin` reports per-tag counts in its `excluded` field. The
match is exact, so a tag that merely starts with `archive` is unaffected.
`--include-excluded` turns the exclusion off.

The core layer never lets an exclusion cost a kept entry its place: it widens
its query until it holds a full set of entries that survive the exclusion. The
recent and bridge layers fetch double their quota, which absorbs the ordinary
case but is not a guarantee — enough excluded entries in one layer can still
leave the wake set short, and batch-archiving is how you would meet that, since
a freshly tagged entry counts as recent for seven days.

Every layer counts only as far as its quota is filled, so an entry ranked below
the wake set is never counted. The reported figure is an upper bound on the
entries the exclusion kept out — an excluded entry displaces everything after
it, so one reached only because an earlier exclusion pushed the window down is
counted too, although it would not have made the cut untagged.

#command(
  "mx memory wake-fetch",
  [Fetch facts for the wake ritual. Returns entries with resonance >= 3
  across all types, sorted by resonance (highest first). Designed as a
  data source for wake ritual presentation.],
  flags: (
    ([`--days`],  [`int`], [Number of days to look back. Default: `15`.]),
    ([`--limit`], [`int`], [Maximum number of results. Default: `100`.]),
    ([`--exclude-tags`], [`string`], [Comma-separated list of tag prefixes. Drops any entry that carries at least one tag prefix-matching at least one of these values -- useful for excluding whole tag namespaces (e.g. `project/` removes every entry tagged `project/<anything>`). Matching is OR across both the entry's tags and the supplied prefixes. Empty segments from trailing commas are ignored.]),
  ),
  examples: (
    "mx memory wake-fetch",
    "mx memory wake-fetch --days 30 --limit 50",
    "# Exclude an entire tag namespace from the wake set\nmx memory wake-fetch --exclude-tags 'project/'",
    "# Exclude multiple namespaces at once\nmx memory wake-fetch --exclude-tags 'project/,scratch/'",
  ),
)


// ═══════════════════════════════════════════════════════════════════════
// EMBEDDINGS & ANCHORING
// ═══════════════════════════════════════════════════════════════════════

== Embeddings and anchoring <embeddings>

Embeddings enable semantic search and automatic relationship discovery.
Each entry can have a vector embedding generated from its title and content.
Anchors are connections between entries discovered via embedding similarity.

=== Chunked embeddings <chunked-embeddings>

Entries longer than 400 tokens are automatically split into overlapping chunks
before embedding. This ensures semantic search covers the full content of long
entries, not just the first 400 tokens.

*How it works:*

+ The entry's embedding text (title + body/summary + tags) is tokenized using
  the BGE-Base-EN-v1.5 tokenizer.
+ If the text fits within 400 tokens, a single embedding is generated and
  stored on the entry --- exactly as before. No chunks are created.
+ If the text exceeds 400 tokens, it is split into overlapping chunks with a
  sliding window: 400 tokens per chunk, 100-token overlap (stride 300).
+ Each chunk is embedded separately and stored in the `embedding_chunk` table.
+ A normalized mean vector of all chunk embeddings is stored on the entry's
  `embedding` field for `auto-anchor` compatibility.
+ The entry's `chunk_count` field records how many chunks were created (0 for
  unchunked entries).

*Semantic search with chunks:*

When `mx memory search --semantic` runs, it queries both unchunked entry
embeddings and chunk embeddings in parallel. Results are merged by taking the
maximum similarity score per entry --- if a chunk from entry X scores 0.92 and
the entry's mean vector scores 0.85, the entry's final score is 0.92. This
ensures long entries surface when any section is relevant, not just when the
overall average is relevant.

#tip[Short entries (≤400 tokens) behave exactly as before --- single embedding,
no chunks, no behavior change. Chunking only activates for entries that exceed
the 400-token threshold.]

#note[The `embedding_text()` method on entries no longer truncates body content.
The chunker handles length management, ensuring no content is lost during
embedding.]

#command(
  "mx memory embed",
  [Generate a vector embedding for one or all entries. Embeddings power
  semantic search (`--semantic` flag on `search`) and automatic anchoring.
  Long entries (>400 tokens) are automatically split into overlapping chunks,
  with each chunk embedded separately. Short entries get a single embedding.],
  flags: (
    ([`-a, --all`], [`flag`], [Embed all knowledge entries (instead of a single ID).]),
    ([`--long-only`], [`int`], [Only re-embed entries whose `embedding_text()` exceeds this many tokens. Entries at or below the threshold are skipped entirely. Use with `--all`. Useful for selectively re-embedding long entries that were previously truncated at a smaller token limit (e.g., 512).]),
  ),
  examples: (
    "mx memory embed kn-abc123",
    "mx memory embed --all",
    "# Re-embed only entries that exceed 512 tokens\nmx memory embed --all --long-only 512",
  ),
)

#command(
  "mx memory auto-anchor",
  [Automatically add anchors between entries based on embedding similarity.
  Processes a single entry or all entries that have embeddings.

  Also re-evaluates existing anchors: any anchor whose cosine similarity has
  fallen below the threshold (default 0.75) or risen above the near-duplicate
  ceiling (0.95) is pruned. This keeps the anchor graph self-cleaning --
  anchors that made sense once but no longer do are removed automatically.],
  flags: (
    ([`--threshold`],   [`float`], [Minimum cosine similarity (0.0--1.0). Default: `0.75`.]),
    ([`--max-anchors`], [`int`],   [Maximum anchors to add per entry. Default: `5`.]),
    ([`--dry-run`],     [`flag`],  [Preview changes without writing.]),
    ([`--detailed`],    [`flag`],  [Show similarity scores in output.]),
    ([`--fill`],        [`flag`],  [Only process entries with zero existing anchors. Fills gaps in the graph without touching already-anchored entries.]),
  ),
  examples: (
    "mx memory auto-anchor",
    "mx memory auto-anchor kn-abc123 --threshold 0.8 --max-anchors 3",
    "mx memory auto-anchor --dry-run --detailed",
    "mx memory auto-anchor --fill",
  ),
)

#tip[A typical workflow: run `mx memory embed --all` to generate embeddings,
then `mx memory auto-anchor --dry-run --detailed` to preview anchor
candidates, then `mx memory auto-anchor` to write them.]

#note[Anchors are also maintained automatically on every write operation
(`add`, `update`, `edit`, `append`, `prepend`, `restore`). After each write,
mx re-evaluates anchors and prunes stale ones using the same similarity
thresholds. Pass `--no-auto-anchor` on any of these commands to skip this
step -- useful for bulk operations or cleanup scripts where the overhead is
unwanted.]

#note[*Deferred embedding:* Pass `--no-embed` (or set `MX_SKIP_WRITE_EMBED=1`)
on any write command (`add`, `update`, `edit`, `append`, `prepend`, `restore`,
`add-batch`) to skip synchronous embedding generation on that write. The entry
is written and fully durable immediately; only the vector embedding is deferred.

*Trade-off:* entries written with `--no-embed` are absent from `--semantic`
(vector) search results until a later `mx memory embed --all` run fills the
gap. Keyword search (`mx memory search <query>`) and tag-based filters are
unaffected.

This flag exists to amortize the ~435 MB embedding model cold-load: use it with
`add-batch` (which does its own single hoisted embed pass at the end), or set
`MX_SKIP_WRITE_EMBED=1` globally in a deployment where a nightly
`mx memory embed --all` (e.g. via Cinder's `embed.nix` timer) keeps the graph
fresh.]


// ═══════════════════════════════════════════════════════════════════════
// RELATIONSHIPS
// ═══════════════════════════════════════════════════════════════════════

== Relationships <relationships>

Explicit typed edges between entries. While anchors are discovered
automatically via embedding similarity, relationships are manually declared
semantic connections.

#command(
  "mx memory relationships list",
  [List all relationships for an entry.],
  flags: (
    ([`--json`], [`flag`], [Output as JSON.]),
  ),
  examples: (
    "mx memory relationships list kn-abc123",
  ),
)

#command(
  "mx memory relationships add",
  [Add a typed relationship between two entries. By default, the target entry
  (`--to`) is automatically reinforced by +1 (capped at 10) when the
  relationship is created -- being linked to means the fact proved relevant.
  The `contradicts` and `supersedes` types are excluded from auto-reinforcement
  because boosting an outdated or contradicted entry works against intent.],
  flags: (
    ([`--from`],         [`string`], [Source entry ID.]),
    ([`--to`],           [`string`], [Target entry ID.]),
    ([`--type`],         [`string`], [Relationship type: `related`, `supersedes`, `extends`, `implements`, `contradicts`.]),
    ([`--no-reinforce`], [`flag`],   [Skip automatic reinforcement of the target entry.]),
  ),
  examples: (
    "mx memory relationships add --from kn-abc --to kn-def --type extends",
    "mx memory relationships add --from kn-abc --to kn-ghi --type supersedes",
    "# Add a relationship without auto-reinforcing the target\nmx memory relationships add --from kn-abc --to kn-def --type related --no-reinforce",
  ),
)

#command(
  "mx memory relationships delete",
  [Delete a relationship by its ID.],
  flags: (),
  examples: (
    "mx memory relationships delete rel-abc123",
  ),
)


// ═══════════════════════════════════════════════════════════════════════
// SEEDING
// ═══════════════════════════════════════════════════════════════════════

== Seeding <seeding>

Seed commands populate the knowledge graph from on-disk artifacts. Used for
initial setup and bulk import.

#command(
  "mx memory seed agents",
  [Seed agents from markdown files with YAML frontmatter. Reads from
  `$MX_HOME/memory/seed/agents/` by default.],
  flags: (
    ([`-p, --path`], [`path`], [Path to agents directory. Defaults to `$MX_HOME/memory/seed/agents/`.]),
  ),
  examples: (
    "mx memory seed agents",
    "mx memory seed agents --path /data/agents/",
  ),
)

#note[Legacy fallback: if `$MX_HOME/memory/seed/agents/` does not exist, mx
checks `$MX_HOME/agents/` and emits a stderr warning. This fallback will be
removed in a future release.]

#command(
  "mx memory seed knowledge",
  [Seed knowledge from JSONL files. With no path, scans
  `$MX_HOME/memory/seed/knowledge/*.jsonl` and imports every file found. With
  a path, imports just that single file.],
  flags: (),
  examples: (
    "mx memory seed knowledge",
    "mx memory seed knowledge /data/knowledge/bootstrap.jsonl",
  ),
)


// ═══════════════════════════════════════════════════════════════════════
// HEALTH & STATISTICS
// ═══════════════════════════════════════════════════════════════════════

== Health and statistics <health>

#command(
  "mx memory stats",
  [Show index statistics -- entry counts, category breakdown, and other
  aggregate metrics.],
  flags: (
    ([`--json`], [`flag`], [Output as JSON.]),
  ),
  examples: (
    "mx memory stats",
    "mx memory stats --json",
  ),
)

#command(
  "mx memory health",
  [Show graph health vitality percentages: embedding coverage, anchor
  coverage, and stale high-resonance entries.],
  flags: (
    ([`--json`], [`flag`], [Output as JSON (default format for dashboard consumers).]),
  ),
  examples: (
    "mx memory health",
    "mx memory health --json",
  ),
)

#command(
  "mx memory growth",
  [Show per-week entry growth over the last 8 weeks.],
  flags: (
    ([`--json`], [`flag`], [Output as JSON array of 8 integers (oldest to newest).]),
  ),
  examples: (
    "mx memory growth",
    "mx memory growth --json",
  ),
)

#command(
  "mx memory open-threads",
  [List open threads (`category:thread` entries with `state=\"open\"` or no
  state).],
  flags: (
    ([`--json`], [`flag`], [Output as JSON array (required for dashboard consumers).]),
  ),
  examples: (
    "mx memory open-threads",
    "mx memory open-threads --json",
  ),
)


// ═══════════════════════════════════════════════════════════════════════
// EXPORT
// ═══════════════════════════════════════════════════════════════════════

== Export <export>

#command(
  "mx memory export",
  [Export the entire knowledge database to a file or directory.],
  flags: (
    ([`-f, --format`], [`string`], [Output format: `md`, `jsonl`, `csv`. Default: `md`.]),
    ([`-o, --output`], [`path`],   [Output directory for `md` format (defaults to `./memory-export`), or file for `jsonl`/`csv` (defaults to stdout).]),
  ),
  examples: (
    "mx memory export",
    "mx memory export -f jsonl -o backup.jsonl",
    "mx memory export -f csv -o entries.csv",
    "mx memory export -f md -o /data/export/",
  ),
)


// ═══════════════════════════════════════════════════════════════════════
// REINFORCEMENT
// ═══════════════════════════════════════════════════════════════════════

== Reinforcement <reinforcement>

Reinforcement is the mechanism by which the knowledge graph breathes in --
entries that are used, referenced, or linked gain resonance, counteracting
the natural decay of the exhale. There are three reinforcement paths:

+ *Explicit reinforcement* via `mx memory reinforce` -- directly boost an
  entry's resonance.
+ *Auto-reinforce on relationship creation* -- when
  `mx memory relationships add` links to a target entry, the target is
  reinforced by +1 (capped at 10). The `contradicts` and `supersedes`
  types are excluded. Use `--no-reinforce` to opt out.
+ *Search activation* via `mx memory search --activate` -- marks returned
  results as intentionally consumed, resetting their decay clock and
  incrementing their activation count.

#command(
  "mx memory reinforce",
  [Reinforce a knowledge entry by incrementing its resonance, updating
  `last_activated`, and incrementing `activation_count`. Used to signal
  that an entry remains relevant.],
  flags: (
    ([`--amount`], [`int`], [Amount to increase resonance by. Default: `1`.]),
    ([`--cap`],    [`int`], [Maximum resonance cap. Default: `10`.]),
    ([`--json`],   [`flag`], [Output as JSON.]),
  ),
  examples: (
    "mx memory reinforce kn-abc123",
    "mx memory reinforce kn-abc123 --amount 2 --cap 8",
  ),
)


// ═══════════════════════════════════════════════════════════════════════
// METADATA MANAGEMENT
// ═══════════════════════════════════════════════════════════════════════

== Metadata management <metadata>

The knowledge graph has several registries for typed metadata. These
commands manage the registries themselves -- the types, categories, and
agent identities that entries reference.

=== Agents

#command(
  "mx memory agents list",
  [List all registered agents.],
  flags: (
    ([`--json`], [`flag`], [Output as JSON.]),
  ),
  examples: (
    "mx memory agents list",
  ),
)

#command(
  "mx memory agents add",
  [Register a new agent.],
  flags: (
    ([`-d, --description`], [`string`], [Agent description.]),
    ([`-D, --domain`],      [`string`], [Agent domain/responsibility.]),
  ),
  examples: (
    "mx memory agents add whistledown -d \"Round-trip builder\" -D \"development\"",
  ),
)

#command(
  "mx memory agents show",
  [Show details for a specific agent.],
  flags: (),
  examples: (
    "mx memory agents show whistledown",
  ),
)

=== Projects

#command(
  "mx memory projects list",
  [List all registered projects.],
  flags: (
    ([`--json`], [`flag`], [Output as JSON.]),
  ),
  examples: (
    "mx memory projects list",
  ),
)

#command(
  "mx memory projects add",
  [Register a new project.],
  flags: (
    ([`--id`],          [`string`], [Unique project identifier.]),
    ([`--name`],        [`string`], [Human-readable project name.]),
    ([`--path`],        [`path`],   [Local filesystem path to the project.]),
    ([`--repo-url`],    [`string`], [Git repository URL (e.g., `owner/repo`).]),
    ([`--description`], [`string`], [Project description.]),
  ),
  examples: (
    "mx memory projects add --id mx --name \"mx CLI\" \\\n  --repo-url coryzibell/mx --path ~/recipes/coryzibell/mx",
  ),
)

=== Categories

#command(
  "mx memory categories list",
  [List all categories.],
  flags: (
    ([`--json`], [`flag`], [Output as JSON.]),
  ),
  examples: (
    "mx memory categories list",
  ),
)

#command(
  "mx memory categories add",
  [Add a new category.],
  flags: (),
  examples: (
    "mx memory categories add pitfall \"Things that went wrong and why\"",
  ),
)

#command(
  "mx memory categories remove",
  [Remove a category (only if no entries use it).],
  flags: (),
  examples: (
    "mx memory categories remove pitfall",
  ),
)

=== Applicability

#command(
  "mx memory applicability list",
  [List all applicability types.],
  flags: (),
  examples: (
    "mx memory applicability list",
  ),
)

#command(
  "mx memory applicability add",
  [Add a new applicability type.],
  flags: (
    ([`--id`],          [`string`], [Unique identifier.]),
    ([`--description`], [`string`], [Description of when this applicability applies.]),
    ([`--scope`],       [`string`], [Scope constraint (e.g., `project`, `global`).]),
  ),
  examples: (
    "mx memory applicability add --id rust-only \\\n  --description \"Applies only to Rust projects\" --scope project",
  ),
)

=== Type registries

These are read-only registries listing the valid values for typed fields.
Each supports `list` with an optional `--json` flag.

#table(
  columns: (auto, auto),
  table.header([*Command*], [*Lists valid values for*]),
  [`mx memory tags list`],              [Tags used across entries. Supports `--category` filter.],
  [`mx memory source-types list`],      [Source types (`manual`, `ram`, `cache`, `agent_session`).],
  [`mx memory entry-types list`],       [Entry types (`primary`, `summary`, `synthesis`).],
  [`mx memory session-types list`],     [Session types (e.g., `development`, `review`, `exploration`).],
  [`mx memory relationship-types list`], [Relationship types (`related`, `supersedes`, `extends`, `implements`, `contradicts`).],
  [`mx memory content-types list`],     [Content types (`text`, `code`, `config`, `data`, `binary`).],
)

All type registry `list` commands accept `--json` for structured output.
`tags list` also accepts `--category` to filter tags to a specific category.


// ═══════════════════════════════════════════════════════════════════════
// SESSION TRACKING
// ═══════════════════════════════════════════════════════════════════════

== Session tracking <sessions>

Sessions group entries created during a work period. Entries can be linked
to sessions, and facts can be queried by their source session.

#command(
  "mx memory sessions list",
  [List sessions, optionally filtered by project.],
  flags: (
    ([`--project`], [`string`], [Filter by project ID.]),
    ([`--json`],    [`flag`],   [Output as JSON.]),
  ),
  examples: (
    "mx memory sessions list",
    "mx memory sessions list --project mx",
  ),
)

#command(
  "mx memory sessions create",
  [Create a new session.],
  flags: (
    ([`--session-type`], [`string`], [Session type (e.g., `development`, `review`, `exploration`).]),
    ([`--project`],      [`string`], [Associated project ID.]),
  ),
  examples: (
    "mx memory sessions create --session-type development --project mx",
  ),
)

#command(
  "mx memory sessions close",
  [Close an active session.],
  flags: (
    ([`--id`], [`string`], [Session ID to close.]),
  ),
  examples: (
    "mx memory sessions close --id ses-abc123",
  ),
)

#command(
  "mx memory for-session",
  [List facts extracted from a specific session. The session ID can be
  provided with or without the `kn-` prefix.],
  flags: (
    ([`--json`], [`flag`], [Output as JSON.]),
  ),
  examples: (
    "mx memory for-session ses-abc123",
  ),
)

#command(
  "mx memory fact-session",
  [Get the session a fact was extracted from. The fact ID can be provided
  with or without the `kn-` prefix.],
  flags: (
    ([`--json`], [`flag`], [Output as JSON.]),
  ),
  examples: (
    "mx memory fact-session kn-abc123",
  ),
)