@qearlyao/familiar 0.2.4 → 0.3.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 (83) hide show
  1. package/README.md +4 -0
  2. package/config.example.toml +2 -2
  3. package/dist/agent/payload-normalizers.js +52 -0
  4. package/dist/agent/session-helpers.js +86 -0
  5. package/dist/agent/tool-descriptions.js +4 -0
  6. package/dist/agent/tools.js +30 -0
  7. package/dist/agent/transcript-log.js +93 -0
  8. package/dist/agent/types.js +1 -0
  9. package/dist/agent-core.js +82 -0
  10. package/dist/agent-work-queue.js +55 -0
  11. package/dist/agent.js +91 -322
  12. package/dist/browser-tools.js +80 -28
  13. package/dist/chat-log.js +15 -3
  14. package/dist/cli.js +36 -6
  15. package/dist/config/enums.js +35 -0
  16. package/dist/config/interpolate.js +15 -0
  17. package/dist/config/model-refs.js +11 -0
  18. package/dist/config/readers.js +116 -0
  19. package/dist/config/sections.js +113 -0
  20. package/dist/config/types.js +1 -0
  21. package/dist/config-registry.js +26 -7
  22. package/dist/config.js +8 -271
  23. package/dist/discord/channel.js +32 -0
  24. package/dist/discord/chunking.js +163 -0
  25. package/dist/discord/client.js +44 -0
  26. package/dist/discord/commands.js +181 -0
  27. package/dist/discord/inbound.js +44 -0
  28. package/dist/discord/send.js +106 -0
  29. package/dist/discord/turn.js +55 -0
  30. package/dist/discord.js +266 -1186
  31. package/dist/ids.js +11 -0
  32. package/dist/image-gen.js +90 -10
  33. package/dist/index.js +1 -0
  34. package/dist/memory/index/store.js +21 -17
  35. package/dist/memory/index/vector-codec.js +2 -2
  36. package/dist/memory/lcm/context-transformer.js +6 -2
  37. package/dist/memory/lcm/segment-manager.js +6 -2
  38. package/dist/memory/lcm/store/index-ids.js +6 -0
  39. package/dist/memory/lcm/store/inserts.js +31 -0
  40. package/dist/memory/lcm/store/normalizers.js +91 -0
  41. package/dist/memory/lcm/store/row-mappers.js +114 -0
  42. package/dist/memory/lcm/store/row-types.js +1 -0
  43. package/dist/memory/lcm/store/serialization.js +37 -0
  44. package/dist/memory/lcm/store/snapshots.js +73 -0
  45. package/dist/memory/lcm/store.js +20 -360
  46. package/dist/owner-identity.js +29 -0
  47. package/dist/runtime-manager.js +51 -0
  48. package/dist/runtime.js +89 -41
  49. package/dist/scheduler-runner.js +243 -0
  50. package/dist/scheduler.js +1 -1
  51. package/dist/service.js +1 -0
  52. package/dist/settings.js +3 -0
  53. package/dist/util/fs.js +1 -1
  54. package/dist/web/event-hub.js +246 -0
  55. package/dist/{web-http.js → web/http.js} +19 -5
  56. package/dist/web/memes.js +25 -0
  57. package/dist/web/messages.js +345 -0
  58. package/dist/web/multipart.js +80 -0
  59. package/dist/web/payloads.js +34 -0
  60. package/dist/{web-static.js → web/static.js} +19 -14
  61. package/dist/web/stream.js +69 -0
  62. package/dist/web-tools/cache.js +42 -0
  63. package/dist/web-tools/config.js +16 -0
  64. package/dist/web-tools/fetch-providers.js +119 -0
  65. package/dist/web-tools/format.js +88 -0
  66. package/dist/web-tools/http.js +81 -0
  67. package/dist/web-tools/routing.js +29 -0
  68. package/dist/web-tools/safety.js +73 -0
  69. package/dist/web-tools/search-providers.js +277 -0
  70. package/dist/web-tools/types.js +54 -0
  71. package/dist/web-tools/util.js +23 -0
  72. package/dist/web-tools.js +9 -798
  73. package/dist/web.js +416 -984
  74. package/npm-shrinkwrap.json +242 -201
  75. package/package.json +4 -4
  76. package/web/dist/assets/index-CSkxUQCr.js +63 -0
  77. package/web/dist/assets/index-DllM6RqL.css +2 -0
  78. package/web/dist/index.html +6 -3
  79. package/web/dist/assets/index-B23WT77N.js +0 -63
  80. package/web/dist/assets/index-D3MotFzN.css +0 -2
  81. /package/dist/{web-auth.js → web/auth.js} +0 -0
  82. /package/dist/{web-events.js → web/events.js} +0 -0
  83. /package/dist/{web-types.js → web/types.js} +0 -0
