@remnic/core 9.3.743 → 9.3.744

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.
@@ -108,6 +108,16 @@ import { RecallSectionCoordinator } from "./orchestration/recall-section-coordin
108
108
  import { QmdResultResolver, qmdCollectionPathParts, qmdResultPathCandidates } from "./orchestration/qmd-result-resolver.js";
109
109
  import { ContradictionLinkingCoordinator } from "./orchestration/contradiction-linking-coordinator.js";
110
110
  export { hasIdentityRecoveryIntent, resolveEffectiveIdentityInjectionMode } from "./orchestration/recall-result-formatter.js";
111
+ import {
112
+ GraphRecallCoordinator,
113
+ mergeGraphExpandedResults,
114
+ graphPathRelativeToStorage,
115
+ blendGraphExpandedRecallScore,
116
+ type GraphRecallRankedResult,
117
+ type GraphRecallShadowComparison,
118
+ } from "./orchestration/graph-recall-coordinator.js";
119
+ export { mergeGraphExpandedResults, graphPathRelativeToStorage, blendGraphExpandedRecallScore } from "./orchestration/graph-recall-coordinator.js";
120
+ export type { GraphRecallRankedResult, GraphRecallShadowComparison } from "./orchestration/graph-recall-coordinator.js";
111
121
  import {
112
122
  runLiveConnectorsOnce,
113
123
  type LiveConnectorsRunSummary,
@@ -506,21 +516,6 @@ export interface GraphRecallSnapshot {
506
516
  shadowComparison?: GraphRecallShadowComparison;
507
517
  }
508
518
 
509
- export interface GraphRecallRankedResult {
510
- path: string;
511
- score: number;
512
- docid?: string;
513
- sourceLabels: string[];
514
- }
515
-
516
- export interface GraphRecallShadowComparison {
517
- baselineCount: number;
518
- graphCount: number;
519
- overlapCount: number;
520
- overlapRatio: number;
521
- averageOverlapDelta: number;
522
- }
523
-
524
519
  export interface IntentDebugSnapshot {
525
520
  recordedAt: string;
526
521
  promptHash: string;
@@ -1343,63 +1338,6 @@ export function computeQmdHybridFetchLimit(
1343
1338
  const artifactHeadroom = Math.max(20, Math.max(0, maxArtifactRecall) * 8);
1344
1339
  return Math.min(400, cappedRecallLimit + artifactHeadroom);
1345
1340
  }
1346
-
1347
- export function mergeGraphExpandedResults(
1348
- primary: QmdSearchResult[],
1349
- expanded: QmdSearchResult[],
1350
- ): QmdSearchResult[] {
1351
- const mergedByPath = new Map<string, QmdSearchResult>();
1352
- for (const item of [...primary, ...expanded]) {
1353
- const prev = mergedByPath.get(item.path);
1354
- if (!prev) {
1355
- mergedByPath.set(item.path, item);
1356
- continue;
1357
- }
1358
- const better = item.score > prev.score ? item : prev;
1359
- const snippet = prev.snippet || item.snippet;
1360
- mergedByPath.set(item.path, { ...better, snippet });
1361
- }
1362
- return Array.from(mergedByPath.values());
1363
- }
1364
-
1365
- export function graphPathRelativeToStorage(
1366
- storageDir: string,
1367
- candidatePath: string,
1368
- ): string | null {
1369
- const absolutePath = path.isAbsolute(candidatePath)
1370
- ? candidatePath
1371
- : path.resolve(storageDir, candidatePath);
1372
- const rel = path.relative(storageDir, absolutePath);
1373
- if (!rel || rel === ".") return null;
1374
- if (rel.startsWith("..")) return null;
1375
- return rel.split(path.sep).join("/");
1376
- }
1377
-
1378
- function normalizeGraphActivationScore(score: number): number {
1379
- const bounded = Number.isFinite(score) && score > 0 ? score : 0;
1380
- return bounded / (1 + bounded);
1381
- }
1382
-
1383
- export function blendGraphExpandedRecallScore(options: {
1384
- graphActivationScore: number;
1385
- seedRecallScore: number;
1386
- activationWeight: number;
1387
- blendMin: number;
1388
- blendMax: number;
1389
- }): number {
1390
- const graphNorm = normalizeGraphActivationScore(options.graphActivationScore);
1391
- const seedScore = Number.isFinite(options.seedRecallScore)
1392
- ? Math.min(1, Math.max(0, options.seedRecallScore))
1393
- : 0;
1394
- const weight = Math.min(1, Math.max(0, options.activationWeight));
1395
- const rawMin = Math.min(1, Math.max(0, options.blendMin));
1396
- const rawMax = Math.min(1, Math.max(0, options.blendMax));
1397
- const minBound = Math.min(rawMin, rawMax);
1398
- const maxBound = Math.max(rawMin, rawMax);
1399
- const blended = graphNorm * weight + seedScore * (1 - weight);
1400
- return Math.max(minBound, Math.min(maxBound, blended));
1401
- }
1402
-
1403
1341
  export function summarizeGraphShadowComparison(
1404
1342
  baseline: QmdSearchResult[],
1405
1343
  merged: QmdSearchResult[],
@@ -1845,6 +1783,7 @@ export class Orchestrator {
1845
1783
  readonly recallSectionCoordinator: RecallSectionCoordinator;
1846
1784
  readonly qmdResultResolver: QmdResultResolver;
1847
1785
  readonly contradictionLinkingCoordinator: ContradictionLinkingCoordinator;
1786
+ readonly graphRecallCoordinator: GraphRecallCoordinator;
1848
1787
  private heartbeatObserverChains = new Map<string, Promise<void>>();
1849
1788
  private recentExtractionFingerprints = new Map<string, number>();
1850
1789
  private readonly consolidationObservers = new Set<
@@ -2859,6 +2798,19 @@ export class Orchestrator {
2859
2798
  getExtraction: () => this.extraction,
2860
2799
  });
2861
2800
  this.modelRegistry = new ModelRegistry(config.memoryDir);
2801
+ this.graphRecallCoordinator = new GraphRecallCoordinator({
2802
+ getConfig: () => this.config,
2803
+ getStorage: () => this.storage,
2804
+ storageFor: (namespace) => this.storageRouter.storageFor(namespace),
2805
+ graphIndexFor: (storage) => this.graphIndexFor(storage),
2806
+ namespaceFromPath: (p) => this.namespaceFromPath(p),
2807
+ resolveColdQmdResultForRecall: (result, fallbackStorage, recallNamespaces) =>
2808
+ this.resolveColdQmdResultForRecall(result, fallbackStorage, recallNamespaces),
2809
+ storageForAbsoluteQmdResultPath: (resultPath, fallbackStorage, recallNamespaces) =>
2810
+ this.storageForAbsoluteQmdResultPath(resultPath, fallbackStorage, recallNamespaces),
2811
+ readQmdResultMemory: (resultPath, fallbackStorage, recallNamespaces) =>
2812
+ this.readQmdResultMemory(resultPath, fallbackStorage, recallNamespaces),
2813
+ });
2862
2814
  this.relevance = new RelevanceStore(config.memoryDir);
2863
2815
  this.negatives = new NegativeExampleStore(config.memoryDir);
2864
2816
  this.lastRecall = new LastRecallStore(config.memoryDir);
@@ -6035,12 +5987,14 @@ export class Orchestrator {
6035
5987
  return capped;
6036
5988
  }
6037
5989
 
5990
+ // Issue #1526 (seam 14): graph-recall expansion moved to
5991
+ // GraphRecallCoordinator. Thin delegation keeps the private API stable
5992
+ // for callers (recallInternal, cold-fallback pipeline) + tests.
6038
5993
  private async expandResultsViaGraph(options: {
6039
5994
  memoryResults: QmdSearchResult[];
6040
5995
  recallNamespaces: string[];
6041
5996
  recallResultLimit: number;
6042
5997
  deadlineAtMs?: number | null;
6043
- /** Issue #681 — when true, bypass graphTraversalConfidenceFloor. */
6044
5998
  includeLowConfidence?: boolean;
6045
5999
  }): Promise<{
6046
6000
  merged: QmdSearchResult[];
@@ -6048,198 +6002,10 @@ export class Orchestrator {
6048
6002
  expandedPaths: GraphRecallExpandedEntry[];
6049
6003
  seedResults: QmdSearchResult[];
6050
6004
  }> {
6051
- const deadlineExpired = (): boolean =>
6052
- typeof options.deadlineAtMs === "number" &&
6053
- Date.now() >= options.deadlineAtMs;
6054
- const byNamespace = new Map<string, QmdSearchResult[]>();
6055
- const addResultForNamespace = (
6056
- namespace: string,
6057
- result: QmdSearchResult,
6058
- ): void => {
6059
- const existing = byNamespace.get(namespace);
6060
- if (existing) {
6061
- existing.push(result);
6062
- } else {
6063
- byNamespace.set(namespace, [result]);
6064
- }
6065
- };
6066
- const resolvedAmbiguousSeeds = new Map<
6067
- string,
6068
- { namespace: string; result: QmdSearchResult } | null
6069
- >();
6070
- const resolveAmbiguousSeedOwner = async (
6071
- result: QmdSearchResult,
6072
- parts: { collection: string; relativePath: string } | null,
6073
- ): Promise<{ namespace: string; result: QmdSearchResult } | null> => {
6074
- const cached = resolvedAmbiguousSeeds.get(result.path);
6075
- if (cached !== undefined) return cached;
6076
- if (deadlineExpired()) {
6077
- resolvedAmbiguousSeeds.set(result.path, null);
6078
- return null;
6079
- }
6080
-
6081
- let resolvedPath = result.path;
6082
- let resolvedResult = result;
6083
- if (parts) {
6084
- const resolvedCold = await this.resolveColdQmdResultForRecall(
6085
- result,
6086
- this.storage,
6087
- options.recallNamespaces,
6088
- );
6089
- if (!resolvedCold || deadlineExpired()) {
6090
- resolvedAmbiguousSeeds.set(result.path, null);
6091
- return null;
6092
- }
6093
- resolvedPath = resolvedCold.result.path;
6094
- resolvedResult = resolvedCold.result;
6095
- }
6096
-
6097
- if (!path.isAbsolute(resolvedPath)) {
6098
- resolvedAmbiguousSeeds.set(result.path, null);
6099
- return null;
6100
- }
6101
- const ownerStorage = await this.storageForAbsoluteQmdResultPath(
6102
- resolvedPath,
6103
- this.storage,
6104
- options.recallNamespaces,
6105
- );
6106
- const ownerNamespace = ownerStorage?.namespace ?? null;
6107
- const resolved =
6108
- ownerNamespace && options.recallNamespaces.includes(ownerNamespace)
6109
- ? { namespace: ownerNamespace, result: resolvedResult }
6110
- : null;
6111
- resolvedAmbiguousSeeds.set(result.path, resolved);
6112
- return resolved;
6113
- };
6114
- const coldCollection = this.config.qmdColdCollection ?? "openclaw-engram-cold";
6115
- for (const result of options.memoryResults) {
6116
- if (deadlineExpired()) break;
6117
- const parts = qmdCollectionPathParts(result.path);
6118
- if (parts?.collection === coldCollection) {
6119
- const resolved = await resolveAmbiguousSeedOwner(result, parts);
6120
- if (resolved) {
6121
- addResultForNamespace(resolved.namespace, resolved.result);
6122
- }
6123
- continue;
6124
- }
6125
- if (path.isAbsolute(result.path)) {
6126
- const resolved = await resolveAmbiguousSeedOwner(result, null);
6127
- if (resolved) {
6128
- addResultForNamespace(resolved.namespace, resolved.result);
6129
- }
6130
- continue;
6131
- }
6132
- const ns = this.namespaceFromPath(result.path);
6133
- if (!options.recallNamespaces.includes(ns)) continue;
6134
- addResultForNamespace(ns, result);
6135
- }
6136
-
6137
- const perNamespaceSeedCap = Math.max(3, options.recallResultLimit);
6138
- const perNamespaceExpandedCap = Math.max(8, options.recallResultLimit * 2);
6139
- const seedPaths: string[] = [];
6140
- const seedResults: QmdSearchResult[] = [];
6141
- const expandedPaths: GraphRecallExpandedEntry[] = [];
6142
- const expandedResults: QmdSearchResult[] = [];
6143
-
6144
- for (const [namespace, nsResults] of byNamespace.entries()) {
6145
- if (deadlineExpired()) break;
6146
- const storage = await this.storageRouter.storageFor(namespace);
6147
- const seedCandidates = nsResults.slice(0, perNamespaceSeedCap);
6148
- seedResults.push(...seedCandidates);
6149
- const seedRelativePaths =
6150
- typeof options.deadlineAtMs === "number"
6151
- ? await this.graphSeedPathsWithinDeadline(
6152
- storage,
6153
- seedCandidates,
6154
- options.deadlineAtMs,
6155
- [namespace],
6156
- )
6157
- : (
6158
- await Promise.all(
6159
- seedCandidates.map((result) =>
6160
- this.graphSeedPathRelativeToStorage(storage, result, [
6161
- namespace,
6162
- ]),
6163
- ),
6164
- )
6165
- ).filter(
6166
- (value): value is string =>
6167
- typeof value === "string" && value.length > 0,
6168
- );
6169
- if (deadlineExpired()) break;
6170
- if (seedRelativePaths.length === 0) continue;
6171
-
6172
- const seedRecallScore = seedCandidates.reduce(
6173
- (max, item) => Math.max(max, item.score),
6174
- 0,
6175
- );
6176
- seedPaths.push(
6177
- ...seedRelativePaths.map((rel) => path.join(storage.dir, rel)),
6178
- );
6179
- const seedSet = new Set(seedRelativePaths);
6180
- const expanded = await this.graphIndexFor(storage).spreadingActivation(
6181
- seedRelativePaths,
6182
- this.config.maxGraphTraversalSteps,
6183
- {
6184
- ...(options.includeLowConfidence === true ? { includeLowConfidence: true } : {}),
6185
- ...(typeof options.deadlineAtMs === "number"
6186
- ? { deadlineAtMs: options.deadlineAtMs }
6187
- : {}),
6188
- },
6189
- );
6190
- if (expanded.length === 0) continue;
6191
- if (deadlineExpired()) break;
6192
-
6193
- for (const candidate of expanded.slice(0, perNamespaceExpandedCap)) {
6194
- if (deadlineExpired()) break;
6195
- if (seedSet.has(candidate.path)) continue;
6196
- const memoryPath = path.resolve(storage.dir, candidate.path);
6197
- const memory = await storage.readMemoryByPath(memoryPath);
6198
- if (deadlineExpired()) break;
6199
- if (!memory) continue;
6200
- if (isArtifactMemoryPath(memory.path)) continue;
6201
- if (memory.frontmatter.status && memory.frontmatter.status !== "active")
6202
- continue;
6203
-
6204
- const snippet = memory.content.slice(0, 400);
6205
- const score = blendGraphExpandedRecallScore({
6206
- graphActivationScore: candidate.score,
6207
- seedRecallScore,
6208
- activationWeight: this.config.graphExpansionActivationWeight,
6209
- blendMin: this.config.graphExpansionBlendMin,
6210
- blendMax: this.config.graphExpansionBlendMax,
6211
- });
6212
- expandedResults.push({
6213
- docid: memory.frontmatter.id,
6214
- path: memory.path,
6215
- snippet,
6216
- score,
6217
- });
6218
- expandedPaths.push({
6219
- path: memory.path,
6220
- score,
6221
- namespace,
6222
- seed: path.resolve(storage.dir, candidate.seed),
6223
- hopDepth: candidate.hopDepth,
6224
- decayedWeight: candidate.decayedWeight,
6225
- graphType: candidate.graphType,
6226
- // Issue #681 PR 3/3 — surface the per-edge confidence used for
6227
- // PageRank weighting / floor pruning so downstream observability
6228
- // (recall_xray, memory_graph_explain) can attribute ranking and
6229
- // pruning decisions to specific edges.
6230
- edgeConfidence: candidate.edgeConfidence,
6231
- });
6232
- }
6233
- }
6234
-
6235
- return {
6236
- merged: mergeGraphExpandedResults(options.memoryResults, expandedResults),
6237
- seedPaths,
6238
- expandedPaths,
6239
- seedResults,
6240
- };
6005
+ return this.graphRecallCoordinator.expandResultsViaGraph(options);
6241
6006
  }
6242
-
6007
+ // Issue #1526 (seam 14): graph-recall snapshot moved to
6008
+ // GraphRecallCoordinator. Thin delegation keeps the private API stable.
6243
6009
  private async recordLastGraphRecallSnapshot(options: {
6244
6010
  storage: StorageManager;
6245
6011
  prompt: string;
@@ -6255,45 +6021,8 @@ export class Orchestrator {
6255
6021
  finalResults?: GraphRecallRankedResult[];
6256
6022
  shadowComparison?: GraphRecallShadowComparison;
6257
6023
  }): Promise<void> {
6258
- try {
6259
- const snapshotPath = path.join(
6260
- options.storage.dir,
6261
- "state",
6262
- "last_graph_recall.json",
6263
- );
6264
- await mkdir(path.dirname(snapshotPath), { recursive: true });
6265
- const now = new Date().toISOString();
6266
- const totalSeedCount = options.seedPaths.length;
6267
- const totalExpandedCount = options.expandedPaths.length;
6268
- const seeds = options.seedPaths.slice(0, 64);
6269
- const expanded = clampGraphRecallExpandedEntries(
6270
- options.expandedPaths,
6271
- 64,
6272
- );
6273
- const payload = {
6274
- recordedAt: now,
6275
- mode: options.recallMode,
6276
- queryHash: createHash("sha256").update(options.prompt).digest("hex"),
6277
- queryLength: options.prompt.length,
6278
- namespaces: options.recallNamespaces,
6279
- seedCount: totalSeedCount,
6280
- expandedCount: totalExpandedCount,
6281
- seeds,
6282
- expanded,
6283
- status: options.status,
6284
- reason: options.reason,
6285
- shadowMode: options.shadowMode === true,
6286
- queryIntent: options.queryIntent,
6287
- seedResults: (options.seedResults ?? []).slice(0, 64),
6288
- finalResults: (options.finalResults ?? []).slice(0, 64),
6289
- shadowComparison: options.shadowComparison,
6290
- };
6291
- await writeFile(snapshotPath, JSON.stringify(payload, null, 2), "utf-8");
6292
- } catch (err) {
6293
- log.debug(`last graph recall write failed: ${err}`);
6294
- }
6024
+ return this.graphRecallCoordinator.recordLastGraphRecallSnapshot(options);
6295
6025
  }
6296
-
6297
6026
  private async recordLastIntentSnapshot(options: {
6298
6027
  storage: StorageManager;
6299
6028
  snapshot: IntentDebugSnapshot;
@@ -18231,46 +17960,6 @@ export class Orchestrator {
18231
17960
  );
18232
17961
  }
18233
17962
 
18234
-
18235
- private async graphSeedPathRelativeToStorage(
18236
- storage: StorageManager,
18237
- result: QmdSearchResult,
18238
- recallNamespaces: readonly string[] = [],
18239
- ): Promise<string | null> {
18240
- const parts = qmdCollectionPathParts(result.path);
18241
- if (parts) {
18242
- const memory = await this.readQmdResultMemory(
18243
- result.path,
18244
- storage,
18245
- recallNamespaces,
18246
- );
18247
- return memory
18248
- ? graphPathRelativeToStorage(storage.dir, memory.path)
18249
- : null;
18250
- }
18251
- return graphPathRelativeToStorage(storage.dir, result.path);
18252
- }
18253
-
18254
- private async graphSeedPathsWithinDeadline(
18255
- storage: StorageManager,
18256
- results: QmdSearchResult[],
18257
- deadlineAtMs: number,
18258
- recallNamespaces: readonly string[] = [],
18259
- ): Promise<string[]> {
18260
- const resolved: string[] = [];
18261
- for (const result of results) {
18262
- if (Date.now() >= deadlineAtMs) break;
18263
- const seedPath = await this.graphSeedPathRelativeToStorage(
18264
- storage,
18265
- result,
18266
- recallNamespaces,
18267
- );
18268
- if (Date.now() >= deadlineAtMs) break;
18269
- if (seedPath) resolved.push(seedPath);
18270
- }
18271
- return resolved;
18272
- }
18273
-
18274
17963
  private namespaceFromPath(p: string): string {
18275
17964
  if (!resolveNamespaceCapabilities(this.config).namespaces) return this.config.defaultNamespace;
18276
17965
  const parts = qmdCollectionPathParts(p);