@ai2070/memex 0.9.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 (46) hide show
  1. package/.github/workflows/ci.yml +31 -0
  2. package/.github/workflows/release.yml +35 -0
  3. package/API.md +1078 -0
  4. package/LICENSE +190 -0
  5. package/README.md +574 -0
  6. package/package.json +30 -0
  7. package/src/bulk.ts +128 -0
  8. package/src/envelope.ts +52 -0
  9. package/src/errors.ts +27 -0
  10. package/src/graph.ts +15 -0
  11. package/src/helpers.ts +51 -0
  12. package/src/index.ts +142 -0
  13. package/src/integrity.ts +378 -0
  14. package/src/intent.ts +311 -0
  15. package/src/query.ts +357 -0
  16. package/src/reducer.ts +177 -0
  17. package/src/replay.ts +32 -0
  18. package/src/retrieval.ts +306 -0
  19. package/src/serialization.ts +34 -0
  20. package/src/stats.ts +62 -0
  21. package/src/task.ts +373 -0
  22. package/src/transplant.ts +488 -0
  23. package/src/types.ts +248 -0
  24. package/tests/bugfix-and-coverage.test.ts +958 -0
  25. package/tests/bugfix-holes.test.ts +856 -0
  26. package/tests/bulk.test.ts +256 -0
  27. package/tests/edge-cases-v2.test.ts +355 -0
  28. package/tests/edge-cases.test.ts +661 -0
  29. package/tests/envelope.test.ts +92 -0
  30. package/tests/graph.test.ts +41 -0
  31. package/tests/helpers.test.ts +120 -0
  32. package/tests/integrity.test.ts +371 -0
  33. package/tests/intent.test.ts +276 -0
  34. package/tests/query-advanced.test.ts +252 -0
  35. package/tests/query.test.ts +623 -0
  36. package/tests/reducer.test.ts +342 -0
  37. package/tests/replay.test.ts +145 -0
  38. package/tests/retrieval.test.ts +691 -0
  39. package/tests/serialization.test.ts +118 -0
  40. package/tests/setup.test.ts +7 -0
  41. package/tests/stats.test.ts +163 -0
  42. package/tests/task.test.ts +322 -0
  43. package/tests/transplant.test.ts +385 -0
  44. package/tests/types.test.ts +231 -0
  45. package/tsconfig.json +18 -0
  46. package/vitest.config.ts +7 -0