@@ -1,10 +1,17 @@
1
- import { createHash } from "node:crypto";
2
1
  import { mkdirSync } from "node:fs";
3
2
  import { dirname, resolve } from "node:path";
4
3
  import Database from "better-sqlite3";
5
4
  import { normalizeFtsMatchQuery } from "../index/fts-query.js";
6
5
  import { runInTransaction } from "../util.js";
7
6
  import { readMeta, runLcmMigrations } from "./schema.js";
7
+ import { lcmRecordIndexSourceId, lcmSummaryIndexSourceId } from "./store/index-ids.js";
8
+ import { insertRecordPrepared, insertSummaryPrepared } from "./store/inserts.js";
9
+ import { computeLcmRecordKey, dedupeSummaryParentIds, normalizeRecordInput, normalizeSummaryInput, } from "./store/normalizers.js";
10
+ import { contextItemFromRow, recordFromRow, segmentFromRow, sessionStateFromRow, summaryFromRow, summarySourceFromRow, } from "./store/row-mappers.js";
11
+ import { jsonOrNull } from "./store/serialization.js";
12
+ import { buildSummaryParentSnapshot, buildSummarySnapshot } from "./store/snapshots.js";
13
+ export { lcmRecordIndexSourceId, lcmSummaryIndexSourceId } from "./store/index-ids.js";
14
+ export { computeLcmRecordKey } from "./store/normalizers.js";
8
15
  export class LcmStore {
9
16
  db;
10
17
  ownsDb;
@@ -134,7 +141,7 @@ export class LcmStore {
134
141
  throw new Error("LCM summary depth must be an integer >= 0");
135
142
  }
136
143
  const normalized = normalizeSummaryInput(input);
137
- const runInsert = () => runSummaryInsertTransaction(this, normalized, (id, sources, parents) => {
144
+ const runInsert = () => this.runSummaryInsertTransaction(normalized, (id, sources, parents) => {
138
145
  this.insertSummarySources(id, sources);
139
146
  this.insertSummaryParents(id, parents);
140
147
  });
@@ -294,15 +301,14 @@ export class LcmStore {
294
301
  summary_id, ord, record_id, source_summary_id, source_ref, snapshot_json
295
302
  ) VALUES (?, ?, ?, ?, ?, ?)`);
296
303
  for (const [index, source] of sources.entries()) {
297
- // source_summary_id is legacy advisory lineage only. The canonical
298
- // parent edge is lcm_summary_parents, and this column is scheduled for removal.
304
+ // source_summary_id is advisory only; the canonical parent edge is lcm_summary_parents.
299
305
  insert.run(summaryId, index, source.recordId ?? null, null, source.sourceRef ?? null, jsonOrNull(source.snapshot ?? null));
300
306
  }
301
307
  }
302
308
  insertSummaryParents(summaryId, parents) {
303
309
  if (parents.length === 0)
304
310
  return;
305
- const uniqueParents = dedupeNumbers(parents);
311
+ const uniqueParents = dedupeSummaryParentIds(parents);
306
312
  const existingRows = this.db
307
313
  .prepare(`SELECT id FROM lcm_summaries WHERE id IN (${uniqueParents.map(() => "?").join(",")})`)
308
314
  .all(...uniqueParents);
@@ -315,6 +321,15 @@ export class LcmStore {
315
321
  for (const [index, parentId] of uniqueParents.entries())
316
322
  insert.run(summaryId, parentId, index);
317
323
  }
324
+ runSummaryInsertTransaction(normalized, insertEdges) {
325
+ this.ensureSegment({ id: normalized.segmentId });
326
+ const existing = this.db
327
+ .prepare("SELECT id FROM lcm_summaries WHERE summary_key = ?")
328
+ .get(normalized.summaryKey);
329
+ if (existing)
330
+ return existing.id;
331
+ return insertSummaryPrepared(this.db, normalized, insertEdges);
332
+ }
318
333
  summaryParentMap(summaryIds) {
319
334
  const map = new Map();
320
335
  if (summaryIds.length === 0)
@@ -368,358 +383,3 @@ export class LcmStore {
368
383
  }
369
384
  }
370
385
  }
371
- export function lcmRecordIndexSourceId(id) {
372
- return `lcm_record:${id}`;
373
- }
374
- export function lcmSummaryIndexSourceId(id) {
375
- return `lcm_summary:${id}`;
376
- }
377
- export function computeLcmRecordKey(input) {
378
- const text = (input.text ?? "").trim();
379
- if (!text && input.kind !== "boundary")
380
- throw new Error("LCM record text must not be empty");
381
- const parts = input.parts?.length ? input.parts : null;
382
- return stableHash({
383
- segmentId: input.segmentId,
384
- kind: input.kind,
385
- text: text || "Session boundary",
386
- parts,
387
- happenedAt: input.happenedAt ?? new Date().toISOString(),
388
- source: normalizeSource(input.source),
389
- });
390
- }
391
- function normalizeRecordInput(input) {
392
- const text = (input.text ?? "").trim();
393
- if (!text && input.kind !== "boundary")
394
- throw new Error("LCM record text must not be empty");
395
- const source = normalizeSource(input.source);
396
- const happenedAt = input.happenedAt ?? new Date().toISOString();
397
- const parts = input.parts?.length ? input.parts : null;
398
- const normalizedText = text || "Session boundary";
399
- return {
400
- segmentId: input.segmentId,
401
- kind: input.kind,
402
- text: normalizedText,
403
- parts,
404
- happenedAt,
405
- sessionId: input.sessionId ?? null,
406
- channelKey: input.channelKey ?? null,
407
- channelId: input.channelId ?? null,
408
- jobId: input.jobId ?? null,
409
- source,
410
- attachments: input.attachments?.length ? input.attachments : null,
411
- metadata: input.metadata ?? null,
412
- recordKey: computeLcmRecordKey({ ...input, happenedAt }),
413
- };
414
- }
415
- function insertRecordPrepared(db, normalized) {
416
- const inserted = db
417
- .prepare(`INSERT INTO lcm_records (
418
- record_key, segment_id, kind, text_full, happened_at, session_id, channel_key,
419
- channel_id, job_id, source_type, source_path, source_line, source_record_id,
420
- source_message_id, source_ref, attachments_json, metadata_json, parts_json
421
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
422
- .run(normalized.recordKey, normalized.segmentId, normalized.kind, normalized.text, normalized.happenedAt, normalized.sessionId, normalized.channelKey, normalized.channelId, normalized.jobId, normalized.source.sourceType, normalized.source.sourcePath ?? null, normalized.source.sourceLine ?? null, sourceRecordIdToString(normalized.source.sourceRecordId), normalized.source.sourceMessageId ?? null, normalized.source.sourceRef ?? null, jsonOrNull(normalized.attachments), jsonOrNull(normalized.metadata), jsonOrNull(normalized.parts));
423
- const id = Number(inserted.lastInsertRowid);
424
- if (normalized.kind !== "boundary") {
425
- db.prepare("INSERT INTO lcm_records_fts(rowid, text_full) VALUES (?, ?)").run(id, normalized.text);
426
- }
427
- const row = db.prepare("SELECT * FROM lcm_records WHERE id = ?").get(id);
428
- if (!row)
429
- throw new Error(`Failed to read inserted LCM record: ${id}`);
430
- return row;
431
- }
432
- function normalizeSummaryInput(input) {
433
- const text = (input.text ?? "").trim();
434
- const source = normalizeSource(input.source);
435
- const status = input.status ?? (text ? "ready" : "placeholder");
436
- const normalizedText = text || "";
437
- return {
438
- segmentId: input.segmentId,
439
- depth: input.depth,
440
- status,
441
- text: normalizedText,
442
- pinned: input.pinned ?? false,
443
- coversFromRecordId: input.coversFromRecordId ?? null,
444
- coversToRecordId: input.coversToRecordId ?? null,
445
- source,
446
- sourceItems: input.sourceItems ?? [],
447
- parents: input.parents ?? [],
448
- metadata: input.metadata ?? null,
449
- summaryKey: stableHash({
450
- segmentId: input.segmentId,
451
- depth: input.depth,
452
- status,
453
- text: normalizedText,
454
- coversFromRecordId: input.coversFromRecordId ?? null,
455
- coversToRecordId: input.coversToRecordId ?? null,
456
- source,
457
- parents: input.parents ?? [],
458
- }),
459
- };
460
- }
461
- function insertSummaryPrepared(db, normalized, insertEdges) {
462
- const inserted = db
463
- .prepare(`INSERT INTO lcm_summaries (
464
- summary_key, segment_id, depth, status, text_full, pinned,
465
- covers_from_record_id, covers_to_record_id, snapshot_json, source_type, source_path,
466
- source_line, source_record_id, source_message_id, source_ref, metadata_json
467
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
468
- .run(normalized.summaryKey, normalized.segmentId, normalized.depth, normalized.status, normalized.text, normalized.pinned ? 1 : 0, normalized.coversFromRecordId, normalized.coversToRecordId, null, normalized.source.sourceType, normalized.source.sourcePath ?? null, normalized.source.sourceLine ?? null, sourceRecordIdToString(normalized.source.sourceRecordId), normalized.source.sourceMessageId ?? null, normalized.source.sourceRef ?? null, jsonOrNull(normalized.metadata));
469
- const id = Number(inserted.lastInsertRowid);
470
- db.prepare("INSERT INTO lcm_summaries_fts(rowid, text_full) VALUES (?, ?)").run(id, normalized.text);
471
- insertEdges(id, normalized.sourceItems, normalized.parents);
472
- return id;
473
- }
474
- function runSummaryInsertTransaction(store, normalized, insertEdges) {
475
- store.ensureSegment({ id: normalized.segmentId });
476
- const existing = store.db.prepare("SELECT id FROM lcm_summaries WHERE summary_key = ?").get(normalized.summaryKey);
477
- if (existing)
478
- return existing.id;
479
- return insertSummaryPrepared(store.db, normalized, insertEdges);
480
- }
481
- function dedupeNumbers(values) {
482
- const seen = new Set();
483
- const result = [];
484
- for (const value of values) {
485
- if (!Number.isInteger(value) || value <= 0)
486
- throw new Error("LCM summary parents must be positive integer ids");
487
- if (seen.has(value))
488
- continue;
489
- seen.add(value);
490
- result.push(value);
491
- }
492
- return result;
493
- }
494
- function normalizeSource(source) {
495
- return {
496
- sourceType: source.sourceType,
497
- sourcePath: source.sourcePath ?? null,
498
- sourceLine: source.sourceLine ?? null,
499
- sourceRecordId: source.sourceRecordId ?? null,
500
- sourceMessageId: source.sourceMessageId ?? null,
501
- sourceRef: source.sourceRef ?? null,
502
- };
503
- }
504
- function stableHash(value) {
505
- return createHash("sha256").update(JSON.stringify(value)).digest("hex");
506
- }
507
- function sourceRecordIdToString(value) {
508
- if (value === null || value === undefined)
509
- return null;
510
- return String(value);
511
- }
512
- function jsonOrNull(value) {
513
- if (value === null || value === undefined)
514
- return null;
515
- return JSON.stringify(value);
516
- }
517
- function parseJsonObject(value) {
518
- if (!value)
519
- return null;
520
- try {
521
- const parsed = JSON.parse(value);
522
- return parsed && typeof parsed === "object" && !Array.isArray(parsed)
523
- ? parsed
524
- : null;
525
- }
526
- catch {
527
- return null;
528
- }
529
- }
530
- function parseJsonArray(value) {
531
- if (!value)
532
- return null;
533
- try {
534
- const parsed = JSON.parse(value);
535
- return Array.isArray(parsed) ? parsed : null;
536
- }
537
- catch {
538
- return null;
539
- }
540
- }
541
- function sourceFromRow(row) {
542
- return {
543
- sourceType: row.source_type,
544
- sourcePath: row.source_path,
545
- sourceLine: row.source_line,
546
- sourceRecordId: row.source_record_id,
547
- sourceMessageId: row.source_message_id,
548
- sourceRef: row.source_ref,
549
- };
550
- }
551
- function segmentFromRow(row) {
552
- return {
553
- id: row.id,
554
- status: row.status,
555
- sessionId: row.session_id,
556
- channelKey: row.channel_key,
557
- startedAt: row.started_at,
558
- closedAt: row.closed_at,
559
- rawPrunedAt: row.raw_pruned_at,
560
- boundarySource: parseJsonObject(row.boundary_source_json),
561
- metadata: parseJsonObject(row.metadata_json),
562
- createdAt: row.created_at,
563
- updatedAt: row.updated_at,
564
- };
565
- }
566
- function recordFromRow(row) {
567
- return {
568
- id: row.id,
569
- recordKey: row.record_key,
570
- segmentId: row.segment_id,
571
- kind: row.kind,
572
- text: row.text_full,
573
- parts: parseJsonArray(row.parts_json),
574
- happenedAt: row.happened_at,
575
- sessionId: row.session_id,
576
- channelKey: row.channel_key,
577
- channelId: row.channel_id,
578
- jobId: row.job_id,
579
- source: sourceFromRow(row),
580
- attachments: parseJsonArray(row.attachments_json),
581
- metadata: parseJsonObject(row.metadata_json),
582
- createdAt: row.created_at,
583
- updatedAt: row.updated_at,
584
- };
585
- }
586
- function summaryFromRow(row, parents = []) {
587
- return {
588
- id: row.id,
589
- summaryKey: row.summary_key,
590
- segmentId: row.segment_id,
591
- depth: row.depth,
592
- status: row.status,
593
- text: row.text_full,
594
- pinned: row.pinned === 1,
595
- coversFromRecordId: row.covers_from_record_id,
596
- coversToRecordId: row.covers_to_record_id,
597
- source: sourceFromRow(row),
598
- metadata: parseJsonObject(row.metadata_json),
599
- snapshot: parseJsonArray(row.snapshot_json),
600
- parents,
601
- createdAt: row.created_at,
602
- updatedAt: row.updated_at,
603
- };
604
- }
605
- function summarySourceFromRow(row) {
606
- return {
607
- summaryId: row.summary_id,
608
- ord: row.ord,
609
- recordId: row.record_id,
610
- sourceSummaryId: row.source_summary_id,
611
- sourceRef: row.source_ref,
612
- snapshot: parseJsonObject(row.snapshot_json),
613
- };
614
- }
615
- function contextItemFromRow(row) {
616
- if (row.item_type === "raw") {
617
- if (row.record_id === null)
618
- throw new Error(`Invalid raw LCM context item at ordinal ${row.ordinal}`);
619
- return {
620
- sessionKey: row.session_key,
621
- ordinal: row.ordinal,
622
- type: "raw",
623
- recordId: row.record_id,
624
- summaryId: null,
625
- fingerprint: row.fingerprint,
626
- happenedAt: row.happened_at,
627
- updatedAt: row.updated_at,
628
- };
629
- }
630
- if (row.item_type === "summary") {
631
- if (row.summary_id === null)
632
- throw new Error(`Invalid summary LCM context item at ordinal ${row.ordinal}`);
633
- return {
634
- sessionKey: row.session_key,
635
- ordinal: row.ordinal,
636
- type: "summary",
637
- recordId: null,
638
- summaryId: row.summary_id,
639
- fingerprint: row.fingerprint,
640
- happenedAt: row.happened_at,
641
- updatedAt: row.updated_at,
642
- };
643
- }
644
- throw new Error(`Unknown LCM context item type: ${row.item_type}`);
645
- }
646
- function sessionStateFromRow(row) {
647
- return {
648
- sessionKey: row.session_key,
649
- compactionDebt: row.compaction_debt,
650
- cacheTouchedAt: row.cache_touched_at,
651
- updatedAt: row.updated_at,
652
- };
653
- }
654
- const SUMMARY_SNAPSHOT_TEXT_LIMIT = 4 * 1024;
655
- const SUMMARY_SNAPSHOT_TRUNCATED_SUFFIX = "…[truncated]";
656
- function buildSummarySnapshot(db, summary) {
657
- const rows = db
658
- .prepare(`SELECT * FROM lcm_records
659
- WHERE segment_id = ?
660
- AND id BETWEEN ? AND ?
661
- ORDER BY happened_at, id`)
662
- .all(summary.segment_id, summary.covers_from_record_id, summary.covers_to_record_id);
663
- return rows.map(snapshotRecordFromRow);
664
- }
665
- function buildSummaryParentSnapshot(db, summaryId, visiting) {
666
- if (visiting.has(summaryId))
667
- throw new Error(`Cycle detected in LCM summary parents at ${summaryId}`);
668
- visiting.add(summaryId);
669
- const row = db.prepare("SELECT * FROM lcm_summaries WHERE id = ?").get(summaryId);
670
- if (!row)
671
- throw new Error(`LCM summary does not exist: ${summaryId}`);
672
- let snapshot = parseJsonArray(row.snapshot_json);
673
- if (!snapshot &&
674
- row.covers_from_record_id !== null &&
675
- row.covers_to_record_id !== null &&
676
- db
677
- .prepare("SELECT 1 FROM lcm_records WHERE segment_id = ? AND id BETWEEN ? AND ? LIMIT 1")
678
- .get(row.segment_id, row.covers_from_record_id, row.covers_to_record_id)) {
679
- snapshot = buildSummarySnapshot(db, row);
680
- }
681
- const parentRows = db
682
- .prepare(`SELECT parent_summary_id
683
- FROM lcm_summary_parents
684
- WHERE summary_id = ?
685
- ORDER BY ord, parent_summary_id`)
686
- .all(summaryId);
687
- const parents = parentRows.map((parent) => buildSummaryParentSnapshot(db, parent.parent_summary_id, visiting));
688
- visiting.delete(summaryId);
689
- return {
690
- summaryId: row.id,
691
- depth: row.depth,
692
- text: row.text_full,
693
- coversFromRecordId: row.covers_from_record_id,
694
- coversToRecordId: row.covers_to_record_id,
695
- snapshot,
696
- parents,
697
- };
698
- }
699
- function snapshotRecordFromRow(row) {
700
- const metadata = parseJsonObject(row.metadata_json);
701
- return {
702
- id: row.id,
703
- kind: row.kind,
704
- happened_at: row.happened_at,
705
- role: snapshotRole(row.kind, metadata),
706
- text: truncateSummarySnapshotText(row.text_full),
707
- parts: parseJsonArray(row.parts_json),
708
- attachments: parseJsonArray(row.attachments_json),
709
- };
710
- }
711
- function snapshotRole(kind, metadata) {
712
- if (typeof metadata?.role === "string" && metadata.role.trim())
713
- return metadata.role;
714
- if (kind === "user" || kind === "assistant")
715
- return kind;
716
- if (kind === "tool")
717
- return "tool";
718
- return null;
719
- }
720
- function truncateSummarySnapshotText(text) {
721
- if (text.length <= SUMMARY_SNAPSHOT_TEXT_LIMIT)
722
- return text;
723
- const retainedLength = Math.max(0, SUMMARY_SNAPSHOT_TEXT_LIMIT - SUMMARY_SNAPSHOT_TRUNCATED_SUFFIX.length);
724
- return `${text.slice(0, retainedLength)}${SUMMARY_SNAPSHOT_TRUNCATED_SUFFIX}`;
725
- }
@@ -0,0 +1,29 @@
1
+ import { resolve } from "node:path";
2
+ import { atomicWriteJson, readFileOrNull } from "./util/fs.js";
3
+ import { isRecord } from "./util/guards.js";
4
+ export function ownerIdentityPath(dataDir) {
5
+ return resolve(dataDir, "owner-identity.json");
6
+ }
7
+ export async function loadOwnerIdentity(dataDir) {
8
+ const raw = await readFileOrNull(ownerIdentityPath(dataDir), "utf8");
9
+ if (raw === null)
10
+ return null;
11
+ let parsed;
12
+ try {
13
+ parsed = JSON.parse(raw);
14
+ }
15
+ catch {
16
+ return null;
17
+ }
18
+ if (!isRecord(parsed))
19
+ return null;
20
+ const { botUserId, dmChannelId } = parsed;
21
+ if (typeof botUserId !== "string" || botUserId.length === 0)
22
+ return null;
23
+ if (typeof dmChannelId !== "string" || dmChannelId.length === 0)
24
+ return null;
25
+ return { botUserId, dmChannelId };
26
+ }
27
+ export async function saveOwnerIdentity(dataDir, identity) {
28
+ await atomicWriteJson(ownerIdentityPath(dataDir), identity);
29
+ }
@@ -0,0 +1,51 @@
1
+ import { chatChannelKey, createChatLog } from "./chat-log.js";
2
+ import { ConversationRuntime } from "./runtime.js";
3
+ export function createRuntimeManager(deps) {
4
+ const runtimes = new Map();
5
+ const openEntry = async (channel, channelKey) => {
6
+ const runtime = await ConversationRuntime.connect({
7
+ channelKey,
8
+ log: createChatLog(deps.config, channel),
9
+ ownerId: deps.config.discord.ownerId,
10
+ botUserId: deps.botUserId(),
11
+ });
12
+ const unsubscribe = deps.memoryService?.subscribeRuntime(runtime, runtime.channelKey);
13
+ const release = async () => {
14
+ unsubscribe?.();
15
+ await runtime.disconnect();
16
+ };
17
+ try {
18
+ await runtime.armAfterCurrentTail();
19
+ }
20
+ catch (error) {
21
+ await release();
22
+ throw error;
23
+ }
24
+ return { runtime, release };
25
+ };
26
+ return {
27
+ async getRuntimeForChannel(channel) {
28
+ const channelKey = chatChannelKey(channel);
29
+ const existing = runtimes.get(channelKey);
30
+ if (existing)
31
+ return (await existing).runtime;
32
+ const entryPromise = openEntry(channel, channelKey);
33
+ runtimes.set(channelKey, entryPromise);
34
+ try {
35
+ return (await entryPromise).runtime;
36
+ }
37
+ catch (error) {
38
+ runtimes.delete(channelKey);
39
+ throw error;
40
+ }
41
+ },
42
+ async peekRuntime(channelKey) {
43
+ return (await runtimes.get(channelKey)?.catch(() => undefined))?.runtime;
44
+ },
45
+ async disconnectAll() {
46
+ const entries = await Promise.all([...runtimes.values()].map((entry) => entry.catch(() => undefined)));
47
+ runtimes.clear();
48
+ await Promise.all(entries.flatMap((entry) => (entry ? [entry.release()] : [])));
49
+ },
50
+ };
51
+ }