@chiligpt/memory 0.1.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 (50) hide show
  1. package/README.md +30 -0
  2. package/dist/core/backfill.d.ts +78 -0
  3. package/dist/core/backfill.d.ts.map +1 -0
  4. package/dist/core/backfill.js +155 -0
  5. package/dist/core/context-graph.d.ts +73 -0
  6. package/dist/core/context-graph.d.ts.map +1 -0
  7. package/dist/core/context-graph.js +363 -0
  8. package/dist/core/entity-index.d.ts +33 -0
  9. package/dist/core/entity-index.d.ts.map +1 -0
  10. package/dist/core/entity-index.js +59 -0
  11. package/dist/core/entity.d.ts +56 -0
  12. package/dist/core/entity.d.ts.map +1 -0
  13. package/dist/core/entity.js +65 -0
  14. package/dist/core/narrative.d.ts +110 -0
  15. package/dist/core/narrative.d.ts.map +1 -0
  16. package/dist/core/narrative.js +765 -0
  17. package/dist/core/read-path.d.ts +53 -0
  18. package/dist/core/read-path.d.ts.map +1 -0
  19. package/dist/core/read-path.js +592 -0
  20. package/dist/core/session-tree-index.d.ts +69 -0
  21. package/dist/core/session-tree-index.d.ts.map +1 -0
  22. package/dist/core/session-tree-index.js +131 -0
  23. package/dist/index.d.ts +16 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +15 -0
  26. package/dist/service.d.ts +105 -0
  27. package/dist/service.d.ts.map +1 -0
  28. package/dist/service.js +465 -0
  29. package/dist/storage/adapter.d.ts +61 -0
  30. package/dist/storage/adapter.d.ts.map +1 -0
  31. package/dist/storage/adapter.js +14 -0
  32. package/dist/storage/file-utils.d.ts +7 -0
  33. package/dist/storage/file-utils.d.ts.map +1 -0
  34. package/dist/storage/file-utils.js +37 -0
  35. package/dist/storage/in-memory-entry.d.ts +5 -0
  36. package/dist/storage/in-memory-entry.d.ts.map +1 -0
  37. package/dist/storage/in-memory-entry.js +5 -0
  38. package/dist/storage/in-memory.d.ts +37 -0
  39. package/dist/storage/in-memory.d.ts.map +1 -0
  40. package/dist/storage/in-memory.js +54 -0
  41. package/dist/storage/jsonl-entry.d.ts +6 -0
  42. package/dist/storage/jsonl-entry.d.ts.map +1 -0
  43. package/dist/storage/jsonl-entry.js +6 -0
  44. package/dist/storage/jsonl.d.ts +32 -0
  45. package/dist/storage/jsonl.d.ts.map +1 -0
  46. package/dist/storage/jsonl.js +171 -0
  47. package/dist/types.d.ts +66 -0
  48. package/dist/types.d.ts.map +1 -0
  49. package/dist/types.js +9 -0
  50. package/package.json +46 -0