package/API.md ADDED
@@ -0,0 +1,1078 @@
1
+ # API Reference
2
+
3
+ ## Types
4
+
5
+ ### MemoryItem
6
+
7
+ The core node in the graph.
8
+
9
+ ```ts
10
+ interface MemoryItem {
11
+ id: string; // uuidv7
12
+ scope: string; // e.g. "user:laz/general", "project:cyberdeck"
13
+ kind: MemoryKind; // what it is
14
+ content: Record<string, unknown>;
15
+
16
+ author: string; // "user:laz", "agent:reasoner", "system:rule_x"
17
+ source_kind: SourceKind; // how it got here
18
+ parents?: string[]; // item ids this was derived/inferred from
19
+
20
+ authority: number; // 0..1 -- how much should the system trust this?
21
+ conviction?: number; // 0..1 -- how sure was the author?
22
+ importance?: number; // 0..1 -- how much attention does this need right now? (salience)
23
+
24
+ meta?: {
25
+ agent_id?: string;
26
+ session_id?: string;
27
+ [key: string]: unknown;
28
+ };
29
+ }
30
+ ```
31
+
32
+ **`kind`** -- what the item is:
33
+
34
+ | Kind | Meaning |
35
+ |------|---------|
36
+ | `observation` | Directly witnessed / sensed |
37
+ | `assertion` | Stated as true by an author |
38
+ | `assumption` | Believed but not verified |
39
+ | `hypothesis` | Proposed explanation, testable |
40
+ | `derivation` | Deterministically computed from other items |
41
+ | `simulation` | Output of a hypothetical scenario |
42
+ | `policy` | A rule or guideline |
43
+ | `trait` | A persistent characteristic |
44
+
45
+ Accepts arbitrary strings beyond the known set.
46
+
47
+ **`source_kind`** -- how the item got here:
48
+
49
+ | Source Kind | Meaning |
50
+ |-------------|---------|
51
+ | `user_explicit` | User directly stated it |
52
+ | `observed` | System observed it |
53
+ | `derived_deterministic` | Computed from other items via rules |
54
+ | `agent_inferred` | Agent reasoned it |
55
+ | `simulated` | Produced by simulation |
56
+ | `imported` | Imported from external source |
57
+
58
+ ### Edge
59
+
60
+ Typed relationship between items.
61
+
62
+ ```ts
63
+ interface Edge {
64
+ edge_id: string;
65
+ from: string; // item id
66
+ to: string; // item id
67
+ kind: EdgeKind; // relationship type
68
+ weight?: number;
69
+ author: string;
70
+ source_kind: SourceKind;
71
+ authority: number;
72
+ active: boolean;
73
+ meta?: Record<string, unknown>;
74
+ }
75
+ ```
76
+
77
+ **Edge kinds:**
78
+
79
+ | Kind | Meaning |
80
+ |------|---------|
81
+ | `DERIVED_FROM` | Source was derived from target (external/after-the-fact) |
82
+ | `CONTRADICTS` | Two items assert conflicting things |
83
+ | `SUPPORTS` | Source provides evidence for target |
84
+ | `ABOUT` | Source is about / references target |
85
+ | `SUPERSEDES` | Source replaces target (conflict resolution) |
86
+ | `ALIAS` | Both items refer to the same entity |
87
+
88
+ **`parents` vs `DERIVED_FROM`:**
89
+
90
+ - **`parents`** (on MemoryItem) is the source of truth for provenance. It means "this item was created from these inputs." It's structural — set at creation time, used by `getParents`, `getChildren`, `getSupportTree`, `cascadeRetract`, and the `has_parent`/`is_root` filters.
91
+ - **`DERIVED_FROM`** (edge) is for relationships added after the fact — "we later discovered that item A was influenced by item B." It's relational, not structural.
92
+
93
+ Use `parents` when creating derived items. Use `DERIVED_FROM` edges when annotating relationships between existing items that weren't captured at creation time.
94
+
95
+ ### EventEnvelope
96
+
97
+ Common wrapper for all events on the bus.
98
+
99
+ ```ts
100
+ interface EventEnvelope<T = unknown> {
101
+ id: string; // uuidv7
102
+ namespace: Namespace; // "memory", "task", "agent", "tool", "net", "app", "chat", "system", "debug"
103
+ type: string;
104
+ ts: string; // ISO-8601
105
+ trace_id?: string;
106
+ payload: T;
107
+ }
108
+ ```
109
+
110
+ ### GraphState
111
+
112
+ ```ts
113
+ interface GraphState {
114
+ items: Map<string, MemoryItem>;
115
+ edges: Map<string, Edge>;
116
+ }
117
+ ```
118
+
119
+ ---
120
+
121
+ ## Factories
122
+
123
+ ### createMemoryItem(input)
124
+
125
+ Creates a `MemoryItem` with auto-generated `id` (uuidv7). Validates scores are in [0, 1].
126
+
127
+ ```ts
128
+ const item = createMemoryItem({
129
+ scope: "user:laz/general",
130
+ kind: "observation",
131
+ content: { key: "theme", value: "dark" },
132
+ author: "user:laz",
133
+ source_kind: "user_explicit",
134
+ authority: 0.9,
135
+ });
136
+ ```
137
+
138
+ ### createEdge(input)
139
+
140
+ Creates an `Edge` with auto-generated `edge_id`. Defaults `active` to `true`.
141
+
142
+ ### createEventEnvelope(type, payload, opts?)
143
+
144
+ Creates an `EventEnvelope` with `namespace: "memory"`, auto-generated id and timestamp.
145
+
146
+ ### createGraphState()
147
+
148
+ Returns an empty `GraphState`.
149
+
150
+ ### cloneGraphState(state)
151
+
152
+ Shallow-clones a `GraphState` (new Maps, same entries).
153
+
154
+ ---
155
+
156
+ ## Reducer
157
+
158
+ ### applyCommand(state, cmd)
159
+
160
+ Pure function. Takes a `GraphState` and a `MemoryCommand`, returns a new state and lifecycle events.
161
+
162
+ ```ts
163
+ const { state, events } = applyCommand(state, {
164
+ type: "memory.create",
165
+ item: myItem,
166
+ });
167
+ ```
168
+
169
+ **Commands:**
170
+
171
+ | Command | Fields | Lifecycle Event |
172
+ |---------|--------|-----------------|
173
+ | `memory.create` | `item: MemoryItem` | `memory.created` |
174
+ | `memory.update` | `item_id`, `partial`, `author`, `reason?`, `basis?` | `memory.updated` |
175
+ | `memory.retract` | `item_id`, `author`, `reason?` | `memory.retracted` |
176
+ | `edge.create` | `edge: Edge` | `edge.created` |
177
+ | `edge.update` | `edge_id`, `partial`, `author`, `reason?` | `edge.updated` |
178
+ | `edge.retract` | `edge_id`, `author`, `reason?` | `edge.retracted` |
179
+
180
+ **Merge behavior:**
181
+ - `content` is shallow-merged (`{ ...existing.content, ...partial.content }`)
182
+ - `meta` is shallow-merged (`{ ...existing.meta, ...partial.meta }`)
183
+ - `undefined` values in partials are ignored (field is not changed)
184
+ - `id` in partials is ignored (cannot change item identity)
185
+ - All other fields are replaced
186
+
187
+ **Errors:** `DuplicateMemoryError`, `MemoryNotFoundError`, `DuplicateEdgeError`, `EdgeNotFoundError`.
188
+
189
+ ---
190
+
191
+ ## Queries
192
+
193
+ ### getItems(state, filter?, options?)
194
+
195
+ Returns items matching a filter, with optional sort/limit/offset.
196
+
197
+ ```ts
198
+ const items = getItems(state, {
199
+ scope_prefix: "user:laz/",
200
+ or: [{ kind: "observation" }, { kind: "assertion" }],
201
+ range: { authority: { min: 0.5 } },
202
+ }, {
203
+ sort: [
204
+ { field: "authority", order: "desc" },
205
+ { field: "recency", order: "desc" },
206
+ ],
207
+ limit: 10,
208
+ });
209
+ ```
210
+
211
+ ### MemoryFilter
212
+
213
+ All fields are optional and AND-combined.
214
+
215
+ ```ts
216
+ interface MemoryFilter {
217
+ ids?: string[]; // match any of these item ids
218
+ scope?: string; // exact match
219
+ scope_prefix?: string; // starts with, e.g. "project:"
220
+ author?: string;
221
+ kind?: MemoryKind;
222
+ source_kind?: SourceKind;
223
+
224
+ range?: {
225
+ authority?: { min?: number; max?: number };
226
+ conviction?: { min?: number; max?: number };
227
+ importance?: { min?: number; max?: number };
228
+ };
229
+
230
+ has_parent?: string; // sugar for parents.includes
231
+ is_root?: boolean; // sugar for parents.count.max = 0
232
+ parents?: { // advanced parent query
233
+ includes?: string; // has this parent
234
+ includes_any?: string[]; // has at least one of these parents
235
+ includes_all?: string[]; // has all of these parents
236
+ count?: { min?: number; max?: number };
237
+ };
238
+
239
+ decay?: { // exclude items that have decayed too much
240
+ config: DecayConfig;
241
+ min: number; // 0..1 — minimum decay multiplier to keep
242
+ };
243
+
244
+ created?: { // filter by creation time (from uuidv7 id)
245
+ before?: number; // unix ms
246
+ after?: number; // unix ms
247
+ };
248
+
249
+ not?: MemoryFilter; // exclude items matching this filter
250
+ meta?: Record<string, unknown>;// dot-path exact match
251
+ meta_has?: string[]; // dot-paths that must exist
252
+ or?: MemoryFilter[]; // match if ANY sub-filter matches
253
+ }
254
+ ```
255
+
256
+ ### DecayConfig
257
+
258
+ Used in both filters (exclude decayed items) and scoring (decay-adjusted ranking).
259
+
260
+ ```ts
261
+ interface DecayConfig {
262
+ rate: number; // 0..1 — how much to decay per interval
263
+ interval: "hour" | "day" | "week";
264
+ type: "exponential" | "linear" | "step";
265
+ }
266
+ ```
267
+
268
+ **Examples:**
269
+
270
+ ```ts
271
+ // filter by specific ids (e.g. from vector search results)
272
+ { ids: ["m1", "m3", "m5"] }
273
+
274
+ // all project scopes
275
+ { scope_prefix: "project:" }
276
+
277
+ // observations OR assertions
278
+ { or: [{ kind: "observation" }, { kind: "assertion" }] }
279
+
280
+ // authority between 0.3 and 0.9
281
+ { range: { authority: { min: 0.3, max: 0.9 } } }
282
+
283
+ // items derived from m1 AND m2
284
+ { parents: { includes_all: ["m1", "m2"] } }
285
+
286
+ // items with at least 2 parents
287
+ { parents: { count: { min: 2 } } }
288
+
289
+ // exclude items that have decayed below 50%
290
+ // (older than ~1 day at 50%/day exponential)
291
+ { decay: { config: { rate: 0.5, interval: "day", type: "exponential" }, min: 0.5 } }
292
+
293
+ // exclude hypotheses and simulations
294
+ { not: { or: [{ kind: "hypothesis" }, { kind: "simulation" }] } }
295
+
296
+ // nested meta dot-path
297
+ { meta: { "tags.env": "prod" } }
298
+
299
+ // field must exist, but not be this value
300
+ { meta_has: ["agent_id"], not: { meta: { agent_id: "agent:bad" } } }
301
+
302
+ // items derived from a specific parent
303
+ { has_parent: "m1" }
304
+
305
+ // root items only (no parents)
306
+ { is_root: true }
307
+
308
+ // items older than 24 hours
309
+ { created: { before: Date.now() - 86400000 } }
310
+
311
+ // items created in the last hour
312
+ { created: { after: Date.now() - 3600000 } }
313
+ ```
314
+
315
+ ### QueryOptions
316
+
317
+ ```ts
318
+ interface SortOption {
319
+ field: "authority" | "conviction" | "importance" | "recency";
320
+ order: "asc" | "desc";
321
+ }
322
+
323
+ interface QueryOptions {
324
+ sort?: SortOption | SortOption[]; // single or multi-sort (first = primary)
325
+ limit?: number;
326
+ offset?: number;
327
+ }
328
+ ```
329
+
330
+ `"recency"` sorts by creation time, extracted from the uuidv7 id.
331
+
332
+ ```ts
333
+ // single sort
334
+ { sort: { field: "authority", order: "desc" } }
335
+
336
+ // multi-sort: authority desc, then recency as tiebreaker
337
+ { sort: [
338
+ { field: "authority", order: "desc" },
339
+ { field: "recency", order: "desc" },
340
+ ] }
341
+ ```
342
+
343
+ ### getEdges(state, filter?)
344
+
345
+ Returns edges. Defaults to `active_only: true`.
346
+
347
+ ```ts
348
+ interface EdgeFilter {
349
+ from?: string;
350
+ to?: string;
351
+ kind?: EdgeKind;
352
+ min_weight?: number;
353
+ active_only?: boolean; // default: true
354
+ }
355
+ ```
356
+
357
+ ### getItemById(state, id) / getEdgeById(state, edgeId)
358
+
359
+ Direct lookup by id.
360
+
361
+ ### getRelatedItems(state, itemId, direction?)
362
+
363
+ Items connected via active edges. `direction`: `"from"`, `"to"`, or `"both"` (default).
364
+
365
+ ### getParents(state, itemId)
366
+
367
+ Returns items listed in `parents` of the given item.
368
+
369
+ ### getChildren(state, itemId)
370
+
371
+ Returns items that have the given item in their `parents`.
372
+
373
+ ### extractTimestamp(uuidv7Id)
374
+
375
+ Extracts millisecond unix timestamp from a uuidv7 id.
376
+
377
+ ```ts
378
+ const ms = extractTimestamp(item.id);
379
+ const date = new Date(ms);
380
+ ```
381
+
382
+ ---
383
+
384
+ ## Scored Retrieval
385
+
386
+ ### getScoredItems(state, weights, options?)
387
+
388
+ Scores items by a weighted combination of authority, conviction, and importance, with optional time-based decay. Returns `{ item, score }[]` sorted by score descending.
389
+
390
+ ```ts
391
+ interface ScoreWeights {
392
+ authority?: number; // multiplier
393
+ conviction?: number;
394
+ importance?: number;
395
+ decay?: DecayConfig; // time-based score decay (applied at query time)
396
+ }
397
+
398
+ interface ScoredQueryOptions {
399
+ pre?: MemoryFilter; // filter before scoring
400
+ post?: MemoryFilter; // filter after scoring
401
+ min_score?: number; // drop items below threshold
402
+ limit?: number;
403
+ offset?: number;
404
+ }
405
+ ```
406
+
407
+ **Pipeline:** `pre-filter -> score (with decay) -> min_score -> post-filter -> offset/limit`
408
+
409
+ ```ts
410
+ // scored retrieval with time decay
411
+ const ranked = getScoredItems(
412
+ state,
413
+ {
414
+ authority: 0.5,
415
+ conviction: 0.3,
416
+ importance: 0.2,
417
+ decay: { rate: 0.1, interval: "day", type: "exponential" },
418
+ },
419
+ {
420
+ pre: { scope: "user:laz/general" },
421
+ min_score: 0.3,
422
+ post: { not: { kind: "simulation" } },
423
+ limit: 10,
424
+ },
425
+ );
426
+ ```
427
+
428
+ **Decay types:**
429
+
430
+ | Type | Formula | Behavior |
431
+ |------|---------|----------|
432
+ | `exponential` | `(1 - rate) ^ intervals` | Smooth curve, never reaches zero |
433
+ | `linear` | `max(0, 1 - rate * intervals)` | Straight line to zero |
434
+ | `step` | `(1 - rate) ^ floor(intervals)` | Drops at each interval boundary |
435
+
436
+ Decay is computed at query time from the uuidv7 id timestamp. Stored `importance` is not mutated.
437
+
438
+ ### getItemsByBudget(state, options)
439
+
440
+ Greedy knapsack: pack the highest-scoring items that fit within a cost budget.
441
+
442
+ ```ts
443
+ interface BudgetOptions {
444
+ budget: number; // total budget
445
+ costFn: (item: MemoryItem) => number;
446
+ weights: ScoreWeights; // supports decay
447
+ filter?: MemoryFilter;
448
+ }
449
+
450
+ const context = getItemsByBudget(state, {
451
+ budget: 4096,
452
+ costFn: (item) => JSON.stringify(item.content).length,
453
+ weights: { authority: 0.5, importance: 0.5 },
454
+ filter: { scope: "user:laz/general" },
455
+ });
456
+ ```
457
+
458
+ ---
459
+
460
+ ## Smart Retrieval
461
+
462
+ ### smartRetrieve(state, options)
463
+
464
+ Combined pipeline: score (with decay), filter contradictions, apply diversity, pack within budget.
465
+
466
+ ```ts
467
+ interface SmartRetrievalOptions {
468
+ budget: number;
469
+ costFn: (item: MemoryItem) => number;
470
+ weights: ScoreWeights; // supports decay
471
+ filter?: MemoryFilter;
472
+ contradictions?: "filter" | "surface"; // "filter" = keep winner, "surface" = keep both + flag
473
+ diversity?: DiversityOptions; // penalize duplicate authors/parents/sources
474
+ }
475
+ ```
476
+
477
+ **Pipeline:** `filter -> score (with decay) -> contradiction filter -> diversity re-rank -> budget pack`
478
+
479
+ ```ts
480
+ const context = smartRetrieve(state, {
481
+ budget: 4096,
482
+ costFn: (item) => JSON.stringify(item.content).length,
483
+ weights: {
484
+ authority: 0.5,
485
+ importance: 0.5,
486
+ decay: { rate: 0.1, interval: "day", type: "exponential" },
487
+ },
488
+ filter: { scope: "user:laz/general" },
489
+ contradictions: "surface",
490
+ diversity: { author_penalty: 0.3, parent_penalty: 0.2 },
491
+ });
492
+ ```
493
+
494
+ ### filterContradictions(state, scored)
495
+
496
+ Removes superseded items (losers of resolved contradictions). For unresolved contradictions, keeps only the higher-scoring side. Use when you want a clean, non-contradictory result set.
497
+
498
+ ### surfaceContradictions(state, scored)
499
+
500
+ Removes superseded items but **keeps both sides** of unresolved contradictions. Each item involved in a contradiction gets a `contradicted_by` array listing the opposing items.
501
+
502
+ ```ts
503
+ const result = surfaceContradictions(state, scored);
504
+ // result[0].contradicted_by -> [opposingItem] (if contradicted)
505
+ // result[1].contradicted_by -> [opposingItem]
506
+ // result[2].contradicted_by -> undefined (no contradiction)
507
+ ```
508
+
509
+ Use when the consumer needs to see the tension rather than have it resolved for them.
510
+
511
+ ### applyDiversity(scored, options)
512
+
513
+ Re-ranks scored items with diversity penalties. Items are processed in score order; each subsequent item from the same author/parent/source gets penalized.
514
+
515
+ ```ts
516
+ interface DiversityOptions {
517
+ author_penalty?: number; // penalty per duplicate author (0..1)
518
+ parent_penalty?: number; // penalty per shared parent (0..1)
519
+ source_penalty?: number; // penalty per duplicate source_kind (0..1)
520
+ }
521
+ ```
522
+
523
+ ---
524
+
525
+ ## Provenance
526
+
527
+ ### getSupportTree(state, itemId)
528
+
529
+ Recursively walks `parents` to build a full provenance tree. Handles cycles and missing parents.
530
+
531
+ ```ts
532
+ interface SupportNode {
533
+ item: MemoryItem;
534
+ parents: SupportNode[];
535
+ }
536
+
537
+ const tree = getSupportTree(state, "m4");
538
+ // tree.item = m4
539
+ // tree.parents[0].item = m2
540
+ // tree.parents[0].parents[0].item = m1
541
+ ```
542
+
543
+ ### getSupportSet(state, itemId)
544
+
545
+ Flattened, deduplicated set of all items in the provenance chain (including the root item). The minimal set that justifies a claim.
546
+
547
+ ```ts
548
+ const support = getSupportSet(state, "m4");
549
+ // [m4, m2, m1] -- everything needed to explain why m4 exists
550
+ ```
551
+
552
+ ---
553
+
554
+ ## Bulk Operations
555
+
556
+ ### applyMany(state, filter, transform, author, reason?, options?)
557
+
558
+ Apply a transform function to all matching items in a single pass (one Map clone, not N). Return `Partial<MemoryItem>` to update, `null` to retract, or `{}` to skip.
559
+
560
+ ```ts
561
+ type ItemTransform = (item: MemoryItem) => Partial<MemoryItem> | null;
562
+ ```
563
+
564
+ ```ts
565
+ // decay authority by 10%
566
+ applyMany(state, {}, (item) => ({ authority: item.authority * 0.9 }), "system:decay");
567
+
568
+ // retract low-conviction items, boost the rest
569
+ applyMany(state, { meta: { agent_id: "agent:v1" } },
570
+ (item) => (item.conviction ?? 0) < 0.3 ? null : { authority: 1.0 },
571
+ "system:evaluator"
572
+ );
573
+
574
+ // tag top 50 by importance
575
+ applyMany(state, {}, () => ({ meta: { hot: true } }), "system:tagger",
576
+ undefined, { sort: { field: "importance", order: "desc" }, limit: 50 });
577
+ ```
578
+
579
+ Items retracted by a prior transform in the same batch are skipped (no crash).
580
+
581
+ ### bulkAdjustScores(state, criteria, delta, author, reason?, basis?)
582
+
583
+ Convenience wrapper around `applyMany` for delta-based score adjustments with clamping to [0, 1].
584
+
585
+ ```ts
586
+ interface ScoreAdjustment {
587
+ authority?: number; // delta, not absolute
588
+ conviction?: number;
589
+ importance?: number;
590
+ }
591
+
592
+ bulkAdjustScores(state, { scope: "project:old" }, { authority: -0.2 }, "system:decay");
593
+ ```
594
+
595
+ ### decayImportance(state, olderThanMs, factor, author, reason?)
596
+
597
+ Permanently decay stored importance on old items. Items created more than `olderThanMs` ago have their importance multiplied by `factor`. Skips items with zero or undefined importance.
598
+
599
+ ```ts
600
+ // halve importance on items older than 7 days
601
+ decayImportance(state, 7 * 24 * 60 * 60 * 1000, 0.5, "system:decay");
602
+ ```
603
+
604
+ Note: for query-time decay without mutating stored values, use `ScoreWeights.decay` instead.
605
+
606
+ ---
607
+
608
+ ## Graph Integrity
609
+
610
+ ### Conflict Detection & Resolution
611
+
612
+ ```ts
613
+ // mark two items as contradicting
614
+ markContradiction(state, itemIdA, itemIdB, author, meta?)
615
+
616
+ // find all active contradictions
617
+ getContradictions(state) -> Contradiction[]
618
+
619
+ // resolve: winner supersedes loser, loser authority lowered
620
+ resolveContradiction(state, winnerId, loserId, author, reason?)
621
+ ```
622
+
623
+ ### Staleness & Cascade
624
+
625
+ ```ts
626
+ // find items whose parents are missing (retracted)
627
+ getStaleItems(state) -> StaleItem[]
628
+
629
+ // get direct or transitive dependents
630
+ getDependents(state, itemId, transitive?) -> MemoryItem[]
631
+
632
+ // retract an item and all its transitive dependents
633
+ cascadeRetract(state, itemId, author, reason?)
634
+ -> { state, events, retracted: string[] }
635
+ ```
636
+
637
+ ### Identity / Aliasing
638
+
639
+ ```ts
640
+ // mark two items as referring to the same entity (bidirectional)
641
+ markAlias(state, itemIdA, itemIdB, author, meta?)
642
+
643
+ // direct aliases
644
+ getAliases(state, itemId) -> MemoryItem[]
645
+
646
+ // transitive closure (full identity group)
647
+ getAliasGroup(state, itemId) -> MemoryItem[]
648
+ ```
649
+
650
+ ---
651
+
652
+ ## Event Envelope Utilities
653
+
654
+ ### wrapLifecycleEvent(event, causeId, traceId?)
655
+
656
+ Wraps a `MemoryLifecycleEvent` in an `EventEnvelope` with generated id, timestamp, and `cause_id`.
657
+
658
+ ### wrapStateEvent(item, causeId, traceId?)
659
+
660
+ Creates a `state.memory` envelope.
661
+
662
+ ### wrapEdgeStateEvent(edge, causeId, traceId?)
663
+
664
+ Creates a `state.edge` envelope.
665
+
666
+ ---
667
+
668
+ ## Replay
669
+
670
+ ### replayCommands(commands)
671
+
672
+ Folds an array of `MemoryCommand` from an empty state. Returns final state and all lifecycle events.
673
+
674
+ ### replayFromEnvelopes(envelopes)
675
+
676
+ Sorts `EventEnvelope<MemoryCommand>[]` by timestamp, extracts payloads, replays.
677
+
678
+ ---
679
+
680
+ ## Serialization
681
+
682
+ `GraphState` uses `Map` internally, which doesn't serialize with `JSON.stringify`. These helpers handle conversion.
683
+
684
+ ### toJSON(state) / fromJSON(data)
685
+
686
+ Convert between `GraphState` and a plain serializable object.
687
+
688
+ ```ts
689
+ interface SerializedGraphState {
690
+ items: [string, MemoryItem][];
691
+ edges: [string, Edge][];
692
+ }
693
+
694
+ const data = toJSON(state); // GraphState -> plain object
695
+ const restored = fromJSON(data); // plain object -> GraphState
696
+ ```
697
+
698
+ ### stringify(state, pretty?) / parse(json)
699
+
700
+ Full JSON string round-trip.
701
+
702
+ ```ts
703
+ // save to disk / send over wire
704
+ const json = stringify(state); // compact
705
+ const json = stringify(state, true); // pretty-printed
706
+
707
+ // restore
708
+ const state = parse(json);
709
+ ```
710
+
711
+ All fields are preserved through serialization, including `meta`, `content`, scores, and `parents`.
712
+
713
+ ---
714
+
715
+ ## Stats
716
+
717
+ ### getStats(state)
718
+
719
+ Returns aggregate counts for items and edges.
720
+
721
+ ```ts
722
+ interface GraphStats {
723
+ items: {
724
+ total: number;
725
+ by_kind: Record<string, number>;
726
+ by_source_kind: Record<string, number>;
727
+ by_author: Record<string, number>;
728
+ by_scope: Record<string, number>;
729
+ with_parents: number;
730
+ root: number;
731
+ };
732
+ edges: {
733
+ total: number;
734
+ active: number;
735
+ by_kind: Record<string, number>;
736
+ };
737
+ }
738
+
739
+ const stats = getStats(state);
740
+ // stats.items.total -> 150
741
+ // stats.items.by_kind -> { observation: 80, hypothesis: 30, ... }
742
+ // stats.items.root -> 100
743
+ // stats.edges.active -> 45
744
+ // stats.edges.by_kind -> { SUPPORTS: 20, CONTRADICTS: 5, ... }
745
+ ```
746
+
747
+ ---
748
+
749
+ ## Intent Graph
750
+
751
+ Intents represent goals or objectives. They link to memory items via `root_memory_ids` and are the parent of tasks.
752
+
753
+ ### Types
754
+
755
+ ```ts
756
+ type IntentStatus = "active" | "paused" | "completed" | "cancelled";
757
+
758
+ interface Intent {
759
+ id: string;
760
+ label: string;
761
+ description?: string;
762
+ priority: number; // 0..1
763
+ owner: string; // "user:laz", "agent:reasoner"
764
+ status: IntentStatus;
765
+ context?: Record<string, unknown>;
766
+ root_memory_ids?: string[]; // anchors into the memory graph
767
+ meta?: Record<string, unknown>;
768
+ }
769
+
770
+ interface IntentState {
771
+ intents: Map<string, Intent>;
772
+ }
773
+ ```
774
+
775
+ ### Factory & State
776
+
777
+ ```ts
778
+ const state = createIntentState();
779
+ const intent = createIntent({ label: "find_kati", priority: 0.9, owner: "user:laz" });
780
+ // -> id generated, status defaults to "active"
781
+ ```
782
+
783
+ ### Commands & Reducer
784
+
785
+ ```ts
786
+ const { state, events } = applyIntentCommand(state, { type: "intent.create", intent });
787
+ ```
788
+
789
+ | Command | Valid from | Target status | Event |
790
+ |---------|-----------|---------------|-------|
791
+ | `intent.create` | — | — | `intent.created` |
792
+ | `intent.update` | any | — | `intent.updated` |
793
+ | `intent.pause` | active | paused | `intent.paused` |
794
+ | `intent.resume` | paused | active | `intent.resumed` |
795
+ | `intent.complete` | active, paused | completed | `intent.completed` |
796
+ | `intent.cancel` | active, paused | cancelled | `intent.cancelled` |
797
+
798
+ Invalid transitions throw `InvalidIntentTransitionError`.
799
+
800
+ All lifecycle events have `namespace: "intent"`.
801
+
802
+ ### Query
803
+
804
+ ```ts
805
+ interface IntentFilter {
806
+ owner?: string;
807
+ status?: IntentStatus;
808
+ statuses?: IntentStatus[];
809
+ min_priority?: number;
810
+ has_memory_id?: string; // intent references this memory item
811
+ }
812
+
813
+ getIntents(state, { owner: "user:laz", statuses: ["active", "paused"] });
814
+ getIntentById(state, "i1");
815
+ ```
816
+
817
+ ---
818
+
819
+ ## Task Graph
820
+
821
+ Tasks are units of work tied to an intent. They track execution status, agent assignment, retry attempts, and link to memory items consumed and produced.
822
+
823
+ ### Types
824
+
825
+ ```ts
826
+ type TaskStatus = "pending" | "running" | "completed" | "failed" | "cancelled";
827
+
828
+ interface Task {
829
+ id: string;
830
+ intent_id: string; // parent intent
831
+ action: string; // "search_linkedin", "summarize_case"
832
+ label?: string;
833
+ status: TaskStatus;
834
+ priority: number; // 0..1
835
+ context?: Record<string, unknown>;
836
+ result?: Record<string, unknown>;
837
+ error?: string;
838
+ input_memory_ids?: string[]; // memory items consumed
839
+ output_memory_ids?: string[]; // memory items produced
840
+ agent_id?: string;
841
+ attempt?: number; // incremented on retry
842
+ meta?: Record<string, unknown>;
843
+ }
844
+
845
+ interface TaskState {
846
+ tasks: Map<string, Task>;
847
+ }
848
+ ```
849
+
850
+ ### Factory & State
851
+
852
+ ```ts
853
+ const state = createTaskState();
854
+ const task = createTask({ intent_id: "i1", action: "search_linkedin", priority: 0.8 });
855
+ // -> id generated, status defaults to "pending", attempt defaults to 0
856
+ ```
857
+
858
+ ### Commands & Reducer
859
+
860
+ ```ts
861
+ const { state, events } = applyTaskCommand(state, { type: "task.create", task });
862
+ ```
863
+
864
+ | Command | Valid from | Target status | Event |
865
+ |---------|-----------|---------------|-------|
866
+ | `task.create` | — | — | `task.created` |
867
+ | `task.update` | any | — | `task.updated` |
868
+ | `task.start` | pending, failed | running | `task.started` |
869
+ | `task.complete` | running | completed | `task.completed` |
870
+ | `task.fail` | running | failed | `task.failed` |
871
+ | `task.cancel` | pending, running, failed | cancelled | `task.cancelled` |
872
+
873
+ `task.start` increments `attempt` and optionally sets `agent_id`. `task.fail` → `task.start` is a retry. Invalid transitions throw `InvalidTaskTransitionError`.
874
+
875
+ All lifecycle events have `namespace: "task"`.
876
+
877
+ ### Query
878
+
879
+ ```ts
880
+ interface TaskFilter {
881
+ intent_id?: string;
882
+ action?: string;
883
+ status?: TaskStatus;
884
+ statuses?: TaskStatus[];
885
+ agent_id?: string;
886
+ min_priority?: number;
887
+ has_input_memory_id?: string;
888
+ has_output_memory_id?: string;
889
+ }
890
+
891
+ getTasks(state, { intent_id: "i1", statuses: ["pending", "running"] });
892
+ getTaskById(state, "t1");
893
+ getTasksByIntent(state, "i1");
894
+ ```
895
+
896
+ ---
897
+
898
+ ## Cross-Graph Linking
899
+
900
+ The three graphs (memory, intent, task) reference each other by ID:
901
+
902
+ | From | To | Field |
903
+ |------|----|-------|
904
+ | Intent | Memory | `Intent.root_memory_ids` |
905
+ | Task | Intent | `Task.intent_id` |
906
+ | Task | Memory (input) | `Task.input_memory_ids` |
907
+ | Task | Memory (output) | `Task.output_memory_ids` |
908
+ | Memory | Intent | `MemoryItem.meta.creation_intent_id` |
909
+ | Memory | Task | `MemoryItem.meta.creation_task_id` |
910
+
911
+ No unified query across graphs — each graph has its own getters. The app layer composes them.
912
+
913
+ ---
914
+
915
+ ## Multi-Agent Memory Segmentation
916
+
917
+ MemEX supports multi-agent systems with one shared graph segmented by conventions on `author`, `meta`, and `scope`.
918
+
919
+ ### Memory segmentation fields
920
+
921
+ | Field | Convention | Example |
922
+ |-------|-----------|---------|
923
+ | `author` | Who created the item | `"agent:researcher"`, `"user:laz"` |
924
+ | `meta.agent_id` | Specific agent instance | `"agent:researcher-v2"` |
925
+ | `meta.session_id` | Session scope | `"session-abc"` |
926
+ | `meta.crew_id` | Crew/run scope | `"crew:investigation-42"` |
927
+ | `scope` | Logical namespace | `"project:cyberdeck/research"` |
928
+
929
+ ### Querying by agent
930
+
931
+ ```ts
932
+ // this agent's items only
933
+ getItems(state, { meta: { agent_id: "agent:researcher" } });
934
+
935
+ // all items from a crew run
936
+ getItems(state, { meta: { crew_id: "crew:investigation-42" } });
937
+
938
+ // everything in a project, ranked
939
+ getScoredItems(state, weights, {
940
+ pre: { scope_prefix: "project:cyberdeck/" },
941
+ });
942
+
943
+ // items NOT by a specific agent
944
+ getItems(state, { not: { meta: { agent_id: "agent:bad" } } });
945
+ ```
946
+
947
+ ### Task assignment
948
+
949
+ ```ts
950
+ // assign a task to a specific agent
951
+ applyTaskCommand(state, {
952
+ type: "task.create",
953
+ task: createTask({
954
+ intent_id: "i1",
955
+ action: "search_linkedin",
956
+ priority: 0.8,
957
+ agent_id: "agent:researcher", // assigned agent
958
+ input_memory_ids: ["m1", "m2"],
959
+ }),
960
+ });
961
+
962
+ // query tasks by agent
963
+ getTasks(state, { agent_id: "agent:researcher", status: "pending" });
964
+ ```
965
+
966
+ ### Hard isolation via transplant
967
+
968
+ For sub-agents that need to work independently:
969
+
970
+ ```ts
971
+ // export a slice
972
+ const slice = exportSlice(mem, intents, tasks, {
973
+ memory_ids: relevantIds,
974
+ include_parents: true,
975
+ });
976
+
977
+ // sub-agent works on its own copy...
978
+ // merge back (append-only, existing items untouched)
979
+ const { memState, report } = importSlice(mem, intents, tasks, subAgentSlice);
980
+ ```
981
+
982
+ ---
983
+
984
+ ## Transplant (Export / Import)
985
+
986
+ Move chains of memories, intents, and tasks between graph instances. Useful for sub-agent isolation, migration, cloning workflows, and backup.
987
+
988
+ ### exportSlice(memState, intentState, taskState, options)
989
+
990
+ Walk the graph from anchor ids and collect a self-contained slice.
991
+
992
+ ```ts
993
+ interface ExportOptions {
994
+ memory_ids?: string[];
995
+ intent_ids?: string[];
996
+ task_ids?: string[];
997
+ include_parents?: boolean; // walk parents up-graph
998
+ include_children?: boolean; // walk dependents down-graph
999
+ include_aliases?: boolean; // include ALIAS groups
1000
+ include_related_tasks?: boolean;
1001
+ include_related_intents?: boolean;
1002
+ }
1003
+
1004
+ interface MemexExport {
1005
+ memories: MemoryItem[];
1006
+ edges: Edge[];
1007
+ intents: Intent[];
1008
+ tasks: Task[];
1009
+ }
1010
+ ```
1011
+
1012
+ ```ts
1013
+ // export a full chain: m1 + all children + related intents/tasks
1014
+ const slice = exportSlice(memState, intentState, taskState, {
1015
+ memory_ids: ["m1"],
1016
+ include_children: true,
1017
+ include_related_intents: true,
1018
+ include_related_tasks: true,
1019
+ });
1020
+
1021
+ // slice is plain JSON — serialize and send anywhere
1022
+ const json = JSON.stringify(slice);
1023
+ ```
1024
+
1025
+ ### importSlice(memState, intentState, taskState, slice, options?)
1026
+
1027
+ Import a slice into existing state. Default: skip existing ids, never overwrite.
1028
+
1029
+ ```ts
1030
+ interface ImportOptions {
1031
+ skipExistingIds?: boolean; // default true
1032
+ shallowCompareExisting?: boolean; // default false — detect conflicts
1033
+ reIdOnDifference?: boolean; // default false — mint new ids on conflict
1034
+ }
1035
+
1036
+ interface ImportReport {
1037
+ created: { memories: string[]; intents: string[]; tasks: string[]; edges: string[] };
1038
+ skipped: { memories: string[]; intents: string[]; tasks: string[]; edges: string[] };
1039
+ conflicts: { memories: string[]; intents: string[]; tasks: string[]; edges: string[] };
1040
+ }
1041
+ ```
1042
+
1043
+ ```ts
1044
+ // default: append new, skip existing
1045
+ const { memState, intentState, taskState, report } = importSlice(
1046
+ currentMem, currentIntents, currentTasks,
1047
+ slice,
1048
+ );
1049
+ // report.created.memories -> ["m2", "m3"]
1050
+ // report.skipped.memories -> ["m1"] (already existed)
1051
+
1052
+ // with conflict detection
1053
+ const result = importSlice(mem, intents, tasks, slice, {
1054
+ shallowCompareExisting: true,
1055
+ });
1056
+ // result.report.conflicts.memories -> ["m1"] (exists but different)
1057
+
1058
+ // with re-id on conflict (mint new ids for differing entities)
1059
+ const result2 = importSlice(mem, intents, tasks, slice, {
1060
+ shallowCompareExisting: true,
1061
+ reIdOnDifference: true,
1062
+ });
1063
+ // conflicting entities get new uuidv7 ids, internal refs are rewritten
1064
+ ```
1065
+
1066
+ **Import behavior:**
1067
+
1068
+ | Scenario | `skipExisting` | `shallowCompare` | `reId` | Result |
1069
+ |----------|---------------|-----------------|--------|--------|
1070
+ | ID doesn't exist | — | — | — | Created |
1071
+ | ID exists, no compare | true | false | — | Skipped |
1072
+ | ID exists, same content | true | true | — | Skipped |
1073
+ | ID exists, different content | true | true | false | Conflict (reported, not imported) |
1074
+ | ID exists, different content | true | true | true | New id minted, imported as separate entity |
1075
+
1076
+ When `reIdOnDifference` is true, all internal references (`parents`, `Edge.from/to`, `intent_id`, `input/output_memory_ids`, `root_memory_ids`) are rewritten to the new ids. The original entity is not touched or linked.
1077
+
1078
+ **Re-id timestamp preservation:** new ids are generated at +1ms from the original entity's timestamp (extracted from the uuidv7), not from `Date.now()`. This preserves temporal ordering — decay scoring and recency sort are unaffected. If the +1ms id also collides, it increments by another 1ms until a free slot is found.