@remnic/core 9.3.651 → 9.3.653

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 (90) hide show
  1. package/dist/access-cli.js +10 -10
  2. package/dist/access-http.d.ts +3 -2
  3. package/dist/access-http.js +12 -12
  4. package/dist/access-mcp.d.ts +3 -2
  5. package/dist/access-mcp.js +11 -11
  6. package/dist/access-schema.d.ts +3 -0
  7. package/dist/access-schema.js +1 -1
  8. package/dist/{access-service-DIZRHQ7Q.d.ts → access-service-CdJFd3_b.d.ts} +23 -2
  9. package/dist/access-service.d.ts +3 -2
  10. package/dist/access-service.js +9 -9
  11. package/dist/bootstrap.d.ts +2 -1
  12. package/dist/briefing.js +1 -1
  13. package/dist/{chunk-QT4THOLT.js → chunk-2DGQLOOM.js} +1 -1
  14. package/dist/chunk-2DGQLOOM.js.map +1 -0
  15. package/dist/{chunk-FOVPSMGI.js → chunk-7WEB3FLJ.js} +2 -2
  16. package/dist/{chunk-IJHLC5CH.js → chunk-BNFRL6QW.js} +31 -21
  17. package/dist/{chunk-IJHLC5CH.js.map → chunk-BNFRL6QW.js.map} +1 -1
  18. package/dist/{chunk-SLYD3AH4.js → chunk-E3J6O6N7.js} +177 -5
  19. package/dist/chunk-E3J6O6N7.js.map +1 -0
  20. package/dist/{chunk-MF32AL7N.js → chunk-EW52H5EM.js} +4 -4
  21. package/dist/{chunk-WJK75OCH.js → chunk-GI45G4BK.js} +2 -2
  22. package/dist/{chunk-76QTEJ2Q.js → chunk-JBHXMCYN.js} +2 -2
  23. package/dist/{chunk-4PTKFBST.js → chunk-JVRPJ7D4.js} +126 -26
  24. package/dist/chunk-JVRPJ7D4.js.map +1 -0
  25. package/dist/{chunk-TQUWNX7C.js → chunk-JX2RINDR.js} +2 -2
  26. package/dist/{chunk-RSS2KWN6.js → chunk-NOBL7OUP.js} +14 -6
  27. package/dist/chunk-NOBL7OUP.js.map +1 -0
  28. package/dist/{chunk-I4COC5XW.js → chunk-PYWNNF2I.js} +47 -9
  29. package/dist/chunk-PYWNNF2I.js.map +1 -0
  30. package/dist/{chunk-RKN5J4RO.js → chunk-QQHIQ7JD.js} +6 -6
  31. package/dist/{chunk-5WSDHTBO.js → chunk-SPMZZUEJ.js} +8 -5
  32. package/dist/chunk-SPMZZUEJ.js.map +1 -0
  33. package/dist/{chunk-4HYSMH7D.js → chunk-UAU5U5ML.js} +3 -2
  34. package/dist/chunk-UAU5U5ML.js.map +1 -0
  35. package/dist/{chunk-LFTLXOFX.js → chunk-WTI35CVJ.js} +2 -2
  36. package/dist/{chunk-6UKL6IXM.js → chunk-YM3LR4LS.js} +5 -5
  37. package/dist/{cli-BG4ybtJr.d.ts → cli-DDo7Qgs-.d.ts} +2 -2
  38. package/dist/cli.d.ts +4 -3
  39. package/dist/cli.js +15 -15
  40. package/dist/explicit-capture.d.ts +2 -1
  41. package/dist/index.d.ts +5 -4
  42. package/dist/index.js +16 -16
  43. package/dist/mcp-memory-inspector-app.d.ts +3 -2
  44. package/dist/namespaces/migrate.js +8 -8
  45. package/dist/namespaces/search.d.ts +18 -1
  46. package/dist/namespaces/search.js +7 -7
  47. package/dist/operator-toolkit.js +9 -9
  48. package/dist/{orchestrator-CX-oqwJq.d.ts → orchestrator-8fTZsa0y.d.ts} +2 -0
  49. package/dist/orchestrator.d.ts +2 -1
  50. package/dist/orchestrator.js +9 -9
  51. package/dist/qmd.d.ts +2 -1
  52. package/dist/qmd.js +2 -2
  53. package/dist/schemas.d.ts +22 -22
  54. package/dist/search/factory.js +6 -6
  55. package/dist/search/index.js +6 -6
  56. package/dist/search/lancedb-backend.js +2 -2
  57. package/dist/search/meilisearch-backend.js +2 -2
  58. package/dist/search/orama-backend.js +2 -2
  59. package/dist/search/port.d.ts +6 -0
  60. package/dist/search/port.js +1 -1
  61. package/dist/transfer/types.d.ts +12 -12
  62. package/package.json +1 -1
  63. package/src/access-mcp.test.ts +70 -1
  64. package/src/access-mcp.ts +12 -2
  65. package/src/access-schema.ts +1 -0
  66. package/src/access-service-health.test.ts +402 -0
  67. package/src/access-service.ts +274 -2
  68. package/src/briefing.test.ts +70 -0
  69. package/src/briefing.ts +30 -20
  70. package/src/namespaces/search.test.ts +258 -3
  71. package/src/namespaces/search.ts +184 -30
  72. package/src/orchestrator.ts +11 -1
  73. package/src/qmd.test.ts +102 -0
  74. package/src/qmd.ts +54 -7
  75. package/src/search/port.ts +6 -0
  76. package/dist/chunk-4HYSMH7D.js.map +0 -1
  77. package/dist/chunk-4PTKFBST.js.map +0 -1
  78. package/dist/chunk-5WSDHTBO.js.map +0 -1
  79. package/dist/chunk-I4COC5XW.js.map +0 -1
  80. package/dist/chunk-QT4THOLT.js.map +0 -1
  81. package/dist/chunk-RSS2KWN6.js.map +0 -1
  82. package/dist/chunk-SLYD3AH4.js.map +0 -1
  83. /package/dist/{chunk-FOVPSMGI.js.map → chunk-7WEB3FLJ.js.map} +0 -0
  84. /package/dist/{chunk-MF32AL7N.js.map → chunk-EW52H5EM.js.map} +0 -0
  85. /package/dist/{chunk-WJK75OCH.js.map → chunk-GI45G4BK.js.map} +0 -0
  86. /package/dist/{chunk-76QTEJ2Q.js.map → chunk-JBHXMCYN.js.map} +0 -0
  87. /package/dist/{chunk-TQUWNX7C.js.map → chunk-JX2RINDR.js.map} +0 -0
  88. /package/dist/{chunk-RKN5J4RO.js.map → chunk-QQHIQ7JD.js.map} +0 -0
  89. /package/dist/{chunk-LFTLXOFX.js.map → chunk-WTI35CVJ.js.map} +0 -0
  90. /package/dist/{chunk-6UKL6IXM.js.map → chunk-YM3LR4LS.js.map} +0 -0
