@remnic/core 9.3.637 → 9.3.638

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 (78) hide show
  1. package/dist/access-cli.js +11 -11
  2. package/dist/access-http.d.ts +2 -2
  3. package/dist/access-http.js +8 -6
  4. package/dist/access-mcp.d.ts +2 -2
  5. package/dist/access-mcp.js +7 -5
  6. package/dist/{access-service-GVOE5U3C.d.ts → access-service-DvEvBv9_.d.ts} +2 -1
  7. package/dist/access-service.d.ts +2 -2
  8. package/dist/access-service.js +6 -4
  9. package/dist/bootstrap.d.ts +1 -1
  10. package/dist/causal-behavior.js +2 -2
  11. package/dist/causal-chain.js +2 -2
  12. package/dist/causal-consolidation.js +2 -2
  13. package/dist/causal-retrieval.js +2 -2
  14. package/dist/causal-trajectory-graph.js +1 -1
  15. package/dist/causal-trajectory.js +1 -1
  16. package/dist/{chunk-3XNMN242.js → chunk-2JIYTTQJ.js} +3 -3
  17. package/dist/{chunk-HZVIYZYN.js → chunk-7KRYJUTB.js} +6 -6
  18. package/dist/{chunk-2IT7WFYE.js → chunk-FWIROLS6.js} +2 -1
  19. package/dist/chunk-FWIROLS6.js.map +1 -0
  20. package/dist/{chunk-QJGTSCJN.js → chunk-I3YAA4BI.js} +8 -8
  21. package/dist/{chunk-A6D7A2FW.js → chunk-IHG27THY.js} +4 -4
  22. package/dist/{chunk-3EJ5OOZ3.js → chunk-KXTLEB4L.js} +2 -2
  23. package/dist/{chunk-XM6M6DHG.js → chunk-MNDFVT4F.js} +195 -6
  24. package/dist/chunk-MNDFVT4F.js.map +1 -0
  25. package/dist/{chunk-7V2SGZ3H.js → chunk-NE566K4E.js} +2 -2
  26. package/dist/{chunk-NFNQ7TWF.js → chunk-OSPVAP56.js} +3 -3
  27. package/dist/{chunk-UXFOGILU.js → chunk-QDAWVW75.js} +1 -1
  28. package/dist/{chunk-TQOU3VAT.js → chunk-SSOMTUCA.js} +1 -1
  29. package/dist/{chunk-TFICNXXU.js → chunk-TJRV4MWX.js} +990 -252
  30. package/dist/chunk-TJRV4MWX.js.map +1 -0
  31. package/dist/{chunk-BK35NOJL.js → chunk-U2YPUWRB.js} +3 -3
  32. package/dist/{chunk-EWC6W6AB.js → chunk-UXA5L2DZ.js} +2 -2
  33. package/dist/{chunk-XL7FK7PJ.js → chunk-Y56J7CXW.js} +33 -16
  34. package/dist/{chunk-XL7FK7PJ.js.map → chunk-Y56J7CXW.js.map} +1 -1
  35. package/dist/{cli-B-fHk59D.d.ts → cli-BJBKNWIS.d.ts} +2 -2
  36. package/dist/cli.d.ts +3 -3
  37. package/dist/cli.js +12 -11
  38. package/dist/compounding/engine.js +1 -1
  39. package/dist/explicit-capture.d.ts +1 -1
  40. package/dist/{graph-edge-decay-5ZOK7ZNO.js → graph-edge-decay-PUFNHOBS.js} +2 -2
  41. package/dist/graph-snapshot.js +2 -2
  42. package/dist/graph.d.ts +3 -0
  43. package/dist/graph.js +1 -1
  44. package/dist/index.d.ts +4 -4
  45. package/dist/index.js +16 -16
  46. package/dist/mcp-memory-inspector-app.d.ts +2 -2
  47. package/dist/operator-toolkit.js +2 -2
  48. package/dist/{orchestrator-CoqytbK_.d.ts → orchestrator-ibTTeoUa.d.ts} +9 -0
  49. package/dist/orchestrator.d.ts +1 -1
  50. package/dist/orchestrator.js +9 -9
  51. package/dist/recall-state.d.ts +2 -0
  52. package/dist/recall-state.js +1 -1
  53. package/dist/resume-bundles.js +3 -3
  54. package/dist/schemas.d.ts +22 -22
  55. package/dist/summarizer.js +2 -2
  56. package/dist/transcript.js +2 -2
  57. package/dist/transfer/types.d.ts +12 -12
  58. package/package.json +1 -1
  59. package/src/access-service-namespace.test.ts +740 -0
  60. package/src/access-service.ts +222 -3
  61. package/src/graph.ts +39 -17
  62. package/src/orchestrator.ts +1286 -331
  63. package/src/recall-state.ts +3 -0
  64. package/dist/chunk-2IT7WFYE.js.map +0 -1
  65. package/dist/chunk-TFICNXXU.js.map +0 -1
  66. package/dist/chunk-XM6M6DHG.js.map +0 -1
  67. /package/dist/{chunk-3XNMN242.js.map → chunk-2JIYTTQJ.js.map} +0 -0
  68. /package/dist/{chunk-HZVIYZYN.js.map → chunk-7KRYJUTB.js.map} +0 -0
  69. /package/dist/{chunk-QJGTSCJN.js.map → chunk-I3YAA4BI.js.map} +0 -0
  70. /package/dist/{chunk-A6D7A2FW.js.map → chunk-IHG27THY.js.map} +0 -0
  71. /package/dist/{chunk-3EJ5OOZ3.js.map → chunk-KXTLEB4L.js.map} +0 -0
  72. /package/dist/{chunk-7V2SGZ3H.js.map → chunk-NE566K4E.js.map} +0 -0
  73. /package/dist/{chunk-NFNQ7TWF.js.map → chunk-OSPVAP56.js.map} +0 -0
  74. /package/dist/{chunk-UXFOGILU.js.map → chunk-QDAWVW75.js.map} +0 -0
  75. /package/dist/{chunk-TQOU3VAT.js.map → chunk-SSOMTUCA.js.map} +0 -0
  76. /package/dist/{chunk-BK35NOJL.js.map → chunk-U2YPUWRB.js.map} +0 -0
  77. /package/dist/{chunk-EWC6W6AB.js.map → chunk-UXA5L2DZ.js.map} +0 -0
  78. /package/dist/{graph-edge-decay-5ZOK7ZNO.js.map → graph-edge-decay-PUFNHOBS.js.map} +0 -0
