@qearlyao/familiar 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 (72) hide show
  1. package/.env.example +31 -0
  2. package/HEARTBEAT.md +23 -0
  3. package/LICENSE +21 -0
  4. package/MEMORY.md +1 -0
  5. package/README.md +245 -0
  6. package/SOUL.md +13 -0
  7. package/USER.md +13 -0
  8. package/config.example.toml +221 -0
  9. package/dist/agent-events.js +167 -0
  10. package/dist/agent.js +590 -0
  11. package/dist/browser-tools.js +638 -0
  12. package/dist/chat-log.js +130 -0
  13. package/dist/cli.js +168 -0
  14. package/dist/config.js +804 -0
  15. package/dist/data-retention.js +54 -0
  16. package/dist/discord.js +1203 -0
  17. package/dist/generated-media.js +86 -0
  18. package/dist/image-derivatives.js +102 -0
  19. package/dist/image-gen.js +440 -0
  20. package/dist/inbound-attachments.js +266 -0
  21. package/dist/index.js +10 -0
  22. package/dist/media-understanding.js +120 -0
  23. package/dist/memory/diary/ambient-injector.js +180 -0
  24. package/dist/memory/diary/ambient.js +124 -0
  25. package/dist/memory/diary/chunks.js +231 -0
  26. package/dist/memory/diary/index.js +3 -0
  27. package/dist/memory/diary/indexer.js +93 -0
  28. package/dist/memory/doctor.js +250 -0
  29. package/dist/memory/index/chunk-indexer.js +151 -0
  30. package/dist/memory/index/embedding-provider.js +119 -0
  31. package/dist/memory/index/fts-query.js +18 -0
  32. package/dist/memory/index/retrieval.js +246 -0
  33. package/dist/memory/index/schema.js +157 -0
  34. package/dist/memory/index/store.js +513 -0
  35. package/dist/memory/index/vec.js +72 -0
  36. package/dist/memory/index/vector-codec.js +27 -0
  37. package/dist/memory/lcm/backfill.js +247 -0
  38. package/dist/memory/lcm/condense.js +146 -0
  39. package/dist/memory/lcm/context-transformer.js +662 -0
  40. package/dist/memory/lcm/context.js +421 -0
  41. package/dist/memory/lcm/eviction-score.js +38 -0
  42. package/dist/memory/lcm/index.js +6 -0
  43. package/dist/memory/lcm/indexer.js +200 -0
  44. package/dist/memory/lcm/normalize.js +235 -0
  45. package/dist/memory/lcm/schema.js +188 -0
  46. package/dist/memory/lcm/segment-manager.js +136 -0
  47. package/dist/memory/lcm/store.js +722 -0
  48. package/dist/memory/lcm/summarizer.js +258 -0
  49. package/dist/memory/lcm/types.js +1 -0
  50. package/dist/memory/operator.js +477 -0
  51. package/dist/memory/service.js +202 -0
  52. package/dist/memory/tools.js +205 -0
  53. package/dist/models.js +165 -0
  54. package/dist/persona.js +54 -0
  55. package/dist/runtime.js +493 -0
  56. package/dist/scheduler.js +200 -0
  57. package/dist/settings.js +116 -0
  58. package/dist/skills.js +38 -0
  59. package/dist/tts.js +143 -0
  60. package/dist/web-auth.js +105 -0
  61. package/dist/web-events.js +114 -0
  62. package/dist/web-http.js +29 -0
  63. package/dist/web-static.js +106 -0
  64. package/dist/web-tools.js +940 -0
  65. package/dist/web-types.js +2 -0
  66. package/dist/web.js +844 -0
  67. package/package.json +60 -0
  68. package/web/dist/assets/index-ClgkMgaq.css +2 -0
  69. package/web/dist/assets/index-Cu2QquuR.js +59 -0
  70. package/web/dist/favicon.svg +1 -0
  71. package/web/dist/icons.svg +24 -0
  72. package/web/dist/index.html +20 -0
