memex-python 0.13.0__tar.gz → 0.14.0__tar.gz

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 (67) hide show
  1. memex_python-0.14.0/API.md +923 -0
  2. memex_python-0.14.0/PKG-INFO +686 -0
  3. memex_python-0.14.0/README.md +667 -0
  4. memex_python-0.14.0/benchmarks/perf.py +234 -0
  5. {memex_python-0.13.0 → memex_python-0.14.0}/pyproject.toml +1 -1
  6. {memex_python-0.13.0 → memex_python-0.14.0}/src/memex/integrity.py +70 -29
  7. {memex_python-0.13.0 → memex_python-0.14.0}/src/memex/query.py +31 -11
  8. {memex_python-0.13.0 → memex_python-0.14.0}/src/memex/transplant.py +14 -6
  9. memex_python-0.14.0/tests/test_perf_equivalence.py +307 -0
  10. memex_python-0.13.0/PKG-INFO +0 -150
  11. memex_python-0.13.0/README.md +0 -131
  12. {memex_python-0.13.0 → memex_python-0.14.0}/.github/workflows/release.yml +0 -0
  13. {memex_python-0.13.0 → memex_python-0.14.0}/.github/workflows/test.yml +0 -0
  14. {memex_python-0.13.0 → memex_python-0.14.0}/.gitignore +0 -0
  15. {memex_python-0.13.0 → memex_python-0.14.0}/LICENSE +0 -0
  16. {memex_python-0.13.0 → memex_python-0.14.0}/PLAN.md +0 -0
  17. {memex_python-0.13.0 → memex_python-0.14.0}/renovate.json +0 -0
  18. {memex_python-0.13.0 → memex_python-0.14.0}/src/memex/__init__.py +0 -0
  19. {memex_python-0.13.0 → memex_python-0.14.0}/src/memex/_time.py +0 -0
  20. {memex_python-0.13.0 → memex_python-0.14.0}/src/memex/_uuid.py +0 -0
  21. {memex_python-0.13.0 → memex_python-0.14.0}/src/memex/bulk.py +0 -0
  22. {memex_python-0.13.0 → memex_python-0.14.0}/src/memex/commands.py +0 -0
  23. {memex_python-0.13.0 → memex_python-0.14.0}/src/memex/envelope.py +0 -0
  24. {memex_python-0.13.0 → memex_python-0.14.0}/src/memex/errors.py +0 -0
  25. {memex_python-0.13.0 → memex_python-0.14.0}/src/memex/factories.py +0 -0
  26. {memex_python-0.13.0 → memex_python-0.14.0}/src/memex/graph.py +0 -0
  27. {memex_python-0.13.0 → memex_python-0.14.0}/src/memex/intent.py +0 -0
  28. {memex_python-0.13.0 → memex_python-0.14.0}/src/memex/models.py +0 -0
  29. {memex_python-0.13.0 → memex_python-0.14.0}/src/memex/reducer.py +0 -0
  30. {memex_python-0.13.0 → memex_python-0.14.0}/src/memex/replay.py +0 -0
  31. {memex_python-0.13.0 → memex_python-0.14.0}/src/memex/retrieval.py +0 -0
  32. {memex_python-0.13.0 → memex_python-0.14.0}/src/memex/schemas.py +0 -0
  33. {memex_python-0.13.0 → memex_python-0.14.0}/src/memex/serialization.py +0 -0
  34. {memex_python-0.13.0 → memex_python-0.14.0}/src/memex/stats.py +0 -0
  35. {memex_python-0.13.0 → memex_python-0.14.0}/src/memex/store.py +0 -0
  36. {memex_python-0.13.0 → memex_python-0.14.0}/src/memex/task.py +0 -0
  37. {memex_python-0.13.0 → memex_python-0.14.0}/tests/support.py +0 -0
  38. {memex_python-0.13.0 → memex_python-0.14.0}/tests/test_bugfix_and_coverage.py +0 -0
  39. {memex_python-0.13.0 → memex_python-0.14.0}/tests/test_bugfix_bulk_retract_cascade.py +0 -0
  40. {memex_python-0.13.0 → memex_python-0.14.0}/tests/test_bugfix_holes.py +0 -0
  41. {memex_python-0.13.0 → memex_python-0.14.0}/tests/test_bugfix_regressions.py +0 -0
  42. {memex_python-0.13.0 → memex_python-0.14.0}/tests/test_bugfix_reid_ordering.py +0 -0
  43. {memex_python-0.13.0 → memex_python-0.14.0}/tests/test_bugfix_surface_contradictions_dedup.py +0 -0
  44. {memex_python-0.13.0 → memex_python-0.14.0}/tests/test_bugfix_sweep.py +0 -0
  45. {memex_python-0.13.0 → memex_python-0.14.0}/tests/test_bulk.py +0 -0
  46. {memex_python-0.13.0 → memex_python-0.14.0}/tests/test_cross_graph_fields.py +0 -0
  47. {memex_python-0.13.0 → memex_python-0.14.0}/tests/test_edge_cases.py +0 -0
  48. {memex_python-0.13.0 → memex_python-0.14.0}/tests/test_edge_cases_v2.py +0 -0
  49. {memex_python-0.13.0 → memex_python-0.14.0}/tests/test_envelope.py +0 -0
  50. {memex_python-0.13.0 → memex_python-0.14.0}/tests/test_graph.py +0 -0
  51. {memex_python-0.13.0 → memex_python-0.14.0}/tests/test_helpers.py +0 -0
  52. {memex_python-0.13.0 → memex_python-0.14.0}/tests/test_integrity.py +0 -0
  53. {memex_python-0.13.0 → memex_python-0.14.0}/tests/test_intent.py +0 -0
  54. {memex_python-0.13.0 → memex_python-0.14.0}/tests/test_phase1_foundation.py +0 -0
  55. {memex_python-0.13.0 → memex_python-0.14.0}/tests/test_query.py +0 -0
  56. {memex_python-0.13.0 → memex_python-0.14.0}/tests/test_query_advanced.py +0 -0
  57. {memex_python-0.13.0 → memex_python-0.14.0}/tests/test_reducer.py +0 -0
  58. {memex_python-0.13.0 → memex_python-0.14.0}/tests/test_replay.py +0 -0
  59. {memex_python-0.13.0 → memex_python-0.14.0}/tests/test_retrieval.py +0 -0
  60. {memex_python-0.13.0 → memex_python-0.14.0}/tests/test_serialization.py +0 -0
  61. {memex_python-0.13.0 → memex_python-0.14.0}/tests/test_setup.py +0 -0
  62. {memex_python-0.13.0 → memex_python-0.14.0}/tests/test_stats.py +0 -0
  63. {memex_python-0.13.0 → memex_python-0.14.0}/tests/test_store.py +0 -0
  64. {memex_python-0.13.0 → memex_python-0.14.0}/tests/test_task.py +0 -0
  65. {memex_python-0.13.0 → memex_python-0.14.0}/tests/test_transplant.py +0 -0
  66. {memex_python-0.13.0 → memex_python-0.14.0}/tests/test_types.py +0 -0
  67. {memex_python-0.13.0 → memex_python-0.14.0}/uv.lock +0 -0
