@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
@@ -0,0 +1,378 @@
1
+ import type {
2
+ GraphState,
3
+ MemoryItem,
4
+ Edge,
5
+ MemoryLifecycleEvent,
6
+ ScoreWeights,
7
+ ScoredItem,
8
+ } from "./types.js";
9
+ import { applyCommand } from "./reducer.js";
10
+ import { getEdges, getChildren, getScoredItems } from "./query.js";
11
+ import { uuidv7 } from "uuidv7";
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // 1. Temporal forking — conflict detection & resolution
15
+ // ---------------------------------------------------------------------------
16
+
17
+ export interface Contradiction {
18
+ a: MemoryItem;
19
+ b: MemoryItem;
20
+ edge?: Edge;
21
+ }
22
+
23
+ /**
24
+ * Find all active CONTRADICTS edges and return the item pairs.
25
+ */
26
+ export function getContradictions(state: GraphState): Contradiction[] {
27
+ const contradictEdges = getEdges(state, {
28
+ kind: "CONTRADICTS",
29
+ active_only: true,
30
+ });
31
+ const results: Contradiction[] = [];
32
+ for (const edge of contradictEdges) {
33
+ const a = state.items.get(edge.from);
34
+ const b = state.items.get(edge.to);
35
+ if (a && b) results.push({ a, b, edge });
36
+ }
37
+ return results;
38
+ }
39
+
40
+ /**
41
+ * Mark two items as contradicting each other.
42
+ * Creates a CONTRADICTS edge between them.
43
+ */
44
+ export function markContradiction(
45
+ state: GraphState,
46
+ itemIdA: string,
47
+ itemIdB: string,
48
+ author: string,
49
+ meta?: Record<string, unknown>,
50
+ ): { state: GraphState; events: MemoryLifecycleEvent[] } {
51
+ return applyCommand(state, {
52
+ type: "edge.create",
53
+ edge: {
54
+ edge_id: uuidv7(),
55
+ from: itemIdA,
56
+ to: itemIdB,
57
+ kind: "CONTRADICTS",
58
+ author,
59
+ source_kind: "derived_deterministic",
60
+ authority: 1,
61
+ active: true,
62
+ meta,
63
+ },
64
+ });
65
+ }
66
+
67
+ /**
68
+ * Resolve a contradiction by marking the winner as superseding the loser.
69
+ * - Creates a SUPERSEDES edge (winner -> loser)
70
+ * - Lowers the loser's authority
71
+ * - Retracts the CONTRADICTS edge
72
+ */
73
+ export function resolveContradiction(
74
+ state: GraphState,
75
+ winnerId: string,
76
+ loserId: string,
77
+ author: string,
78
+ reason?: string,
79
+ ): { state: GraphState; events: MemoryLifecycleEvent[] } {
80
+ let current = state;
81
+ const allEvents: MemoryLifecycleEvent[] = [];
82
+
83
+ // find and retract the CONTRADICTS edge(s) between them
84
+ // collect matching edges first to avoid iterating a map while mutating state
85
+ const toRetract: string[] = [];
86
+ for (const edge of current.edges.values()) {
87
+ if (
88
+ edge.kind === "CONTRADICTS" &&
89
+ edge.active &&
90
+ ((edge.from === winnerId && edge.to === loserId) ||
91
+ (edge.from === loserId && edge.to === winnerId))
92
+ ) {
93
+ toRetract.push(edge.edge_id);
94
+ }
95
+ }
96
+
97
+ for (const edgeId of toRetract) {
98
+ const r = applyCommand(current, {
99
+ type: "edge.retract",
100
+ edge_id: edgeId,
101
+ author,
102
+ reason,
103
+ });
104
+ current = r.state;
105
+ allEvents.push(...r.events);
106
+ }
107
+
108
+ if (toRetract.length === 0) {
109
+ throw new Error(
110
+ `No active CONTRADICTS edge between ${winnerId} and ${loserId}`,
111
+ );
112
+ }
113
+
114
+ // create SUPERSEDES edge
115
+ const r1 = applyCommand(current, {
116
+ type: "edge.create",
117
+ edge: {
118
+ edge_id: uuidv7(),
119
+ from: winnerId,
120
+ to: loserId,
121
+ kind: "SUPERSEDES",
122
+ author,
123
+ source_kind: "derived_deterministic",
124
+ authority: 1,
125
+ active: true,
126
+ meta: reason ? { reason } : undefined,
127
+ },
128
+ });
129
+ current = r1.state;
130
+ allEvents.push(...r1.events);
131
+
132
+ // lower loser's authority
133
+ const loser = current.items.get(loserId);
134
+ if (loser) {
135
+ const r2 = applyCommand(current, {
136
+ type: "memory.update",
137
+ item_id: loserId,
138
+ partial: { authority: loser.authority * 0.1 },
139
+ author,
140
+ reason,
141
+ });
142
+ current = r2.state;
143
+ allEvents.push(...r2.events);
144
+ }
145
+
146
+ return { state: current, events: allEvents };
147
+ }
148
+
149
+ // ---------------------------------------------------------------------------
150
+ // 2. Observational continuity — stale detection & cascade
151
+ // ---------------------------------------------------------------------------
152
+
153
+ export interface StaleItem {
154
+ item: MemoryItem;
155
+ missing_parents: string[];
156
+ }
157
+
158
+ /**
159
+ * Find items whose parents have been retracted (missing from state).
160
+ */
161
+ export function getStaleItems(state: GraphState): StaleItem[] {
162
+ const results: StaleItem[] = [];
163
+ for (const item of state.items.values()) {
164
+ if (!item.parents || item.parents.length === 0) continue;
165
+ const missing = item.parents.filter((pid) => !state.items.has(pid));
166
+ if (missing.length > 0) {
167
+ results.push({ item, missing_parents: missing });
168
+ }
169
+ }
170
+ return results;
171
+ }
172
+
173
+ /**
174
+ * Find items that depend on a specific item (directly or transitively).
175
+ */
176
+ export function getDependents(
177
+ state: GraphState,
178
+ itemId: string,
179
+ transitive = false,
180
+ ): MemoryItem[] {
181
+ const direct = getChildren(state, itemId);
182
+ if (!transitive) return direct;
183
+
184
+ const visited = new Set<string>();
185
+ const result: MemoryItem[] = [];
186
+ const queue = [...direct];
187
+
188
+ while (queue.length > 0) {
189
+ const item = queue.pop()!;
190
+ if (visited.has(item.id)) continue;
191
+ visited.add(item.id);
192
+ result.push(item);
193
+ queue.push(...getChildren(state, item.id));
194
+ }
195
+
196
+ return result;
197
+ }
198
+
199
+ /**
200
+ * Cascade retraction: retract an item and all its transitive dependents.
201
+ */
202
+ export function cascadeRetract(
203
+ state: GraphState,
204
+ itemId: string,
205
+ author: string,
206
+ reason?: string,
207
+ ): { state: GraphState; events: MemoryLifecycleEvent[]; retracted: string[] } {
208
+ const dependents = getDependents(state, itemId, true);
209
+ let current = state;
210
+ const allEvents: MemoryLifecycleEvent[] = [];
211
+ const retracted: string[] = [];
212
+
213
+ // retract dependents first (leaves before roots)
214
+ for (const dep of dependents.reverse()) {
215
+ if (!current.items.has(dep.id)) continue;
216
+ const r = applyCommand(current, {
217
+ type: "memory.retract",
218
+ item_id: dep.id,
219
+ author,
220
+ reason: reason ?? `parent ${itemId} retracted`,
221
+ });
222
+ current = r.state;
223
+ allEvents.push(...r.events);
224
+ retracted.push(dep.id);
225
+ }
226
+
227
+ // retract the item itself
228
+ if (current.items.has(itemId)) {
229
+ const r = applyCommand(current, {
230
+ type: "memory.retract",
231
+ item_id: itemId,
232
+ author,
233
+ reason,
234
+ });
235
+ current = r.state;
236
+ allEvents.push(...r.events);
237
+ retracted.push(itemId);
238
+ }
239
+
240
+ return { state: current, events: allEvents, retracted };
241
+ }
242
+
243
+ // ---------------------------------------------------------------------------
244
+ // 3. Recognition vs discovery — identity / aliasing
245
+ // ---------------------------------------------------------------------------
246
+
247
+ /**
248
+ * Mark two items as referring to the same entity.
249
+ * Creates bidirectional ALIAS edges.
250
+ */
251
+ export function markAlias(
252
+ state: GraphState,
253
+ itemIdA: string,
254
+ itemIdB: string,
255
+ author: string,
256
+ meta?: Record<string, unknown>,
257
+ ): { state: GraphState; events: MemoryLifecycleEvent[] } {
258
+ let current = state;
259
+ const allEvents: MemoryLifecycleEvent[] = [];
260
+
261
+ const r1 = applyCommand(current, {
262
+ type: "edge.create",
263
+ edge: {
264
+ edge_id: uuidv7(),
265
+ from: itemIdA,
266
+ to: itemIdB,
267
+ kind: "ALIAS",
268
+ author,
269
+ source_kind: "derived_deterministic",
270
+ authority: 1,
271
+ active: true,
272
+ meta,
273
+ },
274
+ });
275
+ current = r1.state;
276
+ allEvents.push(...r1.events);
277
+
278
+ const r2 = applyCommand(current, {
279
+ type: "edge.create",
280
+ edge: {
281
+ edge_id: uuidv7(),
282
+ from: itemIdB,
283
+ to: itemIdA,
284
+ kind: "ALIAS",
285
+ author,
286
+ source_kind: "derived_deterministic",
287
+ authority: 1,
288
+ active: true,
289
+ meta,
290
+ },
291
+ });
292
+ current = r2.state;
293
+ allEvents.push(...r2.events);
294
+
295
+ return { state: current, events: allEvents };
296
+ }
297
+
298
+ /**
299
+ * Get all items that are aliased to a given item (directly).
300
+ */
301
+ export function getAliases(state: GraphState, itemId: string): MemoryItem[] {
302
+ const aliasEdges = getEdges(state, {
303
+ from: itemId,
304
+ kind: "ALIAS",
305
+ active_only: true,
306
+ });
307
+ const results: MemoryItem[] = [];
308
+ for (const edge of aliasEdges) {
309
+ const item = state.items.get(edge.to);
310
+ if (item) results.push(item);
311
+ }
312
+ return results;
313
+ }
314
+
315
+ /**
316
+ * Get the full alias group for an item (transitive closure).
317
+ */
318
+ export function getAliasGroup(state: GraphState, itemId: string): MemoryItem[] {
319
+ const visited = new Set<string>();
320
+ const result: MemoryItem[] = [];
321
+ const queue = [itemId];
322
+
323
+ while (queue.length > 0) {
324
+ const id = queue.pop()!;
325
+ if (visited.has(id)) continue;
326
+ visited.add(id);
327
+ const item = state.items.get(id);
328
+ if (item) result.push(item);
329
+ const aliases = getEdges(state, {
330
+ from: id,
331
+ kind: "ALIAS",
332
+ active_only: true,
333
+ });
334
+ for (const edge of aliases) {
335
+ queue.push(edge.to);
336
+ }
337
+ }
338
+
339
+ return result;
340
+ }
341
+
342
+ // ---------------------------------------------------------------------------
343
+ // 4. Budget-aware probabilistic retrieval
344
+ // ---------------------------------------------------------------------------
345
+
346
+ export interface BudgetOptions {
347
+ budget: number; // total budget (e.g. token count, cost units)
348
+ costFn: (item: MemoryItem) => number; // cost per item
349
+ weights: ScoreWeights; // scoring weights
350
+ filter?: import("./types.js").MemoryFilter;
351
+ }
352
+
353
+ /**
354
+ * Retrieve the highest-scoring items that fit within a budget.
355
+ * Items are ranked by weighted score, then greedily packed.
356
+ */
357
+ export function getItemsByBudget(
358
+ state: GraphState,
359
+ options: BudgetOptions,
360
+ ): ScoredItem[] {
361
+ const scored = getScoredItems(state, options.weights, {
362
+ pre: options.filter,
363
+ });
364
+
365
+ const results: ScoredItem[] = [];
366
+ let remaining = options.budget;
367
+
368
+ for (const entry of scored) {
369
+ const cost = options.costFn(entry.item);
370
+ if (cost <= remaining) {
371
+ results.push(entry);
372
+ remaining -= cost;
373
+ }
374
+ if (remaining <= 0) break;
375
+ }
376
+
377
+ return results;
378
+ }
package/src/intent.ts ADDED
@@ -0,0 +1,311 @@
1
+ import { uuidv7 } from "uuidv7";
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Types
5
+ // ---------------------------------------------------------------------------
6
+
7
+ export type IntentStatus = "active" | "paused" | "completed" | "cancelled";
8
+
9
+ export interface Intent {
10
+ id: string;
11
+ label: string;
12
+ description?: string;
13
+
14
+ priority: number; // 0..1
15
+ owner: string; // "user:laz", "agent:reasoner", "system:watchdog"
16
+ status: IntentStatus;
17
+
18
+ context?: Record<string, unknown>;
19
+ root_memory_ids?: string[];
20
+
21
+ meta?: Record<string, unknown>;
22
+ }
23
+
24
+ export interface IntentState {
25
+ intents: Map<string, Intent>;
26
+ }
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // State
30
+ // ---------------------------------------------------------------------------
31
+
32
+ export function createIntentState(): IntentState {
33
+ return { intents: new Map() };
34
+ }
35
+
36
+ // ---------------------------------------------------------------------------
37
+ // Factory
38
+ // ---------------------------------------------------------------------------
39
+
40
+ export function createIntent(
41
+ input: Omit<Intent, "id" | "status"> & { id?: string; status?: IntentStatus },
42
+ ): Intent {
43
+ return {
44
+ ...input,
45
+ id: input.id ?? uuidv7(),
46
+ status: input.status ?? "active",
47
+ };
48
+ }
49
+
50
+ // ---------------------------------------------------------------------------
51
+ // Commands
52
+ // ---------------------------------------------------------------------------
53
+
54
+ export type IntentCommand =
55
+ | { type: "intent.create"; intent: Intent }
56
+ | {
57
+ type: "intent.update";
58
+ intent_id: string;
59
+ partial: Partial<Intent>;
60
+ author: string;
61
+ reason?: string;
62
+ }
63
+ | {
64
+ type: "intent.complete";
65
+ intent_id: string;
66
+ author: string;
67
+ reason?: string;
68
+ }
69
+ | {
70
+ type: "intent.cancel";
71
+ intent_id: string;
72
+ author: string;
73
+ reason?: string;
74
+ }
75
+ | { type: "intent.pause"; intent_id: string; author: string; reason?: string }
76
+ | {
77
+ type: "intent.resume";
78
+ intent_id: string;
79
+ author: string;
80
+ reason?: string;
81
+ };
82
+
83
+ // ---------------------------------------------------------------------------
84
+ // Lifecycle events
85
+ // ---------------------------------------------------------------------------
86
+
87
+ export interface IntentLifecycleEvent {
88
+ namespace: "intent";
89
+ type:
90
+ | "intent.created"
91
+ | "intent.updated"
92
+ | "intent.completed"
93
+ | "intent.cancelled"
94
+ | "intent.paused"
95
+ | "intent.resumed";
96
+ intent: Intent;
97
+ cause_type: string;
98
+ }
99
+
100
+ // ---------------------------------------------------------------------------
101
+ // Errors
102
+ // ---------------------------------------------------------------------------
103
+
104
+ export class IntentNotFoundError extends Error {
105
+ constructor(id: string) {
106
+ super(`Intent not found: ${id}`);
107
+ this.name = "IntentNotFoundError";
108
+ }
109
+ }
110
+
111
+ export class DuplicateIntentError extends Error {
112
+ constructor(id: string) {
113
+ super(`Intent already exists: ${id}`);
114
+ this.name = "DuplicateIntentError";
115
+ }
116
+ }
117
+
118
+ export class InvalidIntentTransitionError extends Error {
119
+ constructor(id: string, from: IntentStatus, to: string) {
120
+ super(`Invalid intent transition: ${id} from ${from} to ${to}`);
121
+ this.name = "InvalidIntentTransitionError";
122
+ }
123
+ }
124
+
125
+ // ---------------------------------------------------------------------------
126
+ // Helpers
127
+ // ---------------------------------------------------------------------------
128
+
129
+ function stripUndefined<T extends Record<string, unknown>>(obj: T): Partial<T> {
130
+ const result: Record<string, unknown> = {};
131
+ for (const [key, value] of Object.entries(obj)) {
132
+ if (value !== undefined) result[key] = value;
133
+ }
134
+ return result as Partial<T>;
135
+ }
136
+
137
+ // ---------------------------------------------------------------------------
138
+ // Reducer
139
+ // ---------------------------------------------------------------------------
140
+
141
+ function setStatus(
142
+ state: IntentState,
143
+ id: string,
144
+ targetStatus: IntentStatus,
145
+ validFrom: IntentStatus[],
146
+ author: string,
147
+ causeType: string,
148
+ eventType: IntentLifecycleEvent["type"],
149
+ ): { state: IntentState; events: IntentLifecycleEvent[] } {
150
+ const existing = state.intents.get(id);
151
+ if (!existing) throw new IntentNotFoundError(id);
152
+ if (!validFrom.includes(existing.status)) {
153
+ throw new InvalidIntentTransitionError(id, existing.status, targetStatus);
154
+ }
155
+ const updated: Intent = { ...existing, status: targetStatus };
156
+ const intents = new Map(state.intents);
157
+ intents.set(id, updated);
158
+ return {
159
+ state: { intents },
160
+ events: [
161
+ {
162
+ namespace: "intent",
163
+ type: eventType,
164
+ intent: updated,
165
+ cause_type: causeType,
166
+ },
167
+ ],
168
+ };
169
+ }
170
+
171
+ export function applyIntentCommand(
172
+ state: IntentState,
173
+ cmd: IntentCommand,
174
+ ): { state: IntentState; events: IntentLifecycleEvent[] } {
175
+ switch (cmd.type) {
176
+ case "intent.create": {
177
+ if (state.intents.has(cmd.intent.id)) {
178
+ throw new DuplicateIntentError(cmd.intent.id);
179
+ }
180
+ const intents = new Map(state.intents);
181
+ intents.set(cmd.intent.id, cmd.intent);
182
+ return {
183
+ state: { intents },
184
+ events: [
185
+ {
186
+ namespace: "intent",
187
+ type: "intent.created",
188
+ intent: cmd.intent,
189
+ cause_type: cmd.type,
190
+ },
191
+ ],
192
+ };
193
+ }
194
+
195
+ case "intent.update": {
196
+ const existing = state.intents.get(cmd.intent_id);
197
+ if (!existing) throw new IntentNotFoundError(cmd.intent_id);
198
+ const { id: _id, status: _status, ...rest } = cmd.partial;
199
+ const updated: Intent = { ...existing, ...stripUndefined(rest) };
200
+ const intents = new Map(state.intents);
201
+ intents.set(cmd.intent_id, updated);
202
+ return {
203
+ state: { intents },
204
+ events: [
205
+ {
206
+ namespace: "intent",
207
+ type: "intent.updated",
208
+ intent: updated,
209
+ cause_type: cmd.type,
210
+ },
211
+ ],
212
+ };
213
+ }
214
+
215
+ case "intent.complete":
216
+ return setStatus(
217
+ state,
218
+ cmd.intent_id,
219
+ "completed",
220
+ ["active", "paused"],
221
+ cmd.author,
222
+ cmd.type,
223
+ "intent.completed",
224
+ );
225
+
226
+ case "intent.cancel":
227
+ return setStatus(
228
+ state,
229
+ cmd.intent_id,
230
+ "cancelled",
231
+ ["active", "paused"],
232
+ cmd.author,
233
+ cmd.type,
234
+ "intent.cancelled",
235
+ );
236
+
237
+ case "intent.pause":
238
+ return setStatus(
239
+ state,
240
+ cmd.intent_id,
241
+ "paused",
242
+ ["active"],
243
+ cmd.author,
244
+ cmd.type,
245
+ "intent.paused",
246
+ );
247
+
248
+ case "intent.resume":
249
+ return setStatus(
250
+ state,
251
+ cmd.intent_id,
252
+ "active",
253
+ ["paused"],
254
+ cmd.author,
255
+ cmd.type,
256
+ "intent.resumed",
257
+ );
258
+ }
259
+ }
260
+
261
+ // ---------------------------------------------------------------------------
262
+ // Query
263
+ // ---------------------------------------------------------------------------
264
+
265
+ export interface IntentFilter {
266
+ owner?: string;
267
+ status?: IntentStatus;
268
+ statuses?: IntentStatus[];
269
+ min_priority?: number;
270
+ has_memory_id?: string;
271
+ }
272
+
273
+ export function getIntents(
274
+ state: IntentState,
275
+ filter?: IntentFilter,
276
+ ): Intent[] {
277
+ if (!filter) return [...state.intents.values()];
278
+
279
+ const results: Intent[] = [];
280
+ for (const intent of state.intents.values()) {
281
+ if (filter.owner !== undefined && intent.owner !== filter.owner) continue;
282
+ if (filter.status !== undefined && intent.status !== filter.status)
283
+ continue;
284
+ if (
285
+ filter.statuses !== undefined &&
286
+ !filter.statuses.includes(intent.status)
287
+ )
288
+ continue;
289
+ if (
290
+ filter.min_priority !== undefined &&
291
+ intent.priority < filter.min_priority
292
+ )
293
+ continue;
294
+ if (filter.has_memory_id !== undefined) {
295
+ if (
296
+ !intent.root_memory_ids ||
297
+ !intent.root_memory_ids.includes(filter.has_memory_id)
298
+ )
299
+ continue;
300
+ }
301
+ results.push(intent);
302
+ }
303
+ return results;
304
+ }
305
+
306
+ export function getIntentById(
307
+ state: IntentState,
308
+ id: string,
309
+ ): Intent | undefined {
310
+ return state.intents.get(id);
311
+ }