@@ -38,6 +38,7 @@ import {
38
38
  runMemoryGovernance,
39
39
  } from "./maintenance/memory-governance.js";
40
40
  import { runProcedureMining } from "./procedural/procedure-miner.js";
41
+ import { displayErrorDetail } from "./runtime/better-sqlite.js";
41
42
  import type { PatternReinforcementResult } from "./maintenance/pattern-reinforcement.js";
42
43
  import type { LiveConnectorsRunSummary } from "./live-connectors-runner.js";
43
44
  import type { WearablesService } from "./wearables/service.js";
@@ -279,10 +280,32 @@ export interface EngramAccessHealthResponse {
279
280
  defaultNamespace: string;
280
281
  searchBackend: string;
281
282
  qmdEnabled: boolean;
283
+ qmd: EngramAccessQmdHealthResponse;
282
284
  nativeKnowledgeEnabled: boolean;
283
285
  projectionAvailable: boolean;
284
286
  }
285
287
 
288
+ export type EngramAccessQmdCollectionState =
289
+ | "present"
290
+ | "missing"
291
+ | "unknown"
292
+ | "skipped";
293
+
294
+ export interface EngramAccessQmdHealthResponse {
295
+ enabled: boolean;
296
+ active: boolean;
297
+ degraded: boolean;
298
+ mode: "cli" | "daemon" | "fallback" | "disabled" | "not-selected";
299
+ collection: string;
300
+ collectionState: EngramAccessQmdCollectionState;
301
+ installedVersion: string | null;
302
+ supportedVersion: string | null;
303
+ supported: boolean | null;
304
+ upgradeAvailable: boolean | null;
305
+ doctorAvailable: boolean | null;
306
+ debugStatus: string;
307
+ }
308
+
286
309
  export interface EngramAccessRecallRequest {
287
310
  query: string;
288
311
  sessionKey?: string;
@@ -2412,6 +2435,8 @@ export class EngramAccessService {
2412
2435
  async health(namespace?: string): Promise<EngramAccessHealthResponse> {
2413
2436
  const resolvedNamespace = this.resolveNamespace(namespace);
2414
2437
  const storage = await this.orchestrator.getStorage(resolvedNamespace);
2438
+ const searchBackend = this.orchestrator.config.searchBackend ?? "qmd";
2439
+ const qmdEnabled = this.orchestrator.config.qmdEnabled === true;
2415
2440
  let projectionAvailable = false;
2416
2441
  try {
2417
2442
  await stat(getMemoryProjectionPath(storage.dir));
@@ -2425,13 +2450,260 @@ export class EngramAccessService {
2425
2450
  memoryDir: storage.dir,
2426
2451
  namespacesEnabled: this.orchestrator.config.namespacesEnabled === true,
2427
2452
  defaultNamespace: this.orchestrator.config.defaultNamespace,
2428
- searchBackend: this.orchestrator.config.searchBackend ?? "qmd",
2429
- qmdEnabled: this.orchestrator.config.qmdEnabled === true,
2453
+ searchBackend,
2454
+ qmdEnabled,
2455
+ qmd: await this.qmdHealth(
2456
+ searchBackend,
2457
+ qmdEnabled,
2458
+ resolvedNamespace,
2459
+ this.qmdCollectionForHealth(resolvedNamespace, storage.dir),
2460
+ ),
2430
2461
  nativeKnowledgeEnabled: this.orchestrator.config.nativeKnowledge?.enabled === true,
2431
2462
  projectionAvailable,
2432
2463
  };
2433
2464
  }
2434
2465
 
2466
+ private qmdCollectionForHealth(namespace: string, storageDir: string): string {
2467
+ if (this.orchestrator.config.namespacesEnabled !== true) {
2468
+ return this.orchestrator.config.qmdCollection;
2469
+ }
2470
+
2471
+ const useLegacyDefaultCollection =
2472
+ namespace === this.orchestrator.config.defaultNamespace &&
2473
+ storageDir === this.orchestrator.config.memoryDir;
2474
+ return namespaceCollectionName(this.orchestrator.config.qmdCollection, namespace, {
2475
+ defaultNamespace: this.orchestrator.config.defaultNamespace,
2476
+ useLegacyDefaultCollection,
2477
+ });
2478
+ }
2479
+
2480
+ private async qmdHealth(
2481
+ searchBackend: string,
2482
+ qmdEnabled: boolean,
2483
+ namespace: string,
2484
+ collection: string,
2485
+ ): Promise<EngramAccessQmdHealthResponse> {
2486
+ if (searchBackend !== "qmd" || !qmdEnabled) {
2487
+ return {
2488
+ enabled: qmdEnabled,
2489
+ active: false,
2490
+ degraded: false,
2491
+ mode: searchBackend !== "qmd" ? "not-selected" : "disabled",
2492
+ collection,
2493
+ collectionState: "skipped",
2494
+ installedVersion: null,
2495
+ supportedVersion: null,
2496
+ supported: null,
2497
+ upgradeAvailable: null,
2498
+ doctorAvailable: null,
2499
+ debugStatus: searchBackend !== "qmd" ? `backend=${searchBackend}` : "backend=disabled",
2500
+ };
2501
+ }
2502
+
2503
+ if (this.orchestrator.config.namespacesEnabled === true) {
2504
+ const namespaceHealth = await this.namespaceQmdHealth(searchBackend, qmdEnabled, namespace, collection);
2505
+ if (namespaceHealth) return namespaceHealth;
2506
+ }
2507
+
2508
+ const qmd = this.orchestrator.qmd;
2509
+ if (!qmd) {
2510
+ return {
2511
+ enabled: true,
2512
+ active: false,
2513
+ degraded: true,
2514
+ mode: "fallback",
2515
+ collection,
2516
+ collectionState: "unknown",
2517
+ installedVersion: null,
2518
+ supportedVersion: null,
2519
+ supported: null,
2520
+ upgradeAvailable: null,
2521
+ doctorAvailable: null,
2522
+ debugStatus: "backend=unavailable",
2523
+ };
2524
+ }
2525
+ const diagnosticAvailable = await this.qmdProbeAvailable(searchBackend, qmdEnabled);
2526
+ const operationalAvailable = diagnosticAvailable || qmd.isAvailable();
2527
+ const collectionState = diagnosticAvailable
2528
+ ? await this.qmdCollectionState(searchBackend, qmdEnabled, collection)
2529
+ : "unknown";
2530
+ const active = operationalAvailable && collectionState !== "missing";
2531
+ const degraded =
2532
+ searchBackend === "qmd" &&
2533
+ qmdEnabled &&
2534
+ (!active || !diagnosticAvailable || collectionState === "unknown");
2535
+ const debugStatus = qmd.debugStatus();
2536
+ const versionStatus =
2537
+ "getVersionStatus" in qmd && typeof qmd.getVersionStatus === "function"
2538
+ ? qmd.getVersionStatus()
2539
+ : null;
2540
+ const daemonMode =
2541
+ "isDaemonMode" in qmd && typeof qmd.isDaemonMode === "function"
2542
+ ? qmd.isDaemonMode() === true
2543
+ : false;
2544
+ const mode =
2545
+ searchBackend !== "qmd"
2546
+ ? "not-selected"
2547
+ : !qmdEnabled
2548
+ ? "disabled"
2549
+ : !active
2550
+ ? "fallback"
2551
+ : daemonMode
2552
+ ? "daemon"
2553
+ : "cli";
2554
+
2555
+ return {
2556
+ enabled: qmdEnabled,
2557
+ active,
2558
+ degraded,
2559
+ mode,
2560
+ collection,
2561
+ collectionState,
2562
+ installedVersion: versionStatus?.installedVersion ?? null,
2563
+ supportedVersion: versionStatus?.supportedVersion ?? null,
2564
+ supported: versionStatus?.supported ?? null,
2565
+ upgradeAvailable: versionStatus?.upgradeAvailable ?? null,
2566
+ doctorAvailable: versionStatus?.capabilities?.doctor ?? null,
2567
+ debugStatus,
2568
+ };
2569
+ }
2570
+
2571
+ private async namespaceQmdHealth(
2572
+ searchBackend: string,
2573
+ qmdEnabled: boolean,
2574
+ namespace: string,
2575
+ fallbackCollection: string,
2576
+ ): Promise<EngramAccessQmdHealthResponse | null> {
2577
+ if (searchBackend !== "qmd" || !qmdEnabled) return null;
2578
+ const searchHealthForNamespace = (
2579
+ this.orchestrator as Orchestrator & {
2580
+ searchHealthForNamespace?: (
2581
+ namespace: string,
2582
+ execution?: { signal?: AbortSignal },
2583
+ ) => Promise<{
2584
+ collection: string;
2585
+ available: boolean;
2586
+ collectionState: EngramAccessQmdCollectionState;
2587
+ debugStatus: string;
2588
+ installedVersion: string | null;
2589
+ supportedVersion: string | null;
2590
+ supported: boolean | null;
2591
+ upgradeAvailable: boolean | null;
2592
+ doctorAvailable: boolean | null;
2593
+ daemonMode: boolean | null;
2594
+ }>;
2595
+ }
2596
+ ).searchHealthForNamespace;
2597
+ if (typeof searchHealthForNamespace !== "function") return null;
2598
+
2599
+ const controller = new AbortController();
2600
+ const timer = setTimeout(() => controller.abort(), 2_000);
2601
+ timer.unref?.();
2602
+ try {
2603
+ const health = await searchHealthForNamespace.call(this.orchestrator, namespace, {
2604
+ signal: controller.signal,
2605
+ });
2606
+ const active = health.available && health.collectionState !== "missing";
2607
+ const degraded = !active || health.collectionState === "unknown";
2608
+ const mode =
2609
+ !active
2610
+ ? "fallback"
2611
+ : health.daemonMode === true
2612
+ ? "daemon"
2613
+ : "cli";
2614
+
2615
+ return {
2616
+ enabled: true,
2617
+ active,
2618
+ degraded,
2619
+ mode,
2620
+ collection: health.collection || fallbackCollection,
2621
+ collectionState: health.collectionState,
2622
+ installedVersion: health.installedVersion,
2623
+ supportedVersion: health.supportedVersion,
2624
+ supported: health.supported,
2625
+ upgradeAvailable: health.upgradeAvailable,
2626
+ doctorAvailable: health.doctorAvailable,
2627
+ debugStatus: health.debugStatus,
2628
+ };
2629
+ } catch (error) {
2630
+ const detail = displayErrorDetail(error) || "unknown";
2631
+ return {
2632
+ enabled: true,
2633
+ active: false,
2634
+ degraded: true,
2635
+ mode: "fallback",
2636
+ collection: fallbackCollection,
2637
+ collectionState: "unknown",
2638
+ installedVersion: null,
2639
+ supportedVersion: null,
2640
+ supported: null,
2641
+ upgradeAvailable: null,
2642
+ doctorAvailable: null,
2643
+ debugStatus: `backend=namespace-unavailable error=${detail}`,
2644
+ };
2645
+ } finally {
2646
+ clearTimeout(timer);
2647
+ }
2648
+ }
2649
+
2650
+ private async qmdCollectionState(
2651
+ searchBackend: string,
2652
+ qmdEnabled: boolean,
2653
+ collection: string,
2654
+ ): Promise<EngramAccessQmdCollectionState> {
2655
+ if (searchBackend !== "qmd" || !qmdEnabled) return "skipped";
2656
+ const qmd = this.orchestrator.qmd;
2657
+ if (!qmd.isAvailable()) return "unknown";
2658
+ if (!qmd.checkCollection) return "skipped";
2659
+
2660
+ const controller = new AbortController();
2661
+ const timer = setTimeout(() => controller.abort(), 2_000);
2662
+ timer.unref?.();
2663
+ try {
2664
+ return await qmd.checkCollection(collection, {
2665
+ signal: controller.signal,
2666
+ });
2667
+ } catch {
2668
+ return "unknown";
2669
+ } finally {
2670
+ clearTimeout(timer);
2671
+ }
2672
+ }
2673
+
2674
+ private async qmdProbeAvailable(
2675
+ searchBackend: string,
2676
+ qmdEnabled: boolean,
2677
+ ): Promise<boolean> {
2678
+ if (searchBackend !== "qmd" || !qmdEnabled) return false;
2679
+ const qmd = this.orchestrator.qmd;
2680
+ if (!qmd) return false;
2681
+
2682
+ const controller = new AbortController();
2683
+ const timer = setTimeout(() => controller.abort(), 2_000);
2684
+ timer.unref?.();
2685
+ try {
2686
+ return await new Promise<boolean>((resolve) => {
2687
+ const onAbort = () => {
2688
+ controller.signal.removeEventListener("abort", onAbort);
2689
+ resolve(false);
2690
+ };
2691
+ controller.signal.addEventListener("abort", onAbort, { once: true });
2692
+ const probe =
2693
+ typeof qmd.checkAvailability === "function"
2694
+ ? qmd.checkAvailability({ signal: controller.signal })
2695
+ : qmd.probe();
2696
+ probe
2697
+ .then(resolve, () => resolve(false))
2698
+ .finally(() => {
2699
+ controller.signal.removeEventListener("abort", onAbort);
2700
+ });
2701
+ });
2702
+ } finally {
2703
+ clearTimeout(timer);
2704
+ }
2705
+ }
2706
+
2435
2707
  async actionConfidence(
2436
2708
  request: EngramAccessActionConfidenceRequest = {},
2437
2709
  ): Promise<EngramAccessActionConfidenceResponse> {
@@ -16,6 +16,7 @@ import {
16
16
  focusMatchesMemory,
17
17
  buildActiveThreads,
18
18
  buildBriefing,
19
+ buildChainFollowupGenerator,
19
20
  BRIEFING_FOLLOWUP_DEFAULT_MODEL,
20
21
  } from "./briefing.js";
21
22
  import type {
@@ -976,6 +977,75 @@ test("buildBriefing: unrelated LLM errors still produce generic message (no fals
976
977
  );
977
978
  });
978
979
 
980
+ test("buildChainFollowupGenerator accepts markdown-fenced followup JSON", async () => {
981
+ const generator = buildChainFollowupGenerator({
982
+ chatCompletion: async () => ({
983
+ content: [
984
+ "```json",
985
+ "{",
986
+ ' "followups": [',
987
+ ' { "text": "Check the launch plan", "rationale": "Open commitment found" }',
988
+ " ]",
989
+ "}",
990
+ "```",
991
+ ].join("\n"),
992
+ }),
993
+ });
994
+
995
+ const followups = await generator({
996
+ sections: {
997
+ activeThreads: [],
998
+ recentEntities: [],
999
+ openCommitments: [{ id: "commit-1", kind: "commitment", text: "Finish launch plan" }],
1000
+ suggestedFollowups: [],
1001
+ },
1002
+ windowLabel: "today",
1003
+ maxFollowups: 3,
1004
+ });
1005
+
1006
+ assert.deepEqual(followups, [
1007
+ {
1008
+ text: "Check the launch plan",
1009
+ rationale: "Open commitment found",
1010
+ },
1011
+ ]);
1012
+ });
1013
+
1014
+ test("buildChainFollowupGenerator skips parseable non-followup JSON candidates", async () => {
1015
+ const generator = buildChainFollowupGenerator({
1016
+ chatCompletion: async () => ({
1017
+ content: [
1018
+ '{ "metadata": "not followups" }',
1019
+ "```json",
1020
+ "{",
1021
+ ' "followups": [',
1022
+ ' { "text": "Review the launch plan", "rationale": "Actual follow-up block" }',
1023
+ " ]",
1024
+ "}",
1025
+ "```",
1026
+ ].join("\n"),
1027
+ }),
1028
+ });
1029
+
1030
+ const followups = await generator({
1031
+ sections: {
1032
+ activeThreads: [],
1033
+ recentEntities: [],
1034
+ openCommitments: [{ id: "commit-1", kind: "commitment", text: "Finish launch plan" }],
1035
+ suggestedFollowups: [],
1036
+ },
1037
+ windowLabel: "today",
1038
+ maxFollowups: 3,
1039
+ });
1040
+
1041
+ assert.deepEqual(followups, [
1042
+ {
1043
+ text: "Review the launch plan",
1044
+ rationale: "Actual follow-up block",
1045
+ },
1046
+ ]);
1047
+ });
1048
+
979
1049
  // ──────────────────────────────────────────────────────────────────────────
980
1050
  // Round 8 Finding UXE4: buildActiveThreads must recompute reason from newer memory
981
1051
  // ──────────────────────────────────────────────────────────────────────────
package/src/briefing.ts CHANGED
@@ -1298,27 +1298,37 @@ function parseFollowupResponse(raw: string, max: number): BriefingFollowup[] {
1298
1298
  // JSON.parse throws on invalid JSON — let the caller catch it so the outer
1299
1299
  // try/catch in buildBriefing can set followupsUnavailableReason rather than
1300
1300
  // silently returning an empty array that masks the parse failure.
1301
- const parsed = JSON.parse(raw) as unknown;
1302
- if (!parsed || typeof parsed !== "object") {
1303
- throw new Error(`LLM returned non-object JSON: ${typeof parsed}`);
1304
- }
1305
- const arr = (parsed as { followups?: unknown }).followups;
1306
- if (!Array.isArray(arr)) {
1307
- throw new Error(`LLM response missing "followups" array`);
1308
- }
1309
- const out: BriefingFollowup[] = [];
1310
- for (const entry of arr) {
1311
- if (!entry || typeof entry !== "object") continue;
1312
- const text = (entry as Record<string, unknown>).text;
1313
- if (typeof text !== "string" || text.trim().length === 0) continue;
1314
- const rationale = (entry as Record<string, unknown>).rationale;
1315
- out.push({
1316
- text: text.trim(),
1317
- rationale: typeof rationale === "string" ? rationale.trim() : undefined,
1318
- });
1319
- if (out.length >= max) break;
1301
+ const candidates = extractJsonCandidates(raw);
1302
+ let lastError: unknown;
1303
+ for (const candidate of candidates) {
1304
+ try {
1305
+ const parsed = JSON.parse(candidate) as unknown;
1306
+ if (!parsed || typeof parsed !== "object") {
1307
+ throw new Error(`LLM returned non-object JSON: ${typeof parsed}`);
1308
+ }
1309
+ const arr = (parsed as { followups?: unknown }).followups;
1310
+ if (!Array.isArray(arr)) {
1311
+ throw new Error(`LLM response missing "followups" array`);
1312
+ }
1313
+ const out: BriefingFollowup[] = [];
1314
+ for (const entry of arr) {
1315
+ if (!entry || typeof entry !== "object") continue;
1316
+ const text = (entry as Record<string, unknown>).text;
1317
+ if (typeof text !== "string" || text.trim().length === 0) continue;
1318
+ const rationale = (entry as Record<string, unknown>).rationale;
1319
+ out.push({
1320
+ text: text.trim(),
1321
+ rationale: typeof rationale === "string" ? rationale.trim() : undefined,
1322
+ });
1323
+ if (out.length >= max) break;
1324
+ }
1325
+ return out;
1326
+ } catch (err) {
1327
+ lastError = err;
1328
+ }
1320
1329
  }
1321
- return out;
1330
+ if (lastError) throw lastError;
1331
+ throw new Error("LLM response contained no JSON candidates");
1322
1332
  }
1323
1333
 
1324
1334
  function stringifyError(err: unknown): string {