@@ -0,0 +1,923 @@
1
+ # memex-python — API reference
2
+
3
+ Complete reference for the public surface re-exported from the top-level `memex`
4
+ package. Everything below is importable directly:
5
+
6
+ ```python
7
+ from memex import create_graph_state, apply_command, get_scored_items, MemexStore
8
+ ```
9
+
10
+ This document covers the **functional core** (pure `state -> (new_state, events)`
11
+ functions), the typed **Pydantic models**, and the optional **`MemexStore`**
12
+ facade. For the conceptual overview see [`README.md`](README.md); for design
13
+ rationale see [`PLAN.md`](PLAN.md).
14
+
15
+ ## Contents
16
+
17
+ - [Conventions](#conventions)
18
+ - [Core concepts](#core-concepts)
19
+ - [Graph state](#graph-state)
20
+ - [Entities & factories](#entities--factories)
21
+ - [Commands & the reducer](#commands--the-reducer)
22
+ - [Querying & filtering](#querying--filtering)
23
+ - [Scoring, decay & sorting](#scoring-decay--sorting)
24
+ - [Retrieval](#retrieval)
25
+ - [Integrity: contradictions, aliases, stale, cascade](#integrity)
26
+ - [Bulk operations](#bulk-operations)
27
+ - [Intent graph](#intent-graph)
28
+ - [Task graph](#task-graph)
29
+ - [Statistics](#statistics)
30
+ - [Replay](#replay)
31
+ - [Serialization](#serialization)
32
+ - [Event envelopes](#event-envelopes)
33
+ - [Transplant: export / import](#transplant)
34
+ - [Validation](#validation)
35
+ - [`MemexStore` facade](#memexstore-facade)
36
+ - [UUID helpers](#uuid-helpers)
37
+ - [Errors](#errors)
38
+ - [Type aliases](#type-aliases)
39
+
40
+ ---
41
+
42
+ ## Conventions
43
+
44
+ **Pure & immutable.** Every mutation has the shape
45
+ `f(state, ...) -> (new_state, events)`. The input `state` is never modified; a
46
+ fresh `GraphState` is returned with the relevant dict cloned. Entities
47
+ (`MemoryItem`, `Edge`, `Intent`, `Task`) are **frozen** Pydantic models — an
48
+ "edit" produces a new instance via `model_copy(update=...)`.
49
+
50
+ **Dicts or models.** Public functions that take a filter, options, or weights
51
+ accept either a plain `dict` or the typed model; dicts are validated through the
52
+ model. The examples mix both styles freely.
53
+
54
+ **Validation is always on.** Constructing any model with an out-of-range score
55
+ (`authority`/`conviction`/`importance`/`weight`/`priority` must be `0..1`) raises
56
+ `pydantic.ValidationError`. Use `Model.model_construct(...)` to deliberately
57
+ bypass validation (e.g. test fixtures).
58
+
59
+ **Keyword-only constructors.** All `create_*` factories take keyword arguments
60
+ only.
61
+
62
+ **Reserved-word fields.** `from`, `not`, and `or` are Python keywords, so the
63
+ attributes are `from_`, `not_`, and `or_`; each serializes to/accepts its bare
64
+ JSON alias (`"from"`, `"not"`, `"or"`).
65
+
66
+ **Wire compatibility.** Command tags (`"memory.create"`), enum values
67
+ (`"derived_deterministic"`, `"DERIVED_FROM"`), and JSON keys are byte-identical
68
+ to the TypeScript `@ai2070/memex`, so a Python and a TS service can share one
69
+ event log.
70
+
71
+ ---
72
+
73
+ ## Core concepts
74
+
75
+ Three independent graphs, each driven by the same `commands -> reducer ->
76
+ lifecycle events` pattern:
77
+
78
+ | Graph | State | Core type | Reducer | Namespace |
79
+ |--------|--------------|--------------|--------------------------|------------|
80
+ | Memory | `GraphState` | `MemoryItem` | `apply_command` | `"memory"` |
81
+ | Intent | `IntentState`| `Intent` | `apply_intent_command` | `"intent"` |
82
+ | Task | `TaskState` | `Task` | `apply_task_command` | `"task"` |
83
+
84
+ Each `MemoryItem` carries three orthogonal `0..1` scores — **authority** (trust),
85
+ **conviction** (author confidence), **importance** (current salience) — plus
86
+ provenance (`parents`) and typed `edges`.
87
+
88
+ ```python
89
+ from memex import create_graph_state, create_memory_item, apply_command, get_scored_items
90
+
91
+ state = create_graph_state()
92
+ item = create_memory_item(
93
+ scope="user:laz/general", kind="observation",
94
+ content={"key": "login_count", "value": 42},
95
+ author="agent:monitor", source_kind="observed",
96
+ authority=0.9, importance=0.7,
97
+ )
98
+ state, events = apply_command(state, {"type": "memory.create", "item": item})
99
+ top = get_scored_items(state, {"authority": 1.0, "importance": 0.5})
100
+ ```
101
+
102
+ ---
103
+
104
+ ## Graph state
105
+
106
+ ### `class GraphState`
107
+
108
+ Frozen dataclass holding the memory graph. Not a Pydantic model (re-validating
109
+ every item per command would be unusably slow).
110
+
111
+ | Field | Type | Description |
112
+ |---------|-------------------------|------------------------|
113
+ | `items` | `dict[str, MemoryItem]` | id → item (insertion-ordered) |
114
+ | `edges` | `dict[str, Edge]` | id → edge (insertion-ordered) |
115
+
116
+ ### `create_graph_state() -> GraphState`
117
+
118
+ A new empty memory graph.
119
+
120
+ ### `clone_graph_state(state: GraphState) -> GraphState`
121
+
122
+ Shallow clone — new dicts, shared (immutable) item/edge instances.
123
+
124
+ ---
125
+
126
+ ## Entities & factories
127
+
128
+ ### `class MemoryItem`
129
+
130
+ Frozen. The core memory node.
131
+
132
+ | Field | Type | Notes |
133
+ |---------------|-----------------------|-----------------------------------------|
134
+ | `id` | `str` | usually a UUIDv7 |
135
+ | `scope` | `str` | namespacing key, e.g. `"user:laz/general"` |
136
+ | `kind` | `str` | open union; see [`KnownMemoryKind`](#type-aliases) |
137
+ | `content` | `dict[str, Any]` | arbitrary payload |
138
+ | `author` | `str` | |
139
+ | `source_kind` | `str` | open union; see [`KnownSourceKind`](#type-aliases) |
140
+ | `parents` | `list[str] \| None` | provenance ids |
141
+ | `authority` | `float` (0..1) | required |
142
+ | `conviction` | `float \| None` (0..1)| |
143
+ | `importance` | `float \| None` (0..1)| |
144
+ | `created_at` | `int \| None` | ms epoch; falls back to the UUIDv7 ts |
145
+ | `intent_id` | `str \| None` | cross-graph link |
146
+ | `task_id` | `str \| None` | cross-graph link |
147
+ | `meta` | `dict[str, Any] \| None` | |
148
+
149
+ ### `class Edge`
150
+
151
+ Frozen, `populate_by_name=True`.
152
+
153
+ | Field | Type | Notes |
154
+ |---------------|-----------------------|-----------------------------------------|
155
+ | `edge_id` | `str` | |
156
+ | `from_` | `str` | JSON alias `"from"` |
157
+ | `to` | `str` | |
158
+ | `kind` | `str` | see [`KnownEdgeKind`](#type-aliases) |
159
+ | `weight` | `float \| None` (0..1)| |
160
+ | `author` | `str` | |
161
+ | `source_kind` | `str` | |
162
+ | `authority` | `float` (0..1) | required |
163
+ | `active` | `bool` | |
164
+ | `meta` | `dict[str, Any] \| None` | |
165
+
166
+ ### `create_memory_item(...) -> MemoryItem`
167
+
168
+ ```python
169
+ create_memory_item(*, scope, kind, content, author, source_kind, authority,
170
+ id=None, parents=None, conviction=None, importance=None,
171
+ created_at=None, intent_id=None, task_id=None, meta=None)
172
+ ```
173
+
174
+ Mints `id` (UUIDv7) when omitted. `created_at` is the explicit value, else the
175
+ id's UUIDv7 timestamp, else the wall clock. Validates score bounds.
176
+
177
+ ### `create_edge(...) -> Edge`
178
+
179
+ ```python
180
+ create_edge(*, from_, to, kind, author, source_kind, authority,
181
+ edge_id=None, active=None, weight=None, meta=None)
182
+ ```
183
+
184
+ `active` defaults to `True`, `edge_id` to a fresh UUIDv7.
185
+
186
+ ---
187
+
188
+ ## Commands & the reducer
189
+
190
+ Commands are a Pydantic discriminated union keyed on `type`. `apply_command`
191
+ accepts a command **model** or a plain **dict** (validated via
192
+ `MemoryCommandAdapter`).
193
+
194
+ ### Memory commands
195
+
196
+ | Model | `type` | Fields (besides `type`) |
197
+ |-----------------|--------------------|-----------------------------------------------|
198
+ | `MemoryCreate` | `"memory.create"` | `item: MemoryItem` |
199
+ | `MemoryUpdate` | `"memory.update"` | `item_id`, `partial: dict`, `author`, `reason?`, `basis?` |
200
+ | `MemoryRetract` | `"memory.retract"` | `item_id`, `author`, `reason?` |
201
+ | `EdgeCreate` | `"edge.create"` | `edge: Edge` |
202
+ | `EdgeUpdate` | `"edge.update"` | `edge_id`, `partial: dict`, `author`, `reason?` |
203
+ | `EdgeRetract` | `"edge.retract"` | `edge_id`, `author`, `reason?` |
204
+
205
+ - `MemoryCommand` — the `Annotated[Union[...], discriminator="type"]` alias.
206
+ - `MemoryCommandAdapter` — `TypeAdapter[MemoryCommand]` for validating raw dicts.
207
+
208
+ ### `apply_command(state, cmd) -> CommandResult`
209
+
210
+ `cmd: MemoryCommand | dict`. Returns a `CommandResult` NamedTuple
211
+ `(state: GraphState, events: list[MemoryLifecycleEvent])`. The result is
212
+ iterable, so `state, events = apply_command(...)` works.
213
+
214
+ Behavior per command:
215
+
216
+ - **create** — raises `DuplicateMemoryError` / `DuplicateEdgeError` on id clash.
217
+ - **update** — shallow-merges `partial` (for `content`/`meta`, merges keys; keys
218
+ cannot be deleted via update); `id`/`created_at` are immutable for items,
219
+ `edge_id`/`from`/`to` for edges. Raises `MemoryNotFoundError` /
220
+ `EdgeNotFoundError` if absent. Updates do **not** re-validate scores.
221
+ - **retract (memory)** — removes the item *and* every incident edge, emitting one
222
+ `edge.retracted` per removed edge.
223
+
224
+ Emits [`MemoryLifecycleEvent`](#class-memorylifecycleevent)s.
225
+
226
+ ### `merge_item(existing: MemoryItem, partial: dict) -> MemoryItem`
227
+ ### `merge_edge(existing: Edge, partial: dict) -> Edge`
228
+
229
+ The merge primitives used by the reducer (no validation, mirroring the TS
230
+ "updates don't validate" guarantee). Exposed for advanced/bulk callers.
231
+
232
+ ### `class MemoryLifecycleEvent`
233
+
234
+ | Field | Type | Notes |
235
+ |--------------|-------------------------------|-------------------------------|
236
+ | `namespace` | `Literal["memory"]` | always `"memory"` |
237
+ | `type` | [`LifecycleEventType`](#type-aliases) | e.g. `"memory.created"` |
238
+ | `item` | `MemoryItem \| None` | set for memory events |
239
+ | `edge` | `Edge \| None` | set for edge events |
240
+ | `cause_type` | `str \| None` | the command tag that caused it|
241
+
242
+ ---
243
+
244
+ ## Querying & filtering
245
+
246
+ ### `get_items(state, filter=None, options=None) -> list[MemoryItem]`
247
+
248
+ Filter, then sort, then page. `filter: MemoryFilter | dict | None`,
249
+ `options: QueryOptions | dict | None`. With no filter, returns all items in
250
+ insertion order.
251
+
252
+ ### `get_item_by_id(state, id) -> MemoryItem | None`
253
+
254
+ ### `matches_filter(item, f: MemoryFilter) -> bool`
255
+
256
+ The predicate behind `get_items` — useful for ad-hoc filtering.
257
+
258
+ ### `class MemoryFilter`
259
+
260
+ All present conditions are AND-combined (except `or_`). `populate_by_name=True`.
261
+
262
+ | Field | Type | Matches when… |
263
+ |----------------|----------------------------|--------------------------------------------------|
264
+ | `ids` | `list[str]` | `item.id` is in the list |
265
+ | `scope` | `str` | exact scope |
266
+ | `scope_prefix` | `str` | `item.scope.startswith(...)` |
267
+ | `author` | `str` | exact |
268
+ | `kind` | `str` | exact |
269
+ | `source_kind` | `str` | exact |
270
+ | `range` | `ScoreRanges` | each score within its `Range` |
271
+ | `intent_id` | `str` | exact |
272
+ | `intent_ids` | `list[str]` | `item.intent_id` in the list |
273
+ | `task_id` | `str` | exact |
274
+ | `task_ids` | `list[str]` | `item.task_id` in the list |
275
+ | `has_parent` | `str` | id is in `item.parents` |
276
+ | `is_root` | `bool` | `True`: no parents; `False`: has parents |
277
+ | `parents` | `ParentsFilter` | see below |
278
+ | `decay` | `DecayFilter` | decay multiplier ≥ `min` |
279
+ | `created` | `CreatedFilter` | timestamp window |
280
+ | `not_` | `MemoryFilter` (alias `not`) | sub-filter does **not** match |
281
+ | `meta` | `dict[str, Any]` | each dotted path equals the value |
282
+ | `meta_has` | `list[str]` | each dotted path exists |
283
+ | `or_` | `list[MemoryFilter]` (alias `or`) | at least one sub-filter matches |
284
+
285
+ Supporting models:
286
+
287
+ - **`Range`** — `min: float | None`, `max: float | None` (inclusive bounds).
288
+ - **`ScoreRanges`** — `authority`, `conviction`, `importance`, each a `Range`.
289
+ - **`ParentsFilter`** — `includes: str`, `includes_any: list[str]`,
290
+ `includes_all: list[str]`, `count: Range` (range over the number of parents).
291
+ - **`DecayFilter`** — `config: DecayConfig`, `min: float` (0..1).
292
+ - **`CreatedFilter`** — `before: int | None` (exclusive upper, `ts < before`),
293
+ `after: int | None` (inclusive lower, `ts >= after`); both ms epoch.
294
+
295
+ `meta`/`meta_has` paths are dotted (`"a.b.c"`) and walk nested dicts.
296
+
297
+ ### `class QueryOptions`
298
+
299
+ | Field | Type | Notes |
300
+ |----------|-----------------------------------|--------------------------------|
301
+ | `sort` | `SortOption \| list[SortOption] \| None` | multi-key, applied in order |
302
+ | `limit` | `int \| None` (≥0) | applied after sort |
303
+ | `offset` | `int \| None` (≥0) | applied after sort |
304
+
305
+ ### `class SortOption`
306
+
307
+ `field: "authority" | "conviction" | "importance" | "recency"`,
308
+ `order: "asc" | "desc"`. `recency` sorts by item timestamp. Ties preserve
309
+ insertion order (stable). An unknown field raises `ValueError`.
310
+
311
+ ### Edges
312
+
313
+ - **`get_edges(state, filter=None) -> list[Edge]`** — `filter: EdgeFilter | dict | None`.
314
+ When no filter is given, only **active** edges are returned.
315
+ - **`get_edge_by_id(state, edge_id) -> Edge | None`**
316
+
317
+ #### `class EdgeFilter`
318
+
319
+ | Field | Type | Notes |
320
+ |--------------|-----------------|--------------------------------------|
321
+ | `from_` | `str` (alias `from`) | exact source |
322
+ | `to` | `str` | exact target |
323
+ | `kind` | `str` | exact |
324
+ | `min_weight` | `float` | `edge.weight >= min_weight` |
325
+ | `active_only`| `bool \| None` | default `True`; `False` includes retracted edges |
326
+
327
+ ### Navigation
328
+
329
+ - **`get_parents(state, item_id) -> list[MemoryItem]`** — resolves `item.parents`
330
+ to the items that exist.
331
+ - **`get_children(state, item_id) -> list[MemoryItem]`** — items listing `item_id`
332
+ in their `parents`.
333
+ - **`get_related_items(state, item_id, direction="both") -> list[MemoryItem]`** —
334
+ items connected by active edges. `direction: "from" | "to" | "both"`. Returns
335
+ a deduplicated, insertion-ordered list excluding the item itself.
336
+
337
+ ---
338
+
339
+ ## Scoring, decay & sorting
340
+
341
+ ### `class ScoreWeights`
342
+
343
+ Multipliers (intentionally unbounded — not `0..1`).
344
+
345
+ | Field | Type | Notes |
346
+ |--------------|-----------------------|--------------------------------|
347
+ | `authority` | `float \| None` | weight on `item.authority` |
348
+ | `conviction` | `float \| None` | weight on `item.conviction` |
349
+ | `importance` | `float \| None` | weight on `item.importance` |
350
+ | `decay` | `DecayConfig \| None` | if set, multiplies the score |
351
+
352
+ ### `class DecayConfig`
353
+
354
+ | Field | Type | Notes |
355
+ |------------|----------------|-----------------------------------------|
356
+ | `rate` | `float` (0..1) | per-interval decay rate |
357
+ | `interval` | `str` | `"hour"` \| `"day"` \| `"week"` |
358
+ | `type` | `str` | `"exponential"` \| `"linear"` \| `"step"` |
359
+
360
+ ### `compute_decay_multiplier(item, decay: DecayConfig) -> float`
361
+
362
+ `intervals = age_ms / interval_ms`, where `age_ms = now - item_timestamp`.
363
+
364
+ - **exponential** → `(1 - rate) ** intervals`
365
+ - **linear** → `max(0, 1 - rate * intervals)`
366
+ - **step** → `(1 - rate) ** floor(intervals)`
367
+
368
+ Future-dated items (age ≤ 0, clock skew) return `1.0`. Unknown `interval`/`type`
369
+ raises `ValueError`.
370
+
371
+ ### `compute_score(item, weights: ScoreWeights) -> float`
372
+
373
+ `authority*w.authority + conviction*w.conviction + importance*w.importance`,
374
+ then `* compute_decay_multiplier(...)` if `weights.decay` is set. Missing weights
375
+ and missing scores are treated as `0`.
376
+
377
+ ### `get_scored_items(state, weights, options=None) -> list[ScoredItem]`
378
+
379
+ `weights: ScoreWeights | dict`, `options: ScoredQueryOptions | dict | None`.
380
+ Pipeline: `pre`-filter → score → sort by score descending → `min_score` →
381
+ `post`-filter → `offset`/`limit`.
382
+
383
+ #### `class ScoredQueryOptions`
384
+
385
+ | Field | Type | Notes |
386
+ |-------------|----------------------|------------------------------------|
387
+ | `pre` | `MemoryFilter \| None` | filter applied before scoring |
388
+ | `post` | `MemoryFilter \| None` | filter applied after scoring |
389
+ | `min_score` | `float \| None` | drop items scoring below this |
390
+ | `limit` | `int \| None` | |
391
+ | `offset` | `int \| None` | |
392
+
393
+ #### `class ScoredItem`
394
+
395
+ `item: MemoryItem`, `score: float`, `contradicted_by: list[MemoryItem] | None`.
396
+ Not frozen — `surface_contradictions` annotates `contradicted_by` in place.
397
+
398
+ ### `extract_timestamp(uuid_id: str) -> int`
399
+
400
+ Extract the ms timestamp from a UUIDv7 id. Raises `InvalidTimestampError` on
401
+ anything that isn't a valid v7 UUID.
402
+
403
+ ---
404
+
405
+ ## Retrieval
406
+
407
+ ### Provenance walks
408
+
409
+ - **`get_support_tree(state, item_id) -> SupportNode | None`** — full provenance
410
+ tree, deduplicating on cycles. Returns `None` if the item is absent.
411
+ - **`get_support_set(state, item_id) -> list[MemoryItem]`** — flattened set of
412
+ items that justify a claim.
413
+ - **`class SupportNode`** — dataclass `item: MemoryItem`, `parents: list[SupportNode]`.
414
+
415
+ ### Contradiction policies
416
+
417
+ Both remove **superseded** items (targets of active `SUPERSEDES` edges).
418
+
419
+ - **`filter_contradictions(state, scored) -> list[ScoredItem]`** — drops the
420
+ lower-scoring side of each unresolved `CONTRADICTS` pair (deterministic
421
+ tie-breaks by score then `edge_id`).
422
+ - **`surface_contradictions(state, scored) -> list[ScoredItem]`** — keeps both
423
+ sides, annotating each via `contradicted_by`. Self-edges are ignored.
424
+
425
+ ### Diversity
426
+
427
+ - **`class DiversityOptions`** — `author_penalty`, `parent_penalty`,
428
+ `source_penalty` (each `float | None`).
429
+ - **`apply_diversity(scored, options) -> list[ScoredItem]`** — subtracts a
430
+ per-duplicate penalty (cumulative per repeated author / parent / source),
431
+ clamps at `0`, and re-sorts by score descending.
432
+
433
+ ### `smart_retrieve(...) -> list[ScoredItem]`
434
+
435
+ ```python
436
+ smart_retrieve(state, *, budget, cost_fn, weights,
437
+ filter=None, contradictions=None, diversity=None)
438
+ ```
439
+
440
+ Score → contradiction policy → diversity → greedy budget pack. `cost_fn:
441
+ Callable[[MemoryItem], float]` must return a finite, non-negative number
442
+ (otherwise `ValueError`). `contradictions: "filter" | "surface" | None`.
443
+ `diversity: DiversityOptions | dict | None`. Greedily appends items whose cost
444
+ fits the remaining `budget`.
445
+
446
+ ```python
447
+ packed = smart_retrieve(
448
+ state, budget=2000, cost_fn=lambda i: len(str(i.content)),
449
+ weights={"authority": 0.6, "importance": 0.4},
450
+ contradictions="surface", diversity={"author_penalty": 0.2},
451
+ )
452
+ ```
453
+
454
+ ### `get_items_by_budget(...) -> list[ScoredItem]`
455
+
456
+ ```python
457
+ get_items_by_budget(state, *, budget, cost_fn, weights, filter=None)
458
+ ```
459
+
460
+ The budget-pack step without contradiction/diversity passes.
461
+
462
+ ---
463
+
464
+ ## Integrity
465
+
466
+ Contradiction & alias management, stale detection, and cascade retraction.
467
+
468
+ ### Contradictions
469
+
470
+ - **`get_contradictions(state) -> list[Contradiction]`** — active `CONTRADICTS`
471
+ pairs whose endpoints both exist.
472
+ - **`mark_contradiction(state, item_id_a, item_id_b, author, meta=None) -> CommandResult`**
473
+ — creates a `CONTRADICTS` edge.
474
+ - **`resolve_contradiction(state, winner_id, loser_id, author, reason=None) -> CommandResult`**
475
+ — retracts the `CONTRADICTS` edge(s) between the pair, adds a `SUPERSEDES`
476
+ edge (winner → loser), and drops the loser's `authority` to 10%. A stale call
477
+ with no matching edge is a no-op.
478
+ - **`class Contradiction`** — `a: MemoryItem`, `b: MemoryItem`, `edge: Edge | None`.
479
+
480
+ ### Stale items & dependents
481
+
482
+ - **`get_stale_items(state) -> list[StaleItem]`** — items whose `parents`
483
+ reference ids no longer present.
484
+ - **`class StaleItem`** — `item: MemoryItem`, `missing_parents: list[str]`.
485
+ - **`get_dependents(state, item_id, transitive=False) -> list[MemoryItem]`** —
486
+ direct children, or the whole dependent subtree when `transitive=True`
487
+ (cycle-safe).
488
+
489
+ ### Cascade retraction
490
+
491
+ - **`cascade_retract(state, item_id, author, reason=None) -> CascadeResult`** —
492
+ retracts an item and all transitive dependents in post-order (leaves first),
493
+ cleaning incident edges. Cycle- and DAG-safe; iterative (no recursion limit).
494
+ - **`class CascadeResult`** — `state: GraphState`,
495
+ `events: list[MemoryLifecycleEvent]`, `retracted: list[str]` (ids in
496
+ retraction order).
497
+
498
+ ### Aliases (identity)
499
+
500
+ - **`mark_alias(state, item_id_a, item_id_b, author, meta=None) -> CommandResult`**
501
+ — creates bidirectional `ALIAS` edges. Self-alias is a no-op.
502
+ - **`get_aliases(state, item_id) -> list[MemoryItem]`** — direct alias targets.
503
+ - **`get_alias_group(state, item_id) -> list[MemoryItem]`** — the full connected
504
+ alias component (transitive closure), including the item itself.
505
+
506
+ ---
507
+
508
+ ## Bulk operations
509
+
510
+ Single-pass transforms that clone the state once instead of per command.
511
+
512
+ ### `apply_many(state, filter, transform, author, reason=None, options=None) -> CommandResult`
513
+
514
+ Applies `transform: Callable[[MemoryItem], dict | None]` to every item matching
515
+ `filter` (with optional `QueryOptions`). The transform returns:
516
+
517
+ - `None` → **retract** the item (and clean incident edges),
518
+ - an **empty dict** → skip,
519
+ - a **partial dict** → update.
520
+
521
+ ```python
522
+ ItemTransform = Callable[[MemoryItem], dict[str, Any] | None]
523
+ ```
524
+
525
+ ### `bulk_adjust_scores(state, criteria, delta, author, reason=None) -> CommandResult`
526
+
527
+ Adds a `ScoreAdjustment` to matching items, clamping each result to `0..1`.
528
+
529
+ - **`class ScoreAdjustment`** — `authority`, `conviction`, `importance` (each
530
+ `float | None`); only the provided deltas are applied.
531
+
532
+ ### `decay_importance(state, older_than_ms, factor, author, reason=None) -> CommandResult`
533
+
534
+ Multiplies `importance` by `factor` for items created before
535
+ `now - older_than_ms`. Items with zero/absent importance are skipped.
536
+
537
+ ---
538
+
539
+ ## Intent graph
540
+
541
+ Active goals with a status machine: `active ⇄ paused → completed / cancelled`.
542
+
543
+ ### `class Intent`
544
+
545
+ Frozen. `id`, `parent_id?`, `label`, `description?`, `priority` (0..1), `owner`,
546
+ `status: IntentStatus`, `context?`, `root_memory_ids?`, `meta?`.
547
+
548
+ - **`IntentStatus`** — `"active" | "paused" | "completed" | "cancelled"`.
549
+ - **`class IntentState`** — frozen dataclass `intents: dict[str, Intent]`.
550
+
551
+ ### State & factories
552
+
553
+ - **`create_intent_state() -> IntentState`**
554
+ - **`create_intent(*, label, priority, owner, id=None, parent_id=None,
555
+ description=None, status=None, context=None, root_memory_ids=None, meta=None)
556
+ -> Intent`** — `status` defaults to `"active"`.
557
+
558
+ ### `apply_intent_command(state, cmd) -> IntentResult`
559
+
560
+ `cmd: IntentCommand | dict`. `IntentResult` is `(state: IntentState,
561
+ events: list[IntentLifecycleEvent])`.
562
+
563
+ | `type` | Fields | Effect |
564
+ |---------------------|------------------------------------------|---------------------------------|
565
+ | `"intent.create"` | `intent: Intent` | add (dup → `DuplicateIntentError`) |
566
+ | `"intent.update"` | `intent_id`, `partial`, `author`, `reason?` | merge (`id`/`status` ignored) |
567
+ | `"intent.complete"` | `intent_id`, `author`, `reason?` | → `completed` (from active/paused) |
568
+ | `"intent.cancel"` | `intent_id`, `author`, `reason?` | → `cancelled` (from active/paused) |
569
+ | `"intent.pause"` | `intent_id`, `author`, `reason?` | → `paused` (from active) |
570
+ | `"intent.resume"` | `intent_id`, `author`, `reason?` | → `active` (from paused) |
571
+
572
+ Invalid transitions raise `InvalidIntentTransitionError`; missing id raises
573
+ `IntentNotFoundError`. `IntentCommand` is the discriminated-union alias.
574
+
575
+ ### Queries
576
+
577
+ - **`get_intents(state, filter=None) -> list[Intent]`** — `filter: IntentFilter | dict | None`.
578
+ - **`get_intent_by_id(state, id) -> Intent | None`**
579
+ - **`get_child_intents(state, parent_id) -> list[Intent]`**
580
+
581
+ #### `class IntentFilter`
582
+
583
+ `owner`, `status`, `statuses: list[IntentStatus]`, `min_priority`,
584
+ `has_memory_id` (in `root_memory_ids`), `parent_id`, `is_root` (bool).
585
+
586
+ ### Events
587
+
588
+ - **`class IntentLifecycleEvent`** — `namespace="intent"`, `type`
589
+ (`"intent.created"` …), `intent: Intent`, `cause_type: str`.
590
+
591
+ ---
592
+
593
+ ## Task graph
594
+
595
+ Units of work tied to intents: `pending → running → completed`, with
596
+ `running → failed → running` retry and `cancel` from any non-terminal state.
597
+
598
+ ### `class Task`
599
+
600
+ Frozen. `id`, `intent_id`, `parent_id?`, `action`, `label?`, `status:
601
+ TaskStatus`, `priority` (0..1), `context?`, `result?`, `error?`,
602
+ `input_memory_ids?`, `output_memory_ids?`, `agent_id?`, `attempt?`, `meta?`.
603
+
604
+ - **`TaskStatus`** — `"pending" | "running" | "completed" | "failed" | "cancelled"`.
605
+ - **`class TaskState`** — frozen dataclass `tasks: dict[str, Task]`.
606
+
607
+ ### State & factories
608
+
609
+ - **`create_task_state() -> TaskState`**
610
+ - **`create_task(*, intent_id, action, priority, id=None, parent_id=None,
611
+ label=None, status=None, context=None, result=None, error=None,
612
+ input_memory_ids=None, output_memory_ids=None, agent_id=None, attempt=None,
613
+ meta=None) -> Task`** — `status` defaults to `"pending"`, `attempt` to `0`.
614
+
615
+ ### `apply_task_command(state, cmd) -> TaskResult`
616
+
617
+ `cmd: TaskCommand | dict`. `TaskResult` is `(state: TaskState,
618
+ events: list[TaskLifecycleEvent])`.
619
+
620
+ | `type` | Fields | Effect |
621
+ |-------------------|------------------------------------------|-------------------------------------|
622
+ | `"task.create"` | `task: Task` | add (dup → `DuplicateTaskError`) |
623
+ | `"task.update"` | `task_id`, `partial`, `author` | merge (`id`/`status` ignored) |
624
+ | `"task.start"` | `task_id`, `agent_id?` | → `running` (from pending/failed), `attempt++` |
625
+ | `"task.complete"` | `task_id`, `result?`, `output_memory_ids?` | → `completed` (from running) |
626
+ | `"task.fail"` | `task_id`, `error`, `retryable?` | → `failed` (from running) |
627
+ | `"task.cancel"` | `task_id`, `reason?` | → `cancelled` (from non-terminal) |
628
+
629
+ Invalid transitions raise `InvalidTaskTransitionError`; missing id raises
630
+ `TaskNotFoundError`. `TaskCommand` is the discriminated-union alias.
631
+
632
+ ### Queries
633
+
634
+ - **`get_tasks(state, filter=None) -> list[Task]`** — `filter: TaskFilter | dict | None`.
635
+ - **`get_task_by_id(state, id) -> Task | None`**
636
+ - **`get_tasks_by_intent(state, intent_id) -> list[Task]`**
637
+ - **`get_child_tasks(state, parent_id) -> list[Task]`**
638
+
639
+ #### `class TaskFilter`
640
+
641
+ `intent_id`, `action`, `status`, `statuses: list[TaskStatus]`, `agent_id`,
642
+ `min_priority`, `has_input_memory_id`, `has_output_memory_id`, `parent_id`,
643
+ `is_root`.
644
+
645
+ ### Events
646
+
647
+ - **`class TaskLifecycleEvent`** — `namespace="task"`, `type` (`"task.created"`
648
+ …), `task: Task`, `cause_type: str`.
649
+
650
+ ---
651
+
652
+ ## Statistics
653
+
654
+ ### `get_stats(state) -> GraphStats`
655
+
656
+ Aggregate counts over a `GraphState`.
657
+
658
+ - **`class GraphStats`** — `items: ItemStats`, `edges: EdgeStats`.
659
+ - **`class ItemStats`** — `total`, `by_kind`, `by_source_kind`, `by_author`,
660
+ `by_scope` (each `dict[str, int]`), `with_parents: int`, `root: int`.
661
+ - **`class EdgeStats`** — `total: int`, `active: int`, `by_kind: dict[str, int]`.
662
+
663
+ ---
664
+
665
+ ## Replay
666
+
667
+ Rebuild a `GraphState` from an event/command log. Integrity-tolerant: per-item
668
+ failures are collected, not raised.
669
+
670
+ ### `replay_commands(commands: list) -> ReplayResult`
671
+
672
+ Fold a list of commands (models or dicts) in order.
673
+
674
+ ### `replay_from_envelopes(envelopes: list) -> ReplayResult`
675
+
676
+ Sort envelopes by their `ts` (strict ISO-8601, ms precision, explicit offset or
677
+ `Z`) and fold their payloads. Each envelope may be a dict or an `EventEnvelope`.
678
+
679
+ - **`class ReplayResult`** — `state: GraphState`,
680
+ `events: list[MemoryLifecycleEvent]`, `skipped: list[ReplayFailure]`.
681
+ - **`class ReplayFailure`** — dataclass `index: int`, `error: Exception`,
682
+ `command=None`, `envelope=None`.
683
+
684
+ ---
685
+
686
+ ## Serialization
687
+
688
+ On-disk shape matches the TS library —
689
+ `{"items": [[id, item], ...], "edges": [[id, edge], ...]}` — with unset
690
+ optionals omitted and edge `from` under its alias.
691
+
692
+ - **`to_json(state) -> SerializedGraphState`** — `dict[str, list[list[Any]]]`.
693
+ - **`from_json(data) -> GraphState`** — tolerates missing `items`/`edges` keys.
694
+ - **`stringify(state, pretty=False) -> str`** — compact, or 2-space indented.
695
+ - **`parse(json_str) -> GraphState`**
696
+ - **`SerializedGraphState`** — the serialized-dict type alias.
697
+
698
+ ```python
699
+ from memex import stringify, parse
700
+ snapshot = stringify(state, pretty=True)
701
+ state = parse(snapshot)
702
+ ```
703
+
704
+ ---
705
+
706
+ ## Event envelopes
707
+
708
+ ### `class EventEnvelope` (generic over `payload`)
709
+
710
+ `id`, `namespace`, `type`, `ts` (ISO string), `trace_id: str | None`, `payload: T`.
711
+
712
+ ### `create_event_envelope(type, payload, *, trace_id=None, namespace="memory") -> EventEnvelope[Any]`
713
+
714
+ Mints `id` (UUIDv7) and `ts` (`now_iso`).
715
+
716
+ ### Wrappers
717
+
718
+ Build envelopes from reducer output for an append-only log:
719
+
720
+ - **`wrap_lifecycle_event(event, cause_id, trace_id=None) -> EventEnvelope[dict]`**
721
+ — wraps a `MemoryLifecycleEvent`; payload carries the set fields plus `cause_id`.
722
+ - **`wrap_state_event(item, cause_id, trace_id=None) -> EventEnvelope[dict]`**
723
+ — a `"state.memory"` snapshot of an item.
724
+ - **`wrap_edge_state_event(edge, cause_id, trace_id=None) -> EventEnvelope[dict]`**
725
+ — a `"state.edge"` snapshot of an edge.
726
+
727
+ ---
728
+
729
+ ## Transplant
730
+
731
+ Export a slice of all three graphs and import it elsewhere, optionally re-id'ing
732
+ on collision.
733
+
734
+ ### `export_slice(mem_state, intent_state, task_state, *, ...) -> MemexExport`
735
+
736
+ Keyword options: `memory_ids`, `intent_ids`, `task_ids` (seed sets);
737
+ `include_parents`, `include_children`, `include_aliases`,
738
+ `include_related_tasks`, `include_related_intents` (all `bool`, default
739
+ `False`). Walks the requested relationships and collects edges between included
740
+ memories.
741
+
742
+ - **`class MemexExport`** — `memories: list[MemoryItem]`, `edges: list[Edge]`,
743
+ `intents: list[Intent]`, `tasks: list[Task]`.
744
+ - **`class ExportOptions`** — the same options as a model (for callers that
745
+ prefer to pass a struct).
746
+
747
+ ### `import_slice(mem_state, intent_state, task_state, slice, *, ...) -> ImportResult`
748
+
749
+ `slice: MemexExport | dict`. Options: `skip_existing_ids=True`,
750
+ `shallow_compare_existing=False`, `re_id_on_difference=False`. When re-id'ing,
751
+ colliding-but-different entities get fresh UUIDv7-shaped ids (1ms after the
752
+ original), and all cross-references (`parents`, `parent_id`, `intent_id`,
753
+ memory-id lists) are remapped via a per-graph pre-pass.
754
+
755
+ - **`class ImportResult`** — `mem_state`, `intent_state`, `task_state`,
756
+ `report: ImportReport`.
757
+ - **`class ImportReport`** — `created`, `updated`, `skipped`, `conflicts`, each
758
+ an `ImportBucket`.
759
+ - **`class ImportBucket`** — `memories`, `intents`, `tasks`, `edges` (each
760
+ `list[str]` of ids).
761
+ - **`class ImportOptions`** — the options as a model.
762
+
763
+ ---
764
+
765
+ ## Validation
766
+
767
+ `memex.schemas` is the validation entry point (the parity shim for
768
+ `@ai2070/memex/schemas`). In Pydantic the models *are* the schema.
769
+
770
+ ```python
771
+ from memex.schemas import validate_command
772
+ from memex import apply_command
773
+
774
+ cmd = validate_command(raw) # raises pydantic.ValidationError on bad shape
775
+ state = apply_command(state, cmd).state
776
+ ```
777
+
778
+ | Function | Returns |
779
+ |-----------------------------------|----------------|
780
+ | `validate_command(raw)` | `MemoryCommand`|
781
+ | `validate_intent_command(raw)` | `IntentCommand`|
782
+ | `validate_task_command(raw)` | `TaskCommand` |
783
+ | `validate_memory_item(raw)` | `MemoryItem` |
784
+ | `validate_edge(raw)` | `Edge` |
785
+
786
+ Schema aliases (the model is the schema): `MemoryItemSchema`, `EdgeSchema`,
787
+ `IntentSchema`, `TaskSchema`. Adapters: `MemoryCommandAdapter`,
788
+ `IntentCommandAdapter`, `TaskCommandAdapter`.
789
+
790
+ ---
791
+
792
+ ## `MemexStore` facade
793
+
794
+ A mutable, object-oriented container over the three graphs. It holds the states,
795
+ rebinds them on each mutation, and returns the emitted events — convenient for
796
+ agents and daemons that don't want to thread `state =` through every call. The
797
+ functional API remains the backbone; the facade just wraps it.
798
+
799
+ ```python
800
+ from memex import MemexStore
801
+
802
+ store = MemexStore()
803
+ a = store.create(scope="user:laz", kind="observation", content={"v": 1},
804
+ author="agent:x", source_kind="observed", authority=0.9)
805
+ store.mark_contradiction(a.id, b_id, "system:detector")
806
+ snapshot = store.dumps(pretty=True)
807
+ restored = MemexStore.loads(snapshot)
808
+ ```
809
+
810
+ ### Constructor
811
+
812
+ `MemexStore(mem=None, intents=None, tasks=None)` — start empty or from existing
813
+ states. Attributes `store.mem`, `store.intents`, `store.tasks` expose the live
814
+ states.
815
+
816
+ ### Memory
817
+
818
+ | Method | Returns | Notes |
819
+ |--------|---------|-------|
820
+ | `apply(cmd)` | `list[MemoryLifecycleEvent]` | apply any memory command |
821
+ | `create(**kwargs)` | `MemoryItem` | forwards to `create_memory_item`, then creates |
822
+ | `add(item)` | `MemoryItem` | create from an existing item |
823
+ | `update(item_id, partial, author, reason=None)` | events | |
824
+ | `retract(item_id, author, reason=None)` | events | |
825
+ | `add_edge(**kwargs)` | `Edge` | forwards to `create_edge`, then creates |
826
+ | `items(filter=None, options=None)` | `list[MemoryItem]` | |
827
+ | `item(id)` | `MemoryItem \| None` | |
828
+ | `scored(weights, options=None)` | `list[ScoredItem]` | |
829
+ | `edges(filter=None)` | `list[Edge]` | |
830
+ | `parents(item_id)` / `children(item_id)` | `list[MemoryItem]` | |
831
+ | `related(item_id, direction="both")` | `list[MemoryItem]` | |
832
+ | `smart_retrieve(**kwargs)` | `list[ScoredItem]` | |
833
+ | `support_tree(item_id)` | `SupportNode \| None` | |
834
+ | `support_set(item_id)` | `list[MemoryItem]` | |
835
+ | `stats()` | `GraphStats` | |
836
+
837
+ ### Integrity
838
+
839
+ | Method | Returns |
840
+ |--------|---------|
841
+ | `mark_contradiction(a, b, author, meta=None)` | events |
842
+ | `resolve_contradiction(winner, loser, author, reason=None)` | events |
843
+ | `mark_alias(a, b, author, meta=None)` | events |
844
+ | `cascade_retract(item_id, author, reason=None)` | `list[str]` (retracted ids) |
845
+ | `contradictions()` | `list[Contradiction]` |
846
+ | `stale_items()` | `list[StaleItem]` |
847
+ | `aliases(item_id)` / `alias_group(item_id)` | `list[MemoryItem]` |
848
+
849
+ ### Bulk
850
+
851
+ `apply_many(filter, transform, author, reason=None, options=None)`,
852
+ `bulk_adjust_scores(criteria, delta, author, reason=None)`,
853
+ `decay_importance(older_than_ms, factor, author, reason=None)` — each returns
854
+ events.
855
+
856
+ ### Intent / Task
857
+
858
+ `apply_intent(cmd)`, `create_intent(**kwargs)`, `get_intents(filter=None)`;
859
+ `apply_task(cmd)`, `create_task(**kwargs)`, `get_tasks(filter=None)`.
860
+
861
+ ### Transplant & serialization
862
+
863
+ `export_slice(**kwargs)` → `MemexExport`; `import_slice(slice, **kwargs)` →
864
+ `ImportReport` (mutates the store's states); `to_json()` →
865
+ `SerializedGraphState`; `dumps(pretty=False)` → `str`; `MemexStore.loads(json_str)`
866
+ → `MemexStore` (classmethod, restores the memory graph).
867
+
868
+ ---
869
+
870
+ ## UUID helpers
871
+
872
+ - **`uuid7(ms=None) -> str`** — generate a UUIDv7 string (RFC 9562) for `ms`
873
+ (defaults to now). Encodes a 48-bit big-endian ms timestamp in the first six
874
+ bytes.
875
+ - **`safe_extract_timestamp(value: str) -> int | None`** — decode the ms
876
+ timestamp from a UUIDv7; returns `None` for non-v7 input or a non-positive
877
+ timestamp (unlike [`extract_timestamp`](#extract_timestampuuid_id-str---int),
878
+ which raises).
879
+
880
+ > Install the optional `fast-uuid` extra (`uuid-utils`) for faster generation.
881
+
882
+ ---
883
+
884
+ ## Errors
885
+
886
+ All domain errors derive from `MemexError`.
887
+
888
+ | Exception | Raised by | Attributes |
889
+ |-----------|-----------|------------|
890
+ | `MemexError` | base class | — |
891
+ | `MemoryNotFoundError` | update/retract of an absent item | `item_id` |
892
+ | `EdgeNotFoundError` | update/retract of an absent edge | `edge_id` |
893
+ | `DuplicateMemoryError` | create with an existing id | `item_id` |
894
+ | `DuplicateEdgeError` | create with an existing edge id | `edge_id` |
895
+ | `InvalidTimestampError` | bad UUIDv7 / envelope timestamp | — |
896
+ | `IntentNotFoundError` | intent reducer | `intent_id` |
897
+ | `DuplicateIntentError` | intent reducer | `intent_id` |
898
+ | `InvalidIntentTransitionError` | intent reducer | `intent_id`, `from_status`, `to_status` |
899
+ | `TaskNotFoundError` | task reducer | `task_id` |
900
+ | `DuplicateTaskError` | task reducer | `task_id` |
901
+ | `InvalidTaskTransitionError` | task reducer | `task_id`, `from_status`, `to_status` |
902
+
903
+ Out-of-range scores and malformed command shapes surface as
904
+ `pydantic.ValidationError`. `cost_fn` contract violations and unknown
905
+ sort/decay enums surface as `ValueError`.
906
+
907
+ ---
908
+
909
+ ## Type aliases
910
+
911
+ Open string unions documented as `Literal` aliases — fields accept any `str`,
912
+ but these are the canonical values.
913
+
914
+ | Alias | Values |
915
+ |-------|--------|
916
+ | `KnownMemoryKind` | `observation`, `assertion`, `assumption`, `hypothesis`, `derivation`, `simulation`, `policy`, `trait` |
917
+ | `KnownSourceKind` | `user_explicit`, `observed`, `derived_deterministic`, `agent_inferred`, `simulated`, `imported` |
918
+ | `KnownEdgeKind` | `DERIVED_FROM`, `CONTRADICTS`, `SUPPORTS`, `ABOUT`, `SUPERSEDES`, `ALIAS` |
919
+ | `KnownNamespace` | `memory`, `task`, `agent`, `tool`, `net`, `app`, `chat`, `system`, `debug` |
920
+ | `LifecycleEventType` | `memory.created`, `memory.updated`, `memory.retracted`, `edge.created`, `edge.updated`, `edge.retracted` |
921
+ | `SortField` | `authority`, `conviction`, `importance`, `recency` |
922
+ | `DecayInterval` | `hour`, `day`, `week` |
923
+ | `DecayType` | `exponential`, `linear`, `step` |