@@ -0,0 +1,465 @@
1
+ /**
2
+ * MemoryService — the public API class for @chili/memory.
3
+ *
4
+ * Wraps the session-scoped state previously held as a module-level singleton
5
+ * in the chili memory extension. Consumers (chili adapter, web app) instantiate
6
+ * one MemoryService per session and call lifecycle methods from their event handlers.
7
+ *
8
+ * Adapted for @chili/memory: no chili-internal imports. Uses IMemoryStorage,
9
+ * LLMCompleteFn, and MemoryBranchEntry from the package's own types.
10
+ */
11
+ import { mkdirSync, writeFileSync } from "node:fs";
12
+ import { join } from "node:path";
13
+ import { loadAllEntities, updateEntity } from "./core/entity.js";
14
+ import { EntityIndex } from "./core/entity-index.js";
15
+ import { writeContextNodeFromCompaction, loadAllContextNodes, updateContextNode } from "./core/context-graph.js";
16
+ import { NarrativeCache, archiveDeadHubs, buildNarrativeSystemPromptBlock, loadAllNarrativeNodes, runNarrativeTick, updateNarrativeLayer, } from "./core/narrative.js";
17
+ import { buildSessionStartInjection, buildPerTurnInjection, memoryEntityLookup, memoryQuery, memoryRecall, } from "./core/read-path.js";
18
+ import { recordSessionStart, loadSessionTree, buildTreeRenderRows, renderSessionTree, } from "./core/session-tree-index.js";
19
+ import { findUnindexedSessions, readSessionForBackfill, runBackfillIndexing } from "./core/backfill.js";
20
+ // ---------------------------------------------------------------------------
21
+ // Orphan detection helpers — adapted for IMemoryStorage
22
+ // ---------------------------------------------------------------------------
23
+ const ONE_YEAR_MS = 365 * 86_400_000;
24
+ // ---------------------------------------------------------------------------
25
+ // MemoryService
26
+ // ---------------------------------------------------------------------------
27
+ export class MemoryService {
28
+ storage;
29
+ complete;
30
+ cwd;
31
+ sessionId;
32
+ sessionFile;
33
+ parentSessionFile;
34
+ // Session-scoped mutable state (set during init())
35
+ entityIndex = new EntityIndex();
36
+ narrativeCache = new NarrativeCache();
37
+ sessionStartInjectionDone = false;
38
+ memoryEnabled = true;
39
+ backfillDone = false;
40
+ turnCount = 0;
41
+ initialized = false;
42
+ constructor(config) {
43
+ this.storage = config.storage;
44
+ this.complete = config.complete;
45
+ this.cwd = config.cwd;
46
+ this.sessionId = config.sessionId;
47
+ this.sessionFile = config.sessionFile ?? "";
48
+ this.parentSessionFile = config.parentSessionFile ?? null;
49
+ }
50
+ // ---------------------------------------------------------------------------
51
+ // Lifecycle — called by the adapter from event hooks
52
+ // ---------------------------------------------------------------------------
53
+ /**
54
+ * Initialise storage, build indices, and return the narrative system prompt block.
55
+ * Call once at session_start before any other method.
56
+ * Returns { systemPromptAppend } for inclusion in the session system prompt.
57
+ */
58
+ async init() {
59
+ // Initialise storage (create dirs, touch files)
60
+ await this.storage.init();
61
+ // Build O(1) entity index from disk (O(N) startup)
62
+ const allEntities = (await loadAllEntities(this.storage)).filter((n) => !n._archived);
63
+ this.entityIndex.build(allEntities);
64
+ // Load all NarrativeNodes into cache
65
+ const allNarratives = await loadAllNarrativeNodes(this.storage);
66
+ this.narrativeCache.build(allNarratives);
67
+ // Build BM25F index from hubs + spoke context nodes
68
+ try {
69
+ const allContextNodes = await loadAllContextNodes(this.storage);
70
+ this.narrativeCache.buildIndex(allContextNodes);
71
+ }
72
+ catch (err) {
73
+ console.error("[memory] BM25F index build error:", err instanceof Error ? err.message : String(err));
74
+ }
75
+ // Record session in persistent tree index (F-74)
76
+ try {
77
+ await recordSessionStart({
78
+ storage: this.storage,
79
+ sessionId: this.sessionId,
80
+ sessionFile: this.sessionFile,
81
+ parentSessionFile: this.parentSessionFile,
82
+ cwd: this.cwd,
83
+ });
84
+ }
85
+ catch (err) {
86
+ console.error("[memory] session tree index error:", err instanceof Error ? err.message : String(err));
87
+ }
88
+ // Archive dead hubs at session start (F-98)
89
+ try {
90
+ const deadCount = await archiveDeadHubs(this.storage, this.narrativeCache);
91
+ if (deadCount > 0) {
92
+ console.log(`[memory] Archived ${deadCount} dead hub(s) at session start`);
93
+ }
94
+ }
95
+ catch (err) {
96
+ console.error("[memory] dead hub detection error:", err instanceof Error ? err.message : String(err));
97
+ }
98
+ this.initialized = true;
99
+ // Build and return the narrative system prompt block (F-62)
100
+ const relevantHubs = this.narrativeCache.getHubsForContext(this.cwd, 5);
101
+ const totalHubCount = this.narrativeCache.size;
102
+ const narrativeBlock = buildNarrativeSystemPromptBlock(relevantHubs, totalHubCount);
103
+ return { systemPromptAppend: narrativeBlock };
104
+ }
105
+ /**
106
+ * Called before each agent turn (before_agent_start).
107
+ * Returns a system prompt injection string, or null if nothing to inject.
108
+ */
109
+ async onBeforeTurn(prompt) {
110
+ if (!this.initialized || !this.memoryEnabled)
111
+ return null;
112
+ const injections = [];
113
+ // F-97: Narrative tick every 20 turns (fire-and-forget)
114
+ this.turnCount++;
115
+ if (this.turnCount > 1 && this.turnCount % 20 === 0) {
116
+ runNarrativeTick(this.storage, this.narrativeCache, this.complete).catch((err) => console.error("[memory] narrative tick error:", err instanceof Error ? err.message : String(err)));
117
+ }
118
+ // F-65: Session start context retrieval (once per session)
119
+ if (!this.sessionStartInjectionDone) {
120
+ this.sessionStartInjectionDone = true;
121
+ const sessionBlock = await buildSessionStartInjection({
122
+ storage: this.storage,
123
+ entityIndex: this.entityIndex,
124
+ narrativeCache: this.narrativeCache,
125
+ cwd: this.cwd,
126
+ sessionPrompt: prompt,
127
+ });
128
+ if (sessionBlock)
129
+ injections.push(sessionBlock);
130
+ }
131
+ // F-66: Per-turn entity-driven injection
132
+ if (prompt) {
133
+ const perTurnBlock = await buildPerTurnInjection({
134
+ userMessage: prompt,
135
+ storage: this.storage,
136
+ entityIndex: this.entityIndex,
137
+ });
138
+ if (perTurnBlock)
139
+ injections.push(perTurnBlock);
140
+ }
141
+ return injections.length > 0 ? injections.join("\n\n") : null;
142
+ }
143
+ /**
144
+ * Called after each agent turn (agent_end).
145
+ * Runs retroactive backfill on first turn, fires-and-forgets.
146
+ */
147
+ async onAgentEnd() {
148
+ if (!this.initialized || this.backfillDone)
149
+ return;
150
+ this.backfillDone = true;
151
+ try {
152
+ const result = await runBackfillIndexing({
153
+ storage: this.storage,
154
+ entityIndex: this.entityIndex,
155
+ narrativeCache: this.narrativeCache,
156
+ complete: this.complete,
157
+ currentSessionId: this.sessionId,
158
+ });
159
+ if (result.processed > 0) {
160
+ console.log(`[memory] Indexed ${result.processed} past session(s) into memory.`);
161
+ }
162
+ }
163
+ catch (err) {
164
+ console.error("[memory] backfill error:", err instanceof Error ? err.message : String(err));
165
+ }
166
+ }
167
+ /**
168
+ * Called when a compaction event fires (session_before_compact).
169
+ * Writes a ContextNode and updates the narrative layer.
170
+ */
171
+ async onCompaction(entries, sessionId, sessionFile, cwd) {
172
+ if (!this.initialized)
173
+ return;
174
+ const newNode = await writeContextNodeFromCompaction({
175
+ branchEntries: entries,
176
+ sessionId,
177
+ sessionFile,
178
+ storage: this.storage,
179
+ entityIndex: this.entityIndex,
180
+ complete: this.complete,
181
+ cwd,
182
+ });
183
+ if (newNode) {
184
+ await updateNarrativeLayer({
185
+ newNode,
186
+ storage: this.storage,
187
+ cache: this.narrativeCache,
188
+ complete: this.complete,
189
+ });
190
+ }
191
+ }
192
+ /**
193
+ * Called at session shutdown (session_shutdown).
194
+ * Performs session-close flush, dead hub archival, and orphan detection.
195
+ */
196
+ async onShutdown() {
197
+ if (!this.initialized)
198
+ return;
199
+ // F-73: Session-close flush — index uncompacted session entries
200
+ try {
201
+ if (this.sessionFile) {
202
+ const sessionData = readSessionForBackfill(this.sessionFile);
203
+ if (sessionData && sessionData.entries.length > 0) {
204
+ const newNode = await writeContextNodeFromCompaction({
205
+ branchEntries: sessionData.entries,
206
+ sessionId: sessionData.sessionId,
207
+ sessionFile: this.sessionFile,
208
+ storage: this.storage,
209
+ entityIndex: this.entityIndex,
210
+ complete: this.complete,
211
+ cwd: sessionData.cwd,
212
+ });
213
+ if (newNode) {
214
+ await updateNarrativeLayer({
215
+ newNode,
216
+ storage: this.storage,
217
+ cache: this.narrativeCache,
218
+ complete: this.complete,
219
+ });
220
+ }
221
+ }
222
+ }
223
+ }
224
+ catch (err) {
225
+ console.error("[memory] session flush error:", err instanceof Error ? err.message : String(err));
226
+ }
227
+ finally {
228
+ // F-98: Archive dead hubs
229
+ try {
230
+ await archiveDeadHubs(this.storage, this.narrativeCache);
231
+ }
232
+ catch (err) {
233
+ console.error("[memory] dead hub detection error:", err instanceof Error ? err.message : String(err));
234
+ }
235
+ // F-56: Orphan detection
236
+ try {
237
+ await this.runOrphanDetection();
238
+ }
239
+ catch (err) {
240
+ console.error("[memory] orphan detection error:", err instanceof Error ? err.message : String(err));
241
+ }
242
+ }
243
+ }
244
+ /**
245
+ * Run retroactive backfill indexing for all unindexed sessions.
246
+ */
247
+ async runBackfill() {
248
+ if (!this.initialized)
249
+ return { processed: 0, skipped: 0, errors: 0, total: 0 };
250
+ return runBackfillIndexing({
251
+ storage: this.storage,
252
+ entityIndex: this.entityIndex,
253
+ narrativeCache: this.narrativeCache,
254
+ complete: this.complete,
255
+ currentSessionId: this.sessionId,
256
+ });
257
+ }
258
+ // ---------------------------------------------------------------------------
259
+ // Query methods — called by tool execute callbacks and web consumers
260
+ // ---------------------------------------------------------------------------
261
+ async query(topic) {
262
+ if (!this.memoryEnabled)
263
+ return "[Memory system disabled for this session]";
264
+ return memoryQuery(topic, this.storage, this.entityIndex, this.narrativeCache, this.cwd, this.complete);
265
+ }
266
+ async entityLookup(name, type) {
267
+ if (!this.memoryEnabled)
268
+ return "[Memory system disabled for this session]";
269
+ return memoryEntityLookup(name, type, this.storage, this.entityIndex);
270
+ }
271
+ async recall(topic, options = {}) {
272
+ if (!this.memoryEnabled)
273
+ return "[Memory system disabled for this session]";
274
+ const fromDate = options.from_date ? new Date(options.from_date) : undefined;
275
+ const toDate = options.to_date ? new Date(options.to_date) : undefined;
276
+ return memoryRecall(topic, this.storage, this.entityIndex, this.narrativeCache, options.max_turns ?? 30, this.cwd, fromDate, toDate, this.complete);
277
+ }
278
+ // ---------------------------------------------------------------------------
279
+ // Toggle and status
280
+ // ---------------------------------------------------------------------------
281
+ enable() {
282
+ this.memoryEnabled = true;
283
+ }
284
+ disable() {
285
+ this.memoryEnabled = false;
286
+ }
287
+ get isEnabled() {
288
+ return this.memoryEnabled;
289
+ }
290
+ get stats() {
291
+ return {
292
+ hubs: this.narrativeCache.size,
293
+ entities: this.entityIndex.size,
294
+ contextNodes: 0, // populated lazily — callers can call query for count
295
+ };
296
+ }
297
+ // ---------------------------------------------------------------------------
298
+ // Session tree view (used by /memory tree)
299
+ // ---------------------------------------------------------------------------
300
+ async renderTree(currentSessionId) {
301
+ const index = await loadSessionTree(this.storage);
302
+ const contextNodes = await this.storage.readAll("context");
303
+ const contextNodeSessions = new Set(contextNodes.map((n) => n.sessionId));
304
+ const rows = buildTreeRenderRows(index, currentSessionId, contextNodeSessions);
305
+ return renderSessionTree(rows);
306
+ }
307
+ /**
308
+ * Count of unindexed sessions. Returned from init() as a hint to the caller
309
+ * (the chili adapter can then notify the user).
310
+ */
311
+ async countUnindexed() {
312
+ try {
313
+ const unindexed = await findUnindexedSessions(this.storage, this.sessionId);
314
+ return unindexed.length;
315
+ }
316
+ catch {
317
+ return 0;
318
+ }
319
+ }
320
+ // ---------------------------------------------------------------------------
321
+ // Skill file management (called from resources_discover)
322
+ // ---------------------------------------------------------------------------
323
+ /**
324
+ * Write SKILL.md files to the extension storage dir and return their paths.
325
+ * Called once per session from the resources_discover event handler.
326
+ */
327
+ getSkillPaths(storageDir) {
328
+ const skillsBase = join(storageDir, "skills");
329
+ const memoryQueryDir = join(skillsBase, "memory-query");
330
+ const memoryLookupDir = join(skillsBase, "memory-lookup");
331
+ const memoryRecallDir = join(skillsBase, "memory-recall");
332
+ mkdirSync(memoryQueryDir, { recursive: true });
333
+ mkdirSync(memoryLookupDir, { recursive: true });
334
+ mkdirSync(memoryRecallDir, { recursive: true });
335
+ writeFileSync(join(memoryQueryDir, "SKILL.md"), SKILL_MEMORY_QUERY, "utf-8");
336
+ writeFileSync(join(memoryLookupDir, "SKILL.md"), SKILL_MEMORY_LOOKUP, "utf-8");
337
+ writeFileSync(join(memoryRecallDir, "SKILL.md"), SKILL_MEMORY_RECALL, "utf-8");
338
+ return [memoryQueryDir, memoryLookupDir, memoryRecallDir];
339
+ }
340
+ // ---------------------------------------------------------------------------
341
+ // Private helpers
342
+ // ---------------------------------------------------------------------------
343
+ async runOrphanDetection() {
344
+ const now = Date.now();
345
+ const archivedIds = new Set();
346
+ for (const node of this.entityIndex.values()) {
347
+ if (now - node.lastReferencedAt > ONE_YEAR_MS) {
348
+ await this.storage.archive("entities-archive", node);
349
+ await updateEntity(this.storage, node.id, { _archived: true });
350
+ archivedIds.add(node.id);
351
+ }
352
+ }
353
+ if (archivedIds.size === 0)
354
+ return;
355
+ const contextNodes = await this.storage.readAll("context");
356
+ for (const ctx of contextNodes) {
357
+ if (!ctx.entityPointerIds?.length)
358
+ continue;
359
+ if (ctx.allEntitiesArchived)
360
+ continue;
361
+ const allArchived = ctx.entityPointerIds.every((id) => archivedIds.has(id));
362
+ if (allArchived) {
363
+ await updateContextNode(this.storage, ctx.id, { allEntitiesArchived: true });
364
+ }
365
+ }
366
+ }
367
+ }
368
+ // ---------------------------------------------------------------------------
369
+ // Skill content strings (F-70, F-71, F-81/F-82)
370
+ // ---------------------------------------------------------------------------
371
+ const SKILL_MEMORY_QUERY = `---
372
+ name: memory-query
373
+ description: Broad memory context recall — loads narrative hub summaries, all linked context and entity nodes, with optional session-level detail. Use when user wants full context on a topic area.
374
+ ---
375
+
376
+ # Memory Query Protocol
377
+
378
+ Use this skill when the user asks about past work, ongoing themes, or broad topic areas stored in memory.
379
+
380
+ ## Three-Step Traversal Protocol
381
+
382
+ ### Step 1 — Narrative Match
383
+ Call \`memory_query(topic)\` with the user's topic. The tool matches narrative hub topics (case-insensitive containment) and returns hub summaries with linked context node summaries. Report all matching hubs and their summaries to orient the user.
384
+
385
+ ### Step 2 — Context Node Load
386
+ The \`memory_query\` response includes context node summaries for each matched hub. For each context node:
387
+ - Check the health annotation: nodes marked \`[STALE: N degraded, M deprecated entities]\` should be flagged.
388
+ - Note the session ID, date, and working directory fields for provenance.
389
+ - Both sides of a supersession pair are always surfaced — present both; never present only the superseding node.
390
+
391
+ ### Step 3 — Entity Investigation (on-demand only)
392
+ **Do not automatically call \`memory_entity_lookup\` for every entity in the result.** Only call it when:
393
+ - The user explicitly asks about the health, history, or validity of a specific named entity.
394
+ - An entity appears in a stale context node and the user wants to know if it is still relevant.
395
+
396
+ When called, \`memory_entity_lookup(name, type)\` returns:
397
+ - Current validity score and label (trusted / uncertain / degraded / deprecated).
398
+ - All context nodes that reference the entity, newest first.
399
+
400
+ Explicitly warn the user when any looked-up entity has validity < 0.3 (degraded or deprecated), citing the entity name and score.
401
+
402
+ ### Layer 1 Traversal (on-demand only)
403
+ **Do not access Layer 1 automatically.** When the user explicitly requests session-level detail or conversation excerpts, use \`memory_recall(topic)\` — it resolves the session file internally and returns conversation turns without requiring manual tree reads or bash greps.
404
+
405
+ ### Response Format
406
+ Present results in this order:
407
+ 1. Hub summary (title, topics, summary, session count).
408
+ 2. Context node summaries, newest first (date, user context, agent context, entity health table).
409
+ 3. Entity detail, only if explicitly requested via Step 3.
410
+ 4. Session conversation turns, only if explicitly requested (use \`memory_recall\`).
411
+ `;
412
+ const SKILL_MEMORY_LOOKUP = `---
413
+ name: memory-lookup
414
+ description: Pinpoint session-level memory retrieval — returns the actual conversation turns from the best matching session. Use when user asks about a specific past exchange or decision.
415
+ ---
416
+
417
+ # Memory Lookup Protocol
418
+
419
+ Use this skill when the user asks about a specific past conversation, decision, or exchange — not just a topic area. This skill always retrieves Layer 1 session turns. Use \`/memory-query\` when the user wants breadth-first topic recall without seeing the raw conversation.
420
+
421
+ ## Two-Step Retrieval Protocol
422
+
423
+ ### Step 1 — Recall
424
+ Call \`memory_recall(topic)\` with the user's topic. This single tool returns:
425
+ - Matching narrative hub context (session ID, date, user context, agent context, entity health).
426
+ - The actual conversation turns from the matched session JSONL.
427
+
428
+ The tool resolves the session file path internally — do **not** manually grep for session paths or read \`layer0-session-tree.jsonl\`. If the tool reports "Session file not found", present the context node summary as the best available record.
429
+
430
+ If the user provides a session ID directly (not a topic), call \`memory_query(sessionId)\` first to locate the context node, then call \`memory_recall\` with one of the context node's topics.
431
+
432
+ ### Step 2 — Entity Lookup (only when explicitly needed)
433
+ Call \`memory_entity_lookup(name, type)\` **only** when the user explicitly asks about a specific named artifact — a file path, function name, module, person, or architectural decision. Do **not** call it for topic phrases like "gardening", "python", or "auth flow" — those are topics for \`memory_recall\`, not entities.
434
+
435
+ When in doubt: if the user said a noun that could be a filename or proper name, use entity lookup. If they described a subject area or past conversation, use \`memory_recall\`.
436
+
437
+ ## Supersession
438
+ Context nodes marked \`[SUPERSEDES: <sessionId>]\` indicate the retrieved session updates an earlier one. Always surface both sides of a supersession pair so the user can see the evolution.
439
+ `;
440
+ const SKILL_MEMORY_RECALL = `---
441
+ name: memory-recall
442
+ description: Unified session recall tool — retrieves context node summary and conversation turns in one call. Agent-callable via memory_recall(topic). No manual file resolution needed.
443
+ ---
444
+
445
+ # memory_recall Tool Reference
446
+
447
+ \`memory_recall\` is an agent tool (not a user slash command). It is the primary retrieval primitive for \`/memory-lookup\`.
448
+
449
+ ## What it does
450
+ Combines narrative hub matching, context node load, and Layer 1 JSONL reading into a single tool call. Returns the context node summary (session ID, date, entities, health) plus the actual conversation turns from the matched session.
451
+
452
+ ## When to use
453
+ - User asks about a specific past conversation or decision → call \`memory_recall(topic)\`
454
+ - Do NOT manually grep for session paths or read \`layer0-session-tree.jsonl\` — the tool resolves these internally via the \`sessionFile\` field (F-75) or session tree index fallback.
455
+
456
+ ## Parameters
457
+ - \`topic\` (string) — the subject area or keyword to match against narrative hubs
458
+ - \`max_turns\` (number, optional) — cap on conversation turns returned (default 30, max 200)
459
+
460
+ ## Session file resolution order
461
+ 1. Uses \`sessionFile\` field on the context node directly (F-75 — present on all modern nodes).
462
+ 2. Falls back to session tree index lookup when \`sessionFile\` is absent (pre-F-75 nodes).
463
+ 3. Reports "Not available" if both fail — presents context node summary as best available record.
464
+ `;
465
+ //# sourceMappingURL=service.js.map
@@ -0,0 +1,61 @@
1
+ /**
2
+ * IMemoryStorage — the storage adapter interface for @chili/memory.
3
+ *
4
+ * Decouples all domain logic from storage concerns. Implementations:
5
+ * - JsonlMemoryStorage (Node.js, file-per-collection, proper-lockfile)
6
+ * - InMemoryStorage (tests and Phase 1 web validation, no persistence)
7
+ * - PostgresMemoryStorage (web, defined by the consuming repo against this interface)
8
+ *
9
+ * readAll() is async throughout — required for Postgres compatibility.
10
+ * JsonlMemoryStorage wraps synchronous readFileSync in Promise.resolve() at
11
+ * zero overhead while satisfying the interface for all consumers.
12
+ */
13
+ /**
14
+ * Active collection identifiers. Each maps to a distinct storage location
15
+ * (file, table, or in-memory map) in the implementing adapter.
16
+ */
17
+ export type MemoryCollection = "context" | "entities" | "narratives" | "session-tree";
18
+ /**
19
+ * Archive collection identifiers. Records moved here are never deleted —
20
+ * consistent with the Layer 1 immutability principle.
21
+ */
22
+ export type MemoryArchiveCollection = "entities-archive" | "narratives-archive";
23
+ export interface IMemoryStorage {
24
+ /**
25
+ * Initialise storage — create directories, tables, or in-memory maps.
26
+ * Must be called once before any other method.
27
+ */
28
+ init(): Promise<void>;
29
+ /**
30
+ * Append a new record to a collection.
31
+ * The record must have a stable string `id` field.
32
+ */
33
+ append<T extends {
34
+ id: string;
35
+ }>(collection: MemoryCollection, record: T): Promise<void>;
36
+ /**
37
+ * Read all active records from a collection, in insertion order.
38
+ *
39
+ * Async throughout so Postgres and other async adapters satisfy the same
40
+ * interface. JsonlMemoryStorage wraps readFileSync in Promise.resolve().
41
+ */
42
+ readAll<T>(collection: MemoryCollection): Promise<T[]>;
43
+ /**
44
+ * Apply a partial patch to the record matching `id` in `collection`.
45
+ * Fields not included in `patch` are left unchanged.
46
+ * No-op when `id` is not found.
47
+ */
48
+ update<T extends {
49
+ id: string;
50
+ }>(collection: MemoryCollection, id: string, patch: Partial<T>): Promise<void>;
51
+ /**
52
+ * Move a record to the archive collection.
53
+ * Implementations must not delete the original — archive is append-only
54
+ * cold storage. The caller is responsible for marking the active record
55
+ * with `_archived: true` via `update()` if needed.
56
+ */
57
+ archive<T extends {
58
+ id: string;
59
+ }>(collection: MemoryArchiveCollection, record: T): Promise<void>;
60
+ }
61
+ //# sourceMappingURL=adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../src/storage/adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAMH;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GACzB,SAAS,GACT,UAAU,GACV,YAAY,GACZ,cAAc,CAAC;AAElB;;;GAGG;AACH,MAAM,MAAM,uBAAuB,GAChC,kBAAkB,GAClB,oBAAoB,CAAC;AAMxB,MAAM,WAAW,cAAc;IAC9B;;;OAGG;IACH,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAEtB;;;OAGG;IACH,MAAM,CAAC,CAAC,SAAS;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEzF;;;;;OAKG;IACH,OAAO,CAAC,CAAC,EAAE,UAAU,EAAE,gBAAgB,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC;IAEvD;;;;OAIG;IACH,MAAM,CAAC,CAAC,SAAS;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,EAC9B,UAAU,EAAE,gBAAgB,EAC5B,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,GACf,OAAO,CAAC,IAAI,CAAC,CAAC;IAEjB;;;;;OAKG;IACH,OAAO,CAAC,CAAC,SAAS;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,EAC/B,UAAU,EAAE,uBAAuB,EACnC,MAAM,EAAE,CAAC,GACP,OAAO,CAAC,IAAI,CAAC,CAAC;CACjB","sourcesContent":["/**\n * IMemoryStorage — the storage adapter interface for @chili/memory.\n *\n * Decouples all domain logic from storage concerns. Implementations:\n * - JsonlMemoryStorage (Node.js, file-per-collection, proper-lockfile)\n * - InMemoryStorage (tests and Phase 1 web validation, no persistence)\n * - PostgresMemoryStorage (web, defined by the consuming repo against this interface)\n *\n * readAll() is async throughout — required for Postgres compatibility.\n * JsonlMemoryStorage wraps synchronous readFileSync in Promise.resolve() at\n * zero overhead while satisfying the interface for all consumers.\n */\n\n// ---------------------------------------------------------------------------\n// Collection names\n// ---------------------------------------------------------------------------\n\n/**\n * Active collection identifiers. Each maps to a distinct storage location\n * (file, table, or in-memory map) in the implementing adapter.\n */\nexport type MemoryCollection =\n\t| \"context\" // ContextNode records — layer2-context.jsonl\n\t| \"entities\" // EntityNode records — layer2-entity.jsonl\n\t| \"narratives\" // NarrativeNode records — layer3-narrative.jsonl\n\t| \"session-tree\"; // SessionTreeEntry records — layer0-session-tree.jsonl\n\n/**\n * Archive collection identifiers. Records moved here are never deleted —\n * consistent with the Layer 1 immutability principle.\n */\nexport type MemoryArchiveCollection =\n\t| \"entities-archive\" // Orphaned EntityNodes (>12 months unreferenced)\n\t| \"narratives-archive\"; // LRU-evicted NarrativeNodes (beyond 20-hub limit)\n\n// ---------------------------------------------------------------------------\n// Adapter interface\n// ---------------------------------------------------------------------------\n\nexport interface IMemoryStorage {\n\t/**\n\t * Initialise storage — create directories, tables, or in-memory maps.\n\t * Must be called once before any other method.\n\t */\n\tinit(): Promise<void>;\n\n\t/**\n\t * Append a new record to a collection.\n\t * The record must have a stable string `id` field.\n\t */\n\tappend<T extends { id: string }>(collection: MemoryCollection, record: T): Promise<void>;\n\n\t/**\n\t * Read all active records from a collection, in insertion order.\n\t *\n\t * Async throughout so Postgres and other async adapters satisfy the same\n\t * interface. JsonlMemoryStorage wraps readFileSync in Promise.resolve().\n\t */\n\treadAll<T>(collection: MemoryCollection): Promise<T[]>;\n\n\t/**\n\t * Apply a partial patch to the record matching `id` in `collection`.\n\t * Fields not included in `patch` are left unchanged.\n\t * No-op when `id` is not found.\n\t */\n\tupdate<T extends { id: string }>(\n\t\tcollection: MemoryCollection,\n\t\tid: string,\n\t\tpatch: Partial<T>,\n\t): Promise<void>;\n\n\t/**\n\t * Move a record to the archive collection.\n\t * Implementations must not delete the original — archive is append-only\n\t * cold storage. The caller is responsible for marking the active record\n\t * with `_archived: true` via `update()` if needed.\n\t */\n\tarchive<T extends { id: string }>(\n\t\tcollection: MemoryArchiveCollection,\n\t\trecord: T,\n\t): Promise<void>;\n}\n"]}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * IMemoryStorage — the storage adapter interface for @chili/memory.
3
+ *
4
+ * Decouples all domain logic from storage concerns. Implementations:
5
+ * - JsonlMemoryStorage (Node.js, file-per-collection, proper-lockfile)
6
+ * - InMemoryStorage (tests and Phase 1 web validation, no persistence)
7
+ * - PostgresMemoryStorage (web, defined by the consuming repo against this interface)
8
+ *
9
+ * readAll() is async throughout — required for Postgres compatibility.
10
+ * JsonlMemoryStorage wraps synchronous readFileSync in Promise.resolve() at
11
+ * zero overhead while satisfying the interface for all consumers.
12
+ */
13
+ export {};
14
+ //# sourceMappingURL=adapter.js.map
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Read all records from an arbitrary JSONL file path.
3
+ * Returns an empty array if the file does not exist or is empty.
4
+ * Malformed lines are silently skipped.
5
+ */
6
+ export declare function readJsonlFile<T>(filePath: string): T[];
7
+ //# sourceMappingURL=file-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-utils.d.ts","sourceRoot":"","sources":["../../src/storage/file-utils.ts"],"names":[],"mappings":"AAUA;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,GAAG,CAAC,EAAE,CAmBtD","sourcesContent":["/**\n * Thin Node.js utility for reading arbitrary JSONL files.\n *\n * NOT part of IMemoryStorage — this is used only by readSessionForBackfill()\n * to read existing session JSONL files that are not managed by the storage\n * adapter. The web consumer would never call readSessionForBackfill (it has\n * no session JSONL files on disk).\n */\nimport { readFileSync } from \"node:fs\";\n\n/**\n * Read all records from an arbitrary JSONL file path.\n * Returns an empty array if the file does not exist or is empty.\n * Malformed lines are silently skipped.\n */\nexport function readJsonlFile<T>(filePath: string): T[] {\n\tlet raw: string;\n\ttry {\n\t\traw = readFileSync(filePath, \"utf-8\");\n\t} catch {\n\t\treturn [];\n\t}\n\n\tconst records: T[] = [];\n\tfor (const line of raw.split(\"\\n\")) {\n\t\tconst trimmed = line.trim();\n\t\tif (!trimmed) continue;\n\t\ttry {\n\t\t\trecords.push(JSON.parse(trimmed) as T);\n\t\t} catch {\n\t\t\t// Skip malformed lines\n\t\t}\n\t}\n\treturn records;\n}\n"]}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Thin Node.js utility for reading arbitrary JSONL files.
3
+ *
4
+ * NOT part of IMemoryStorage — this is used only by readSessionForBackfill()
5
+ * to read existing session JSONL files that are not managed by the storage
6
+ * adapter. The web consumer would never call readSessionForBackfill (it has
7
+ * no session JSONL files on disk).
8
+ */
9
+ import { readFileSync } from "node:fs";
10
+ /**
11
+ * Read all records from an arbitrary JSONL file path.
12
+ * Returns an empty array if the file does not exist or is empty.
13
+ * Malformed lines are silently skipped.
14
+ */
15
+ export function readJsonlFile(filePath) {
16
+ let raw;
17
+ try {
18
+ raw = readFileSync(filePath, "utf-8");
19
+ }
20
+ catch {
21
+ return [];
22
+ }
23
+ const records = [];
24
+ for (const line of raw.split("\n")) {
25
+ const trimmed = line.trim();
26
+ if (!trimmed)
27
+ continue;
28
+ try {
29
+ records.push(JSON.parse(trimmed));
30
+ }
31
+ catch {
32
+ // Skip malformed lines
33
+ }
34
+ }
35
+ return records;
36
+ }
37
+ //# sourceMappingURL=file-utils.js.map
@@ -0,0 +1,5 @@
1
+ /**
2
+ * @chili/memory/storage/in-memory — direct sub-entry point for InMemoryStorage.
3
+ */
4
+ export { InMemoryStorage } from "./in-memory.js";
5
+ //# sourceMappingURL=in-memory-entry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"in-memory-entry.d.ts","sourceRoot":"","sources":["../../src/storage/in-memory-entry.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC","sourcesContent":["/**\n * @chili/memory/storage/in-memory — direct sub-entry point for InMemoryStorage.\n */\nexport { InMemoryStorage } from \"./in-memory.js\";\n"]}
@@ -0,0 +1,5 @@
1
+ /**
2
+ * @chili/memory/storage/in-memory — direct sub-entry point for InMemoryStorage.
3
+ */
4
+ export { InMemoryStorage } from "./in-memory.js";
5
+ //# sourceMappingURL=in-memory-entry.js.map
@@ -0,0 +1,37 @@
1
+ /**
2
+ * InMemoryStorage — IMemoryStorage implementation backed by in-memory Maps.
3
+ *
4
+ * No disk I/O. No locking. All methods return immediately resolved promises.
5
+ * Used for:
6
+ * - Unit tests (replacing temp-dir JSONL setup with fast in-memory fixtures)
7
+ * - Phase 1 web validation (confirms MemoryService lifecycle works without Postgres)
8
+ *
9
+ * Records are stored in insertion order per collection. readAll() returns
10
+ * records in insertion order, matching the JSONL file behaviour.
11
+ */
12
+ import type { IMemoryStorage, MemoryCollection, MemoryArchiveCollection } from "./adapter.js";
13
+ export declare class InMemoryStorage implements IMemoryStorage {
14
+ private readonly store;
15
+ init(): Promise<void>;
16
+ append<T extends {
17
+ id: string;
18
+ }>(collection: MemoryCollection, record: T): Promise<void>;
19
+ readAll<T>(collection: MemoryCollection): Promise<T[]>;
20
+ update<T extends {
21
+ id: string;
22
+ }>(collection: MemoryCollection, id: string, patch: Partial<T>): Promise<void>;
23
+ archive<T extends {
24
+ id: string;
25
+ }>(collection: MemoryArchiveCollection, record: T): Promise<void>;
26
+ /**
27
+ * Reset all collections — useful between tests.
28
+ * Not part of IMemoryStorage interface; test-only utility.
29
+ */
30
+ clear(): void;
31
+ /**
32
+ * Read from archive collections — not part of IMemoryStorage but useful for
33
+ * asserting archive operations in tests.
34
+ */
35
+ readArchive<T>(collection: MemoryArchiveCollection): T[];
36
+ }
37
+ //# sourceMappingURL=in-memory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"in-memory.d.ts","sourceRoot":"","sources":["../../src/storage/in-memory.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAC;AAE9F,qBAAa,eAAgB,YAAW,cAAc;IACrD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAgC;IAEhD,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAE1B;IAEK,MAAM,CAAC,CAAC,SAAS;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAI7F;IAEK,OAAO,CAAC,CAAC,EAAE,UAAU,EAAE,gBAAgB,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC,CAE3D;IAEK,MAAM,CAAC,CAAC,SAAS;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,EACpC,UAAU,EAAE,gBAAgB,EAC5B,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,GACf,OAAO,CAAC,IAAI,CAAC,CAMf;IAEK,OAAO,CAAC,CAAC,SAAS;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,EACrC,UAAU,EAAE,uBAAuB,EACnC,MAAM,EAAE,CAAC,GACP,OAAO,CAAC,IAAI,CAAC,CAIf;IAED;;;OAGG;IACH,KAAK,IAAI,IAAI,CAEZ;IAED;;;OAGG;IACH,WAAW,CAAC,CAAC,EAAE,UAAU,EAAE,uBAAuB,GAAG,CAAC,EAAE,CAEvD;CACD","sourcesContent":["/**\n * InMemoryStorage — IMemoryStorage implementation backed by in-memory Maps.\n *\n * No disk I/O. No locking. All methods return immediately resolved promises.\n * Used for:\n * - Unit tests (replacing temp-dir JSONL setup with fast in-memory fixtures)\n * - Phase 1 web validation (confirms MemoryService lifecycle works without Postgres)\n *\n * Records are stored in insertion order per collection. readAll() returns\n * records in insertion order, matching the JSONL file behaviour.\n */\n\nimport type { IMemoryStorage, MemoryCollection, MemoryArchiveCollection } from \"./adapter.js\";\n\nexport class InMemoryStorage implements IMemoryStorage {\n\tprivate readonly store = new Map<string, unknown[]>();\n\n\tasync init(): Promise<void> {\n\t\t// Nothing to initialise for in-memory storage.\n\t}\n\n\tasync append<T extends { id: string }>(collection: MemoryCollection, record: T): Promise<void> {\n\t\tconst existing = this.store.get(collection) ?? [];\n\t\texisting.push(record);\n\t\tthis.store.set(collection, existing);\n\t}\n\n\tasync readAll<T>(collection: MemoryCollection): Promise<T[]> {\n\t\treturn (this.store.get(collection) ?? []) as T[];\n\t}\n\n\tasync update<T extends { id: string }>(\n\t\tcollection: MemoryCollection,\n\t\tid: string,\n\t\tpatch: Partial<T>,\n\t): Promise<void> {\n\t\tconst records = this.store.get(collection);\n\t\tif (!records) return;\n\t\tconst idx = records.findIndex((r) => (r as { id?: string }).id === id);\n\t\tif (idx === -1) return;\n\t\trecords[idx] = { ...(records[idx] as object), ...patch };\n\t}\n\n\tasync archive<T extends { id: string }>(\n\t\tcollection: MemoryArchiveCollection,\n\t\trecord: T,\n\t): Promise<void> {\n\t\tconst existing = this.store.get(collection) ?? [];\n\t\texisting.push(record);\n\t\tthis.store.set(collection, existing);\n\t}\n\n\t/**\n\t * Reset all collections — useful between tests.\n\t * Not part of IMemoryStorage interface; test-only utility.\n\t */\n\tclear(): void {\n\t\tthis.store.clear();\n\t}\n\n\t/**\n\t * Read from archive collections — not part of IMemoryStorage but useful for\n\t * asserting archive operations in tests.\n\t */\n\treadArchive<T>(collection: MemoryArchiveCollection): T[] {\n\t\treturn (this.store.get(collection) ?? []) as T[];\n\t}\n}\n"]}