@@ -54,7 +54,10 @@ import {
54
54
  } from "./memory-lifecycle-ledger-utils.js";
55
55
  import { getMemoryProjectionPath } from "./memory-projection-store.js";
56
56
  import { canReadNamespace, canWriteNamespace, defaultNamespaceForPrincipal, recallNamespacesForPrincipal, resolvePrincipal } from "./namespaces/principal.js";
57
+ import { namespaceIdentityFromToken } from "./namespaces/identity.js";
57
58
  import { namespaceCollectionName } from "./namespaces/search.js";
59
+ import { SecureStoreLockedError } from "./secure-store/index.js";
60
+ import { isPathInsideStorageRoot } from "./storage-paths.js";
58
61
  import type { LastRecallSnapshot } from "./recall-state.js";
59
62
  import type {
60
63
  GraphRecallSnapshot,
@@ -168,6 +171,52 @@ import { formatProfileTraceAscii } from "./profiling.js";
168
171
 
169
172
  export class EngramAccessInputError extends Error {}
170
173
 
174
+ function qmdCollectionPathParts(resultPath: string): {
175
+ collection: string;
176
+ relativePath: string;
177
+ } | null {
178
+ if (!resultPath || nodePath.isAbsolute(resultPath)) return null;
179
+ const normalized = resultPath.replace(/\\/g, "/").replace(/^\/+/, "");
180
+ const slashIndex = normalized.indexOf("/");
181
+ if (slashIndex <= 0 || slashIndex >= normalized.length - 1) return null;
182
+ const collection = normalized.slice(0, slashIndex);
183
+ if (/^\d{4}-\d{2}-\d{2}$/.test(collection)) return null;
184
+ return {
185
+ collection,
186
+ relativePath: normalized.slice(slashIndex + 1),
187
+ };
188
+ }
189
+
190
+ function qmdResultPathCandidates(
191
+ storageDir: string,
192
+ resultPath: string,
193
+ ): string[] {
194
+ const candidates = new Set<string>();
195
+ const storageRoot = nodePath.resolve(storageDir);
196
+ const addCandidate = (candidate: string) => {
197
+ const resolved = nodePath.resolve(candidate);
198
+ if (isPathInsideStorageRoot(storageRoot, resolved)) {
199
+ candidates.add(resolved);
200
+ }
201
+ };
202
+ const addRelativeCandidates = (relativePath: string) => {
203
+ const normalized = relativePath.replace(/\\/g, "/").replace(/^\/+/, "");
204
+ if (!normalized) return;
205
+ addCandidate(nodePath.join(storageRoot, normalized));
206
+ if (/^\d{4}-\d{2}-\d{2}\//.test(normalized)) {
207
+ addCandidate(nodePath.join(storageRoot, "facts", normalized));
208
+ }
209
+ };
210
+
211
+ if (nodePath.isAbsolute(resultPath)) {
212
+ addCandidate(resultPath);
213
+ } else {
214
+ addRelativeCandidates(resultPath);
215
+ }
216
+
217
+ return [...candidates];
218
+ }
219
+
171
220
  type AccessProfilingReportRequest = {
172
221
  format?: string;
173
222
  limit?: number;
@@ -1437,6 +1486,7 @@ export class EngramAccessService {
1437
1486
  queryLen: options.query.length,
1438
1487
  memoryIds,
1439
1488
  namespace,
1489
+ recallNamespaces: [namespace],
1440
1490
  traceId: options.snapshot.traceId,
1441
1491
  plannerMode: options.normalizedMode,
1442
1492
  requestedMode:
@@ -1500,8 +1550,124 @@ export class EngramAccessService {
1500
1550
  const namespace = snapshot.namespace ? this.resolveNamespace(snapshot.namespace) : this.orchestrator.config.defaultNamespace;
1501
1551
  const storage = await this.orchestrator.getStorage(namespace);
1502
1552
  const storageDir = storage.dir;
1553
+ const recallNamespaces = Array.from(
1554
+ new Set(
1555
+ [
1556
+ namespace,
1557
+ ...(Array.isArray(snapshot.recallNamespaces)
1558
+ ? snapshot.recallNamespaces.map((ns) => this.resolveNamespace(ns))
1559
+ : []),
1560
+ this.orchestrator.config.defaultNamespace,
1561
+ this.orchestrator.config.sharedNamespace,
1562
+ ...(this.orchestrator.config.namespacePolicies ?? []).map((p) => p.name),
1563
+ ].filter((ns): ns is string => typeof ns === "string" && ns.length > 0),
1564
+ ),
1565
+ );
1503
1566
  const results: EngramAccessMemorySummary[] = [];
1504
1567
  const seen = new Set<string>();
1568
+ const collectionNamespaceFromPrefix = (collectionPrefix: string): string | null => {
1569
+ const baseCollection = this.orchestrator.config.qmdCollection;
1570
+ if (collectionPrefix === baseCollection) return this.orchestrator.config.defaultNamespace;
1571
+ const namespaceSuffix = collectionPrefix.startsWith(`${baseCollection}--`)
1572
+ ? collectionPrefix.slice(baseCollection.length + 2)
1573
+ : "";
1574
+ if (!namespaceSuffix) return null;
1575
+
1576
+ const decoded = namespaceIdentityFromToken(namespaceSuffix);
1577
+ if (decoded !== null) return decoded || this.orchestrator.config.defaultNamespace;
1578
+ if (namespaceSuffix.startsWith("ns--")) {
1579
+ const legacyNamespace = namespaceSuffix.slice("ns--".length).trim();
1580
+ return legacyNamespace || null;
1581
+ }
1582
+ return null;
1583
+ };
1584
+ const readResultPath = async (
1585
+ memoryPath: string,
1586
+ ): Promise<{ memory: MemoryFile; baseDir: string } | null> => {
1587
+ const parts = qmdCollectionPathParts(memoryPath);
1588
+ const coldCollection =
1589
+ this.orchestrator.config.qmdColdCollection ?? "openclaw-engram-cold";
1590
+ if (parts && parts.collection === coldCollection) {
1591
+ const storages: Array<{ storage: StorageManager; dir: string }> = [];
1592
+ const seenStorageDirs = new Set<string>();
1593
+ const addStorage = (candidateStorage: StorageManager): void => {
1594
+ const candidateDir = nodePath.resolve(candidateStorage.dir);
1595
+ if (seenStorageDirs.has(candidateDir)) return;
1596
+ seenStorageDirs.add(candidateDir);
1597
+ storages.push({ storage: candidateStorage, dir: candidateDir });
1598
+ };
1599
+ addStorage(storage);
1600
+ for (const recallNamespace of recallNamespaces) {
1601
+ try {
1602
+ addStorage(await this.orchestrator.getStorage(recallNamespace));
1603
+ } catch {
1604
+ continue;
1605
+ }
1606
+ }
1607
+ for (const candidateStorage of storages) {
1608
+ try {
1609
+ const coldRoot = nodePath.join(candidateStorage.dir, "cold");
1610
+ for (const candidatePath of qmdResultPathCandidates(
1611
+ coldRoot,
1612
+ parts.relativePath,
1613
+ )) {
1614
+ const memory =
1615
+ await candidateStorage.storage.readMemoryByPath(candidatePath);
1616
+ if (memory) return { memory, baseDir: candidateStorage.dir };
1617
+ }
1618
+ } catch (err) {
1619
+ if (err instanceof SecureStoreLockedError) throw err;
1620
+ continue;
1621
+ }
1622
+ }
1623
+ return null;
1624
+ }
1625
+
1626
+ const collectionNamespace = parts
1627
+ ? collectionNamespaceFromPrefix(parts.collection)
1628
+ : null;
1629
+
1630
+ if (parts && collectionNamespace) {
1631
+ try {
1632
+ const collectionStorage =
1633
+ await this.orchestrator.getStorage(collectionNamespace);
1634
+ for (const candidate of qmdResultPathCandidates(
1635
+ collectionStorage.dir,
1636
+ parts.relativePath,
1637
+ )) {
1638
+ const memory = await collectionStorage.readMemoryByPath(candidate);
1639
+ if (memory) return { memory, baseDir: collectionStorage.dir };
1640
+ }
1641
+ return null;
1642
+ } catch (err) {
1643
+ if (err instanceof SecureStoreLockedError) throw err;
1644
+ return null;
1645
+ }
1646
+ }
1647
+
1648
+ if (nodePath.isAbsolute(memoryPath)) {
1649
+ const ownerStorage = await this.storageForAbsoluteRecallPath(
1650
+ memoryPath,
1651
+ namespace,
1652
+ recallNamespaces,
1653
+ );
1654
+ if (!ownerStorage) return null;
1655
+ for (const candidate of qmdResultPathCandidates(
1656
+ ownerStorage.dir,
1657
+ memoryPath,
1658
+ )) {
1659
+ const memory = await ownerStorage.storage.readMemoryByPath(candidate);
1660
+ if (memory) return { memory, baseDir: ownerStorage.dir };
1661
+ }
1662
+ return null;
1663
+ }
1664
+
1665
+ for (const candidate of qmdResultPathCandidates(storageDir, memoryPath)) {
1666
+ const memory = await storage.readMemoryByPath(candidate);
1667
+ if (memory) return { memory, baseDir: storageDir };
1668
+ }
1669
+ return null;
1670
+ };
1505
1671
 
1506
1672
  // Pre-fetch raw excerpts once when `disclosure === "raw"` so we don't
1507
1673
  // hit the LCM archive per-result (issue #677 PR 2/4). Excerpts are
@@ -1519,13 +1685,14 @@ export class EngramAccessService {
1519
1685
 
1520
1686
  for (const memoryPath of snapshot.resultPaths ?? []) {
1521
1687
  if (!memoryPath || seen.has(memoryPath)) continue;
1522
- const memory = await storage.readMemoryByPath(memoryPath);
1523
- if (!memory) continue;
1688
+ const resolved = await readResultPath(memoryPath);
1689
+ if (!resolved) continue;
1690
+ const { memory, baseDir } = resolved;
1524
1691
  seen.add(memoryPath);
1525
1692
  results.push(
1526
1693
  this.serializeMemorySummary(
1527
1694
  memory,
1528
- storageDir,
1695
+ baseDir,
1529
1696
  disclosure,
1530
1697
  // Attach the (possibly empty) raw excerpts to the first raw
1531
1698
  // result; subsequent results do not duplicate the array.
@@ -1552,6 +1719,58 @@ export class EngramAccessService {
1552
1719
  return results;
1553
1720
  }
1554
1721
 
1722
+ private async storageForAbsoluteRecallPath(
1723
+ memoryPath: string,
1724
+ primaryNamespace: string,
1725
+ recallNamespaces: readonly string[] = [],
1726
+ ): Promise<{ storage: StorageManager; dir: string } | null> {
1727
+ const resolvedPath = nodePath.resolve(memoryPath);
1728
+ const memoryRoot = nodePath.resolve(this.orchestrator.config.memoryDir);
1729
+ const namespacesRoot = nodePath.join(memoryRoot, "namespaces");
1730
+ const configuredNamespaces = new Set<string>();
1731
+ configuredNamespaces.add(primaryNamespace);
1732
+ for (const namespace of recallNamespaces) {
1733
+ configuredNamespaces.add(namespace);
1734
+ }
1735
+ configuredNamespaces.add(this.orchestrator.config.defaultNamespace);
1736
+ configuredNamespaces.add(this.orchestrator.config.sharedNamespace);
1737
+ if (isPathInsideStorageRoot(namespacesRoot, resolvedPath)) {
1738
+ const relativeToNamespaces = nodePath.relative(namespacesRoot, resolvedPath);
1739
+ const [namespaceSegment] = relativeToNamespaces.split(/[\\/]/);
1740
+ if (namespaceSegment) {
1741
+ configuredNamespaces.add(
1742
+ namespaceIdentityFromToken(namespaceSegment) ?? namespaceSegment,
1743
+ );
1744
+ }
1745
+ }
1746
+ for (const policy of this.orchestrator.config.namespacePolicies ?? []) {
1747
+ configuredNamespaces.add(policy.name);
1748
+ }
1749
+
1750
+ const matches: Array<{ storage: StorageManager; dir: string }> = [];
1751
+ for (const ns of configuredNamespaces) {
1752
+ if (!ns) continue;
1753
+ let candidateStorage: StorageManager;
1754
+ try {
1755
+ candidateStorage = await this.orchestrator.getStorage(ns);
1756
+ } catch {
1757
+ continue;
1758
+ }
1759
+ const candidateRoot = nodePath.resolve(candidateStorage.dir);
1760
+ if (!isPathInsideStorageRoot(candidateRoot, resolvedPath)) continue;
1761
+ if (
1762
+ candidateRoot === memoryRoot &&
1763
+ isPathInsideStorageRoot(namespacesRoot, resolvedPath)
1764
+ ) {
1765
+ continue;
1766
+ }
1767
+ matches.push({ storage: candidateStorage, dir: candidateRoot });
1768
+ }
1769
+
1770
+ matches.sort((a, b) => b.dir.length - a.dir.length);
1771
+ return matches[0] ?? null;
1772
+ }
1773
+
1555
1774
  /**
1556
1775
  * Fetch raw transcript excerpts from the LCM archive for `disclosure ===
1557
1776
  * "raw"` recalls (issue #677 PR 2/4). Returns `null` for non-raw recall
package/src/graph.ts CHANGED
@@ -517,6 +517,8 @@ export class GraphIndex {
517
517
  * Default `false` (floor from config is applied).
518
518
  */
519
519
  includeLowConfidence?: boolean;
520
+ /** Absolute deadline in ms-since-epoch for post-retrieval assembly. */
521
+ deadlineAtMs?: number;
520
522
  }
521
523
  ): Promise<
522
524
  Array<{
@@ -545,15 +547,22 @@ export class GraphIndex {
545
547
  const floor =
546
548
  opts?.includeLowConfidence === true ? 0 : clampConfidenceFloor(this.cfg.graphTraversalConfidenceFloor);
547
549
  const iterations = clampPageRankIterations(this.cfg.graphTraversalPageRankIterations);
550
+ const deadlineAtMs = opts?.deadlineAtMs;
551
+ const deadlineExpired = (): boolean =>
552
+ typeof deadlineAtMs === "number" && Date.now() >= deadlineAtMs;
548
553
 
549
554
  try {
555
+ if (deadlineExpired()) return [];
550
556
  const allEdges = await this.loadEdgesCached();
557
+ if (deadlineExpired()) return [];
551
558
 
552
559
  // Build adjacency index: from → edges, to → edges (bidirectional for entity/time, directional for causal).
553
560
  // Edges below the confidence floor are pruned at index time so neither
554
561
  // direct activation nor downstream BFS expansion can re-introduce them.
555
562
  const adj = new Map<string, GraphEdge[]>();
556
- for (const edge of allEdges) {
563
+ for (let i = 0; i < allEdges.length; i += 1) {
564
+ if ((i & 1023) === 0 && deadlineExpired()) return [];
565
+ const edge = allEdges[i];
557
566
  const conf = readEdgeConfidence(edge);
558
567
  if (conf < floor) continue;
559
568
  if (!adj.has(edge.from)) adj.set(edge.from, []);
@@ -583,13 +592,28 @@ export class GraphIndex {
583
592
  frontier.set(`${seed}\0${seed}`, { node: seed, seed, activation: 1 });
584
593
  reachedBySeed.set(seed, new Set([seed]));
585
594
  }
595
+ const finalizeScores = () =>
596
+ Array.from(scores.entries())
597
+ .map(([p, score]) => ({
598
+ path: p,
599
+ score,
600
+ seed: provenance.get(p)?.seed ?? "",
601
+ hopDepth: provenance.get(p)?.hopDepth ?? 0,
602
+ decayedWeight: provenance.get(p)?.decayedWeight ?? 0,
603
+ graphType: provenance.get(p)?.graphType ?? "entity",
604
+ edgeConfidence: provenance.get(p)?.edgeConfidence ?? 1,
605
+ }))
606
+ .sort((a, b) => b.score - a.score);
586
607
 
587
608
  for (let hop = 0; hop < steps && frontier.size > 0; hop++) {
609
+ if (deadlineExpired()) return finalizeScores();
588
610
  const nextFrontier = new Map<string, { node: string; seed: string; activation: number }>();
589
611
 
590
612
  for (const { node, seed: sourceSeed, activation } of frontier.values()) {
591
613
  const edges = adj.get(node) ?? [];
592
- for (const edge of edges) {
614
+ for (let i = 0; i < edges.length; i += 1) {
615
+ if ((i & 1023) === 0 && deadlineExpired()) return finalizeScores();
616
+ const edge = edges[i];
593
617
  const neighbor = edge.to === node ? edge.from : edge.to;
594
618
  const conf = readEdgeConfidence(edge);
595
619
  // Defense in depth: the adjacency build already drops sub-floor
@@ -645,15 +669,17 @@ export class GraphIndex {
645
669
  // edges, weighted by edge confidence. Damping is fixed at the
646
670
  // canonical 0.85 so the ranking stays comparable across queries;
647
671
  // the `iterations` knob bounds compute, not behavior shape.
648
- if (iterations > 0 && scores.size > 1) {
672
+ if (!deadlineExpired() && iterations > 0 && scores.size > 1) {
649
673
  applyPageRankRefinement(scores, adj, {
650
674
  iterations,
651
675
  floor,
652
676
  damping: 0.85,
677
+ deadlineAtMs,
653
678
  });
654
679
  }
655
680
 
656
681
  // Apply lateral inhibition if enabled (Synapse-inspired competitive suppression)
682
+ if (deadlineExpired()) return finalizeScores();
657
683
  if (this.cfg.graphLateralInhibitionEnabled && scores.size > 1) {
658
684
  const inhibited = applyLateralInhibition(scores, {
659
685
  beta: this.cfg.graphLateralInhibitionBeta,
@@ -664,17 +690,7 @@ export class GraphIndex {
664
690
  }
665
691
  }
666
692
 
667
- return Array.from(scores.entries())
668
- .map(([p, score]) => ({
669
- path: p,
670
- score,
671
- seed: provenance.get(p)?.seed ?? "",
672
- hopDepth: provenance.get(p)?.hopDepth ?? 0,
673
- decayedWeight: provenance.get(p)?.decayedWeight ?? 0,
674
- graphType: provenance.get(p)?.graphType ?? "entity",
675
- edgeConfidence: provenance.get(p)?.edgeConfidence ?? 1,
676
- }))
677
- .sort((a, b) => b.score - a.score);
693
+ return finalizeScores();
678
694
  } catch (err) {
679
695
  const { log } = await import("./logger.js");
680
696
  log.warn(`[graph] spreadingActivation error: ${err}`);
@@ -725,11 +741,13 @@ export function clampPageRankIterations(raw: unknown): number {
725
741
  export function applyPageRankRefinement(
726
742
  scores: Map<string, number>,
727
743
  adj: Map<string, GraphEdge[]>,
728
- opts: { iterations: number; floor: number; damping: number }
744
+ opts: { iterations: number; floor: number; damping: number; deadlineAtMs?: number }
729
745
  ): void {
730
- const { iterations, floor, damping } = opts;
746
+ const { iterations, floor, damping, deadlineAtMs } = opts;
731
747
  if (iterations <= 0 || scores.size === 0) return;
732
748
  const safeDamping = Math.min(1, Math.max(0, damping));
749
+ const deadlineExpired = (): boolean =>
750
+ typeof deadlineAtMs === "number" && Date.now() >= deadlineAtMs;
733
751
 
734
752
  // Pre-compute confidence-weighted out-edge totals for normalization.
735
753
  // Done once per refinement, not per iteration, since adjacency is
@@ -748,6 +766,7 @@ export function applyPageRankRefinement(
748
766
  };
749
767
  const outboundTotal = new Map<string, number>();
750
768
  for (const [node, edges] of adj.entries()) {
769
+ if (deadlineExpired()) return;
751
770
  if (!scores.has(node)) continue; // only candidate nodes redistribute
752
771
  let sum = 0;
753
772
  for (const edge of edges) {
@@ -758,6 +777,7 @@ export function applyPageRankRefinement(
758
777
  }
759
778
 
760
779
  for (let i = 0; i < iterations; i += 1) {
780
+ if (deadlineExpired()) return;
761
781
  const next = new Map<string, number>();
762
782
  // Teleport / damping floor: every node retains `(1 - damping) * score`
763
783
  // of its current activation so dangling nodes do not bleed to zero.
@@ -776,7 +796,9 @@ export function applyPageRankRefinement(
776
796
  next.set(node, (next.get(node) ?? 0) + safeDamping * score);
777
797
  continue;
778
798
  }
779
- for (const edge of outEdges) {
799
+ for (let edgeIndex = 0; edgeIndex < outEdges.length; edgeIndex += 1) {
800
+ if ((edgeIndex & 1023) === 0 && deadlineExpired()) return;
801
+ const edge = outEdges[edgeIndex];
780
802
  if (!eligible(edge, node)) continue;
781
803
  const conf = readEdgeConfidence(edge);
782
804
  const neighbor = edge.to === node ? edge.from : edge.to;