@@ -0,0 +1,477 @@
1
+ import { existsSync, statSync } from "node:fs";
2
+ import { mkdir, stat } from "node:fs/promises";
3
+ import { resolve } from "node:path";
4
+ import { stdin as input, stdout as output } from "node:process";
5
+ import { createInterface } from "node:readline/promises";
6
+ import { indexAllDiaryFiles } from "./diary/indexer.js";
7
+ import { applyDoctorFixes, runDoctor } from "./doctor.js";
8
+ import { ChunkIndexer } from "./index/chunk-indexer.js";
9
+ import { createEmbeddingProvider } from "./index/embedding-provider.js";
10
+ import { backfillFromChatLogs } from "./lcm/backfill.js";
11
+ import { indexLcmRecords, indexLcmSummaries } from "./lcm/indexer.js";
12
+ import { lcmRecordIndexSourceId, lcmSummaryIndexSourceId } from "./lcm/store.js";
13
+ import { MemoryService } from "./service.js";
14
+ export async function runMemoryOperator(config, argv) {
15
+ const [command, ...args] = argv;
16
+ if (!command || command === "--help" || command === "help") {
17
+ console.log(memoryHelp());
18
+ return;
19
+ }
20
+ switch (command) {
21
+ case "status":
22
+ await withOperatorService(config, async (service) => printStatus(config, service, args));
23
+ return;
24
+ case "doctor":
25
+ await withOperatorService(config, async (service) => runDoctorCommand(service, args));
26
+ return;
27
+ case "reindex":
28
+ await withOperatorService(config, async (service) => {
29
+ const { controller, dispose } = installMemoryAbortHandler("aborting reindex — finishing current corpus…");
30
+ try {
31
+ await reindex(config, service, parseReindexArgs(args), undefined, controller.signal);
32
+ }
33
+ finally {
34
+ dispose();
35
+ }
36
+ });
37
+ return;
38
+ case "backfill":
39
+ await withOperatorService(config, async (service) => backfill(config, service, parseBackfillArgs(config, args)));
40
+ return;
41
+ case "prune":
42
+ await withOperatorService(config, async (service) => prune(service, await parsePruneArgs(args)));
43
+ return;
44
+ case "backup":
45
+ await withOperatorService(config, async (service) => backup(config, service, parseBackupArgs(args)));
46
+ return;
47
+ default:
48
+ throw new Error(`Unknown memory subcommand: ${command}\n${memoryHelp()}`);
49
+ }
50
+ }
51
+ export function memoryHelp() {
52
+ return [
53
+ "Usage:",
54
+ " familiar memory [workspace] status [--json]",
55
+ " familiar memory [workspace] doctor [--clean]",
56
+ " familiar memory [workspace] reindex [--corpus <name>] [--force]",
57
+ " familiar memory [workspace] backfill [--channels <ch1,ch2>] [--data-dir <path>] [--dry-run]",
58
+ " familiar memory [workspace] prune --new-session-retain-depth <N> [--yes] [--vacuum]",
59
+ " familiar memory [workspace] backup <out-dir>",
60
+ ].join("\n");
61
+ }
62
+ async function withOperatorService(config, fn) {
63
+ const service = MemoryService.createWithoutRuntime(config);
64
+ try {
65
+ return await fn(service);
66
+ }
67
+ finally {
68
+ service.close();
69
+ }
70
+ }
71
+ function printStatus(config, service, args) {
72
+ const json = hasOnlyFlags(args, ["--json"]) && args.includes("--json");
73
+ const status = collectStatus(config, service);
74
+ if (json) {
75
+ console.log(JSON.stringify(status, null, 2));
76
+ return;
77
+ }
78
+ printPlainStatus(status);
79
+ }
80
+ function collectStatus(config, service) {
81
+ const lcmPath = resolve(config.memory.lcmDir, "lcm.sqlite");
82
+ const indexPath = resolve(config.memory.indexDir, "memory.sqlite");
83
+ const indexStats = service.memoryStore.stats();
84
+ return {
85
+ paths: {
86
+ lcm: { path: lcmPath, sizeBytes: fileSize(lcmPath) },
87
+ index: { path: indexPath, sizeBytes: fileSize(indexPath) },
88
+ },
89
+ counts: {
90
+ lcmRecords: countRows(service.lcmStore, "lcm_records"),
91
+ lcmSummariesByDepth: countGrouped(service.lcmStore, "SELECT depth AS key, COUNT(*) AS n FROM lcm_summaries GROUP BY depth"),
92
+ lcmSegments: {
93
+ active: countWhere(service.lcmStore, "lcm_segments", "status = 'active'"),
94
+ closed: countWhere(service.lcmStore, "lcm_segments", "status = 'closed'"),
95
+ },
96
+ lcmContextItems: countRows(service.lcmStore, "lcm_context_items"),
97
+ lcmSessionState: countRows(service.lcmStore, "lcm_session_state"),
98
+ memoryChunksByCorpus: countGrouped(service.memoryStore, "SELECT corpus AS key, COUNT(*) AS n FROM memory_chunks GROUP BY corpus"),
99
+ memoryIndexSources: countRows(service.memoryStore, "memory_index_sources"),
100
+ memoryFtsRows: indexStats.ftsRows,
101
+ memoryVectorRows: indexStats.vectorRows,
102
+ },
103
+ embedding: service.memoryStore.embeddingConfig(),
104
+ vector: {
105
+ capability: indexStats.vectorCapability,
106
+ available: indexStats.vectorAvailable,
107
+ },
108
+ projectionFailures: service.stats().projectionFailures,
109
+ requiresReindex: indexStats.requiresReindex,
110
+ schemaVersions: {
111
+ lcm: service.lcmStore.schemaVersion(),
112
+ index: readMemoryMeta(service.memoryStore, "schema_version"),
113
+ },
114
+ };
115
+ }
116
+ function runDoctorCommand(service, args) {
117
+ const clean = hasOnlyFlags(args, ["--clean"]) && args.includes("--clean");
118
+ const report = runDoctor({ lcm: service.lcmStore, index: service.memoryStore });
119
+ const projectionFailures = service.stats().projectionFailures;
120
+ if (projectionFailures > 0) {
121
+ report.findings.push({
122
+ kind: "projection_failures",
123
+ detail: `${projectionFailures} memory projection failure(s) swallowed in this process`,
124
+ fixable: false,
125
+ });
126
+ report.clean = false;
127
+ }
128
+ if (report.findings.length === 0) {
129
+ console.log("Memory doctor: clean");
130
+ }
131
+ else {
132
+ console.log(`Memory doctor: ${report.findings.length} finding(s)`);
133
+ for (const finding of report.findings)
134
+ console.log(`- ${formatFinding(finding)}`);
135
+ }
136
+ if (clean) {
137
+ const result = applyDoctorFixes({ lcm: service.lcmStore, index: service.memoryStore }, report);
138
+ console.log(result.summary);
139
+ }
140
+ if (!report.clean)
141
+ process.exitCode = 1;
142
+ }
143
+ async function reindex(config, service, options, embeddingProvider, signal) {
144
+ if (options.force) {
145
+ service.memoryStore.db
146
+ .prepare("DELETE FROM memory_meta WHERE k IN ('embedding_model', 'embedding_dimensions', 'requires_reindex')")
147
+ .run();
148
+ }
149
+ const corpora = options.corpus ? [options.corpus] : ["lcm_record", "lcm_summary", "diary_chunk"];
150
+ const runDelete = () => {
151
+ for (const corpus of corpora)
152
+ deleteCorpus(service.memoryStore, corpus);
153
+ };
154
+ if (service.memoryStore.db.inTransaction)
155
+ runDelete();
156
+ else
157
+ service.memoryStore.db.transaction(runDelete).immediate();
158
+ const indexer = options.force
159
+ ? new ChunkIndexer({
160
+ store: service.memoryStore,
161
+ embeddingProvider: embeddingProvider ?? createEmbeddingProvider(config),
162
+ })
163
+ : embeddingProvider
164
+ ? new ChunkIndexer({ store: service.memoryStore, embeddingProvider })
165
+ : service.indexer;
166
+ let chunks = 0;
167
+ if (corpora.includes("lcm_record")) {
168
+ if (signal?.aborted) {
169
+ console.log(`Reindexed ${chunks} chunk(s)`);
170
+ return;
171
+ }
172
+ const result = await indexLcmRecords({ indexer, records: service.lcmStore.listRecords(), signal });
173
+ chunks += result.ids.length;
174
+ printProgress(chunks);
175
+ }
176
+ if (corpora.includes("lcm_summary")) {
177
+ if (signal?.aborted) {
178
+ console.log(`Reindexed ${chunks} chunk(s)`);
179
+ return;
180
+ }
181
+ const result = await indexLcmSummaries({ indexer, summaries: service.lcmStore.listSummaries(), signal });
182
+ chunks += result.ids.length;
183
+ printProgress(chunks);
184
+ }
185
+ if (corpora.includes("diary_chunk")) {
186
+ if (signal?.aborted) {
187
+ console.log(`Reindexed ${chunks} chunk(s)`);
188
+ return;
189
+ }
190
+ const result = await indexAllDiaryFiles({ config, indexer, signal });
191
+ for (const file of result.files) {
192
+ if (signal?.aborted)
193
+ break;
194
+ chunks += file.result.ids.length;
195
+ printProgress(chunks);
196
+ }
197
+ }
198
+ console.log(`Reindexed ${chunks} chunk(s)`);
199
+ }
200
+ async function backfill(config, service, options) {
201
+ const embeddingProvider = createEmbeddingProvider(config);
202
+ const { controller, dispose } = installMemoryAbortHandler("aborting backfill — finishing current batch…");
203
+ try {
204
+ const report = await backfillFromChatLogs({
205
+ lcmStore: service.lcmStore,
206
+ memoryStore: service.memoryStore,
207
+ indexer: service.indexer,
208
+ embeddingProvider,
209
+ config,
210
+ }, { ...options, signal: controller.signal });
211
+ printBackfillReport(report);
212
+ if (report.errors.length > 0)
213
+ process.exitCode = 1;
214
+ }
215
+ finally {
216
+ dispose();
217
+ }
218
+ }
219
+ async function prune(service, options) {
220
+ if (!options.yes && !(await confirm(`Prune closed LCM raw records with retain depth ${options.retainDepth}?`))) {
221
+ console.log("Prune cancelled");
222
+ return;
223
+ }
224
+ const activeSegments = service.lcmStore.listSegments().filter((segment) => segment.status === "active");
225
+ const activeSegmentId = activeSegments.at(-1)?.id ?? null;
226
+ const runClose = () => {
227
+ for (const segment of activeSegments) {
228
+ if (segment.id !== activeSegmentId)
229
+ service.lcmStore.closeSegment(segment.id);
230
+ }
231
+ };
232
+ if (service.lcmStore.db.inTransaction)
233
+ runClose();
234
+ else
235
+ service.lcmStore.db.transaction(runClose).immediate();
236
+ const report = service.lcmStore.applyNewSessionRetention({
237
+ newSessionRetainDepth: options.retainDepth,
238
+ activeSegmentId,
239
+ vacuum: options.vacuum,
240
+ });
241
+ for (const ref of report.indexDeletes)
242
+ service.memoryStore.deleteBySource(ref.corpus, ref.sourceId);
243
+ const closedActive = activeSegments.filter((segment) => segment.id !== activeSegmentId);
244
+ if (closedActive.length > 0) {
245
+ const runReopen = () => {
246
+ for (const segment of closedActive) {
247
+ service.lcmStore.db
248
+ .prepare("UPDATE lcm_segments SET status = 'active', closed_at = NULL, updated_at = unixepoch() WHERE id = ?")
249
+ .run(segment.id);
250
+ }
251
+ };
252
+ if (service.lcmStore.db.inTransaction)
253
+ runReopen();
254
+ else
255
+ service.lcmStore.db.transaction(runReopen).immediate();
256
+ }
257
+ console.log(`Pruned ${report.rawRecordsDeleted} raw record(s), ${report.summariesDeleted} summary row(s), ` +
258
+ `${report.affectedSegments.length} closed segment(s) scanned`);
259
+ }
260
+ async function backup(config, service, outDir) {
261
+ await mkdir(outDir, { recursive: true });
262
+ const lcmOut = resolve(outDir, "lcm.sqlite");
263
+ const memoryOut = resolve(outDir, "memory.sqlite");
264
+ await Promise.all([service.lcmStore.db.backup(lcmOut), service.memoryStore.db.backup(memoryOut)]);
265
+ const [lcmStat, memoryStat] = await Promise.all([stat(lcmOut), stat(memoryOut)]);
266
+ console.log(`LCM backup: ${lcmOut} (${formatBytes(lcmStat.size)})`);
267
+ console.log(`Index backup: ${memoryOut} (${formatBytes(memoryStat.size)})`);
268
+ void config;
269
+ }
270
+ function parseReindexArgs(args) {
271
+ let corpus;
272
+ let force = false;
273
+ for (let index = 0; index < args.length; index++) {
274
+ const arg = args[index];
275
+ if (arg === "--force") {
276
+ force = true;
277
+ continue;
278
+ }
279
+ if (arg === "--corpus") {
280
+ corpus = args[++index];
281
+ if (!corpus)
282
+ throw new Error("Missing value for --corpus");
283
+ continue;
284
+ }
285
+ throw new Error(`Unknown reindex argument: ${arg}`);
286
+ }
287
+ return { corpus, force };
288
+ }
289
+ function parseBackfillArgs(config, args) {
290
+ let dataDir = config.workspace.dataDir;
291
+ let channels;
292
+ let dryRun = false;
293
+ for (let index = 0; index < args.length; index++) {
294
+ const arg = args[index];
295
+ if (arg === "--dry-run") {
296
+ dryRun = true;
297
+ continue;
298
+ }
299
+ if (arg === "--data-dir") {
300
+ const raw = args[++index];
301
+ if (!raw)
302
+ throw new Error("Missing value for --data-dir");
303
+ dataDir = resolve(raw);
304
+ continue;
305
+ }
306
+ if (arg === "--channels") {
307
+ const raw = args[++index];
308
+ if (!raw)
309
+ throw new Error("Missing value for --channels");
310
+ channels = raw
311
+ .split(",")
312
+ .map((channel) => channel.trim())
313
+ .filter(Boolean);
314
+ continue;
315
+ }
316
+ throw new Error(`Unknown backfill argument: ${arg}`);
317
+ }
318
+ return { dataDir, channels, dryRun };
319
+ }
320
+ async function parsePruneArgs(args) {
321
+ let retainDepth;
322
+ let yes = false;
323
+ let vacuum = false;
324
+ for (let index = 0; index < args.length; index++) {
325
+ const arg = args[index];
326
+ if (arg === "--yes") {
327
+ yes = true;
328
+ continue;
329
+ }
330
+ if (arg === "--vacuum") {
331
+ vacuum = true;
332
+ continue;
333
+ }
334
+ if (arg === "--new-session-retain-depth") {
335
+ const raw = args[++index];
336
+ if (!raw)
337
+ throw new Error("Missing value for --new-session-retain-depth");
338
+ retainDepth = Number(raw);
339
+ continue;
340
+ }
341
+ throw new Error(`Unknown prune argument: ${arg}`);
342
+ }
343
+ if (retainDepth === undefined || !Number.isInteger(retainDepth) || retainDepth < -1) {
344
+ throw new Error("prune requires --new-session-retain-depth <integer >= -1>");
345
+ }
346
+ return { retainDepth, yes, vacuum };
347
+ }
348
+ function parseBackupArgs(args) {
349
+ if (args.length !== 1)
350
+ throw new Error("backup requires <out-dir>");
351
+ return resolve(args[0]);
352
+ }
353
+ function hasOnlyFlags(args, allowed) {
354
+ for (const arg of args) {
355
+ if (!allowed.includes(arg))
356
+ throw new Error(`Unknown argument: ${arg}`);
357
+ }
358
+ return true;
359
+ }
360
+ function deleteCorpus(store, corpus) {
361
+ const rows = store.db.prepare("SELECT source_id FROM memory_index_sources WHERE corpus = ?").all(corpus);
362
+ const sourceIds = new Set(rows.map((row) => row.source_id));
363
+ for (const sourceId of sourceIds)
364
+ store.deleteBySourceUnsafe(corpus, sourceId);
365
+ const orphanRows = store.db.prepare("SELECT id FROM memory_chunks WHERE corpus = ?").all(corpus);
366
+ for (const row of orphanRows) {
367
+ store.db.prepare("DELETE FROM memory_fts WHERE rowid = ?").run(row.id);
368
+ store.db.prepare("DELETE FROM memory_chunks WHERE id = ?").run(row.id);
369
+ }
370
+ }
371
+ function installMemoryAbortHandler(message) {
372
+ const controller = new AbortController();
373
+ const onSigint = () => {
374
+ if (!controller.signal.aborted)
375
+ console.log(message);
376
+ controller.abort();
377
+ };
378
+ process.once("SIGINT", onSigint);
379
+ return {
380
+ controller,
381
+ dispose: () => process.off("SIGINT", onSigint),
382
+ };
383
+ }
384
+ function countRows(store, table) {
385
+ const row = store.db.prepare(`SELECT COUNT(*) AS n FROM ${table}`).get();
386
+ return row.n;
387
+ }
388
+ function countWhere(store, table, where) {
389
+ const row = store.db.prepare(`SELECT COUNT(*) AS n FROM ${table} WHERE ${where}`).get();
390
+ return row.n;
391
+ }
392
+ function countGrouped(store, sql) {
393
+ const rows = store.db.prepare(sql).all();
394
+ return Object.fromEntries(rows.map((row) => [String(row.key), row.n]));
395
+ }
396
+ function readMemoryMeta(store, key) {
397
+ const row = store.db.prepare("SELECT v FROM memory_meta WHERE k = ?").get(key);
398
+ return row ? Number(row.v) : null;
399
+ }
400
+ function fileSize(path) {
401
+ return existsSync(path) ? statSync(path).size : 0;
402
+ }
403
+ function formatFinding(finding) {
404
+ return `${finding.kind} ${finding.fixable ? "[fixable]" : "[manual]"}: ${finding.detail}`;
405
+ }
406
+ function printPlainStatus(status) {
407
+ console.log("Memory status");
408
+ console.log(`LCM DB: ${status.paths.lcm.path} (${formatBytes(status.paths.lcm.sizeBytes)})`);
409
+ console.log(`Index DB: ${status.paths.index.path} (${formatBytes(status.paths.index.sizeBytes)})`);
410
+ console.log(`LCM records: ${status.counts.lcmRecords}`);
411
+ console.log(`LCM summaries by depth: ${JSON.stringify(status.counts.lcmSummariesByDepth)}`);
412
+ console.log(`LCM segments: active=${status.counts.lcmSegments.active} closed=${status.counts.lcmSegments.closed}`);
413
+ console.log(`LCM context items: ${status.counts.lcmContextItems}`);
414
+ console.log(`LCM session state: ${status.counts.lcmSessionState}`);
415
+ console.log(`Memory chunks by corpus: ${JSON.stringify(status.counts.memoryChunksByCorpus)}`);
416
+ console.log(`Memory index sources: ${status.counts.memoryIndexSources}`);
417
+ console.log(`Memory FTS rows: ${status.counts.memoryFtsRows}`);
418
+ console.log(`Memory vector rows: ${status.counts.memoryVectorRows} (${status.vector.available ? status.vector.capability : "blob-js fallback"})`);
419
+ console.log(`Embedding: ${status.embedding.provider}/${status.embedding.model} dim=${status.embedding.dimensions}`);
420
+ console.log(`Projection failures: ${status.projectionFailures}`);
421
+ if (status.requiresReindex)
422
+ console.log("Reindex required: run familiar memory reindex --force");
423
+ console.log(`Schema versions: lcm=${status.schemaVersions.lcm ?? "unknown"} index=${status.schemaVersions.index ?? "unknown"}`);
424
+ }
425
+ function printProgress(chunks) {
426
+ if (chunks > 0 && chunks % 100 === 0)
427
+ console.log(`Reindexed ${chunks} chunk(s)`);
428
+ }
429
+ function printBackfillReport(report) {
430
+ const rows = [
431
+ ["chatFilesProcessed", report.chatFilesProcessed],
432
+ ["transcriptFilesProcessed", report.transcriptFilesProcessed],
433
+ ["recordsInserted", report.recordsInserted],
434
+ ["recordsSkippedDuplicate", report.recordsSkippedDuplicate],
435
+ ["segmentsCreated", report.segmentsCreated],
436
+ ["summariesInserted", report.summariesInserted],
437
+ ["indexedChunks", report.indexedChunks],
438
+ ["errors", report.errors.length],
439
+ ];
440
+ const width = Math.max(...rows.map(([field]) => field.length));
441
+ console.log("Memory backfill summary");
442
+ for (const [field, value] of rows)
443
+ console.log(`${field.padEnd(width)} ${value}`);
444
+ for (const error of report.errors)
445
+ console.log(`error ${error}`);
446
+ }
447
+ async function confirm(question) {
448
+ const rl = createInterface({ input, output });
449
+ try {
450
+ const answer = await rl.question(`${question} Type yes to continue: `);
451
+ return answer.trim().toLowerCase() === "yes";
452
+ }
453
+ finally {
454
+ rl.close();
455
+ }
456
+ }
457
+ function formatBytes(bytes) {
458
+ if (bytes < 1024)
459
+ return `${bytes} B`;
460
+ const units = ["KB", "MB", "GB", "TB"];
461
+ let value = bytes / 1024;
462
+ let unit = 0;
463
+ while (value >= 1024 && unit < units.length - 1) {
464
+ value /= 1024;
465
+ unit += 1;
466
+ }
467
+ return `${value.toFixed(value >= 10 ? 1 : 2)} ${units[unit]}`;
468
+ }
469
+ export const __memoryOperatorTest = {
470
+ collectStatus,
471
+ reindex,
472
+ prune,
473
+ backup,
474
+ backfill,
475
+ lcmRecordIndexSourceId,
476
+ lcmSummaryIndexSourceId,
477
+ };
@@ -0,0 +1,202 @@
1
+ import { watch } from "node:fs";
2
+ import { basename, resolve } from "node:path";
3
+ import { __ambientDiaryInjectorTest, AmbientDiaryInjector } from "./diary/ambient-injector.js";
4
+ import { DIARY_INDEX_FILE_RE, indexAllDiaryFiles, indexDiaryFile, removeDiaryFileIndex } from "./diary/indexer.js";
5
+ import { ChunkIndexer } from "./index/chunk-indexer.js";
6
+ import { createEmbeddingProvider } from "./index/embedding-provider.js";
7
+ import { MemoryIndexStore } from "./index/store.js";
8
+ import { LcmContextTransformer } from "./lcm/context-transformer.js";
9
+ import { LcmSegmentManager } from "./lcm/segment-manager.js";
10
+ import { LcmStore } from "./lcm/store.js";
11
+ import { DefaultLcmSummarizer } from "./lcm/summarizer.js";
12
+ import { createMemoryTools } from "./tools.js";
13
+ export function createMemoryService(config, options = {}) {
14
+ return new DefaultMemoryService(config, options);
15
+ }
16
+ export const MemoryService = {
17
+ createWithoutRuntime(config, options = {}) {
18
+ return new DefaultMemoryService(config, options);
19
+ },
20
+ };
21
+ class DefaultMemoryService {
22
+ config;
23
+ lcmStore;
24
+ memoryStore;
25
+ embeddingProvider;
26
+ indexer;
27
+ segmentManager;
28
+ contextTransformer;
29
+ ambientInjector;
30
+ diaryWatchDebounceMs;
31
+ diaryWatcher;
32
+ diaryWatchTimers = new Map();
33
+ constructor(config, options = {}) {
34
+ this.config = config;
35
+ this.lcmStore = LcmStore.open(config);
36
+ this.memoryStore = MemoryIndexStore.open(config);
37
+ this.reconcileSharedIndex();
38
+ this.embeddingProvider = createEmbeddingProvider(config);
39
+ this.indexer = new ChunkIndexer({ store: this.memoryStore, embeddingProvider: this.embeddingProvider });
40
+ this.segmentManager = new LcmSegmentManager({
41
+ lcmStore: this.lcmStore,
42
+ memoryStore: this.memoryStore,
43
+ indexer: this.indexer,
44
+ newSessionRetainDepth: config.memory.lcm.newSessionRetainDepth,
45
+ onRotate: (sessionKey) => this.contextTransformer.invalidateSession(sessionKey),
46
+ });
47
+ this.contextTransformer = new LcmContextTransformer({
48
+ settings: config.memory.lcm,
49
+ lcmStore: this.lcmStore,
50
+ indexer: this.indexer,
51
+ summarizer: options.summarizer ?? new DefaultLcmSummarizer(config),
52
+ segmentManager: this.segmentManager,
53
+ now: options.now,
54
+ });
55
+ this.ambientInjector = new AmbientDiaryInjector({
56
+ store: this.memoryStore,
57
+ embeddingProvider: this.embeddingProvider,
58
+ enabled: config.memory.ambient.enabled,
59
+ topK: config.memory.ambient.topK,
60
+ minQueryLength: config.memory.ambient.minQueryLength,
61
+ throttleSeconds: config.memory.ambient.throttleSeconds,
62
+ weightSimilarity: config.memory.ambient.weightSimilarity,
63
+ weightValence: config.memory.ambient.weightValence,
64
+ weightRecency: config.memory.ambient.weightRecency,
65
+ weightIntensity: config.memory.ambient.weightIntensity,
66
+ });
67
+ this.diaryWatchDebounceMs = options.diaryWatchDebounceMs ?? 3000;
68
+ }
69
+ async indexDiaries() {
70
+ await indexAllDiaryFiles({ config: this.config, indexer: this.indexer, store: this.memoryStore });
71
+ }
72
+ watchDiaries() {
73
+ if (this.diaryWatcher)
74
+ return;
75
+ try {
76
+ this.diaryWatcher = watch(this.config.memory.diariesDir, { persistent: true }, (_eventType, filename) => {
77
+ if (!filename)
78
+ return;
79
+ this.scheduleDiaryIndex(String(filename));
80
+ });
81
+ this.scheduleDiaryCatchUpIndex();
82
+ }
83
+ catch (error) {
84
+ if (isEnoent(error)) {
85
+ console.info(`diary watcher found no diary directory at ${this.config.memory.diariesDir}; disabled`);
86
+ return;
87
+ }
88
+ throw error;
89
+ }
90
+ this.diaryWatcher.on("error", (error) => {
91
+ console.error("diary watcher failed", error);
92
+ this.diaryWatcher?.close();
93
+ this.diaryWatcher = undefined;
94
+ });
95
+ }
96
+ memoryTools() {
97
+ return createMemoryTools({ store: this.memoryStore, embeddingProvider: this.embeddingProvider });
98
+ }
99
+ subscribeRuntime(runtime, sessionId) {
100
+ return this.segmentManager.subscribeRuntime(runtime, sessionId);
101
+ }
102
+ async transformContext(messages, signal, options = {}) {
103
+ const compacted = await this.contextTransformer.transformLcmContext(messages, signal, options);
104
+ if (options.skipAmbient)
105
+ return compacted;
106
+ return this.ambientInjector.inject(compacted, signal, options.sessionKey ?? options.sessionId ?? "default");
107
+ }
108
+ async serviceCompactionDebt(sessionKey) {
109
+ await this.contextTransformer.serviceCompactionDebt(sessionKey);
110
+ }
111
+ close() {
112
+ this.diaryWatcher?.close();
113
+ this.diaryWatcher = undefined;
114
+ for (const timer of this.diaryWatchTimers.values())
115
+ clearTimeout(timer);
116
+ this.diaryWatchTimers.clear();
117
+ this.memoryStore.close();
118
+ this.lcmStore.close();
119
+ }
120
+ async flush() {
121
+ await this.segmentManager.flush();
122
+ }
123
+ stats() {
124
+ return { projectionFailures: this.segmentManager.stats().projectionFailures };
125
+ }
126
+ reconcileSharedIndex() {
127
+ this.memoryStore.reconcileSources((source) => {
128
+ if (source.corpus === "lcm_record") {
129
+ const id = parseIndexSourceId(source.sourceId, "lcm_record");
130
+ return id !== null && this.lcmStore.getRecord(id) !== null;
131
+ }
132
+ if (source.corpus === "lcm_summary") {
133
+ const id = parseIndexSourceId(source.sourceId, "lcm_summary");
134
+ return id !== null && this.lcmStore.getSummary(id) !== null;
135
+ }
136
+ return true;
137
+ });
138
+ }
139
+ scheduleDiaryIndex(filename) {
140
+ const sourceId = basename(filename);
141
+ if (!DIARY_INDEX_FILE_RE.test(sourceId))
142
+ return;
143
+ const existing = this.diaryWatchTimers.get(sourceId);
144
+ if (existing)
145
+ clearTimeout(existing);
146
+ const timer = setTimeout(() => {
147
+ this.diaryWatchTimers.delete(sourceId);
148
+ void this.indexDiarySource(sourceId);
149
+ }, this.diaryWatchDebounceMs);
150
+ this.diaryWatchTimers.set(sourceId, timer);
151
+ }
152
+ scheduleDiaryCatchUpIndex() {
153
+ const timerKey = "__all__";
154
+ const existing = this.diaryWatchTimers.get(timerKey);
155
+ if (existing)
156
+ clearTimeout(existing);
157
+ const timer = setTimeout(() => {
158
+ this.diaryWatchTimers.delete(timerKey);
159
+ void this.indexDiaries().catch((error) => console.error("diary watcher catch-up indexing failed", error));
160
+ }, this.diaryWatchDebounceMs);
161
+ this.diaryWatchTimers.set(timerKey, timer);
162
+ }
163
+ async indexDiarySource(sourceId) {
164
+ const path = resolve(this.config.memory.diariesDir, sourceId);
165
+ try {
166
+ const result = await indexDiaryFile({
167
+ config: this.config,
168
+ indexer: this.indexer,
169
+ store: this.memoryStore,
170
+ path,
171
+ });
172
+ if ("skipped" in result)
173
+ return;
174
+ if (process.env.DEBUG === "memory-index") {
175
+ console.error(JSON.stringify({
176
+ event: "diary_index_file",
177
+ sourceId,
178
+ chunks: result.result.ids.length,
179
+ embedded: result.result.embedded,
180
+ reused: result.result.reused,
181
+ }));
182
+ }
183
+ }
184
+ catch (error) {
185
+ if (isEnoent(error)) {
186
+ await removeDiaryFileIndex({ config: this.config, indexer: this.indexer, path });
187
+ return;
188
+ }
189
+ console.error(`diary index failed: ${path}`, error);
190
+ }
191
+ }
192
+ }
193
+ function parseIndexSourceId(value, prefix) {
194
+ if (!value?.startsWith(`${prefix}:`))
195
+ return null;
196
+ const id = Number(value.slice(prefix.length + 1));
197
+ return Number.isInteger(id) && id > 0 ? id : null;
198
+ }
199
+ function isEnoent(error) {
200
+ return !!error && typeof error === "object" && "code" in error && error.code === "ENOENT";
201
+ }
202
+ export const __memoryServiceTest = __ambientDiaryInjectorTest;