@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.
- package/.github/workflows/ci.yml +31 -0
- package/.github/workflows/release.yml +35 -0
- package/API.md +1078 -0
- package/LICENSE +190 -0
- package/README.md +574 -0
- package/package.json +30 -0
- package/src/bulk.ts +128 -0
- package/src/envelope.ts +52 -0
- package/src/errors.ts +27 -0
- package/src/graph.ts +15 -0
- package/src/helpers.ts +51 -0
- package/src/index.ts +142 -0
- package/src/integrity.ts +378 -0
- package/src/intent.ts +311 -0
- package/src/query.ts +357 -0
- package/src/reducer.ts +177 -0
- package/src/replay.ts +32 -0
- package/src/retrieval.ts +306 -0
- package/src/serialization.ts +34 -0
- package/src/stats.ts +62 -0
- package/src/task.ts +373 -0
- package/src/transplant.ts +488 -0
- package/src/types.ts +248 -0
- package/tests/bugfix-and-coverage.test.ts +958 -0
- package/tests/bugfix-holes.test.ts +856 -0
- package/tests/bulk.test.ts +256 -0
- package/tests/edge-cases-v2.test.ts +355 -0
- package/tests/edge-cases.test.ts +661 -0
- package/tests/envelope.test.ts +92 -0
- package/tests/graph.test.ts +41 -0
- package/tests/helpers.test.ts +120 -0
- package/tests/integrity.test.ts +371 -0
- package/tests/intent.test.ts +276 -0
- package/tests/query-advanced.test.ts +252 -0
- package/tests/query.test.ts +623 -0
- package/tests/reducer.test.ts +342 -0
- package/tests/replay.test.ts +145 -0
- package/tests/retrieval.test.ts +691 -0
- package/tests/serialization.test.ts +118 -0
- package/tests/setup.test.ts +7 -0
- package/tests/stats.test.ts +163 -0
- package/tests/task.test.ts +322 -0
- package/tests/transplant.test.ts +385 -0
- package/tests/types.test.ts +231 -0
- package/tsconfig.json +18 -0
- 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.
|