@remnic/core 9.3.741 → 9.3.742

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/dist/access-boundary.d.ts +2 -2
  2. package/dist/access-boundary.js +2 -2
  3. package/dist/access-cli.js +8 -8
  4. package/dist/access-http.d.ts +2 -2
  5. package/dist/access-http.js +6 -6
  6. package/dist/access-mcp.d.ts +2 -2
  7. package/dist/access-mcp.js +5 -5
  8. package/dist/access-operations-batch.js +3 -3
  9. package/dist/access-operations.d.ts +2 -2
  10. package/dist/access-operations.js +4 -4
  11. package/dist/{access-service-Cbk-Kv_A.d.ts → access-service-K8n12dXu.d.ts} +1 -1
  12. package/dist/access-service.d.ts +2 -2
  13. package/dist/access-service.js +1 -1
  14. package/dist/access-surface-catalog.d.ts +2 -2
  15. package/dist/{auto-sync-G3V5AN4C.js → auto-sync-XHSKKQAH.js} +2 -2
  16. package/dist/bootstrap.d.ts +1 -1
  17. package/dist/{chunk-DBVBV2RF.js → chunk-7ADHJ6SU.js} +3 -3
  18. package/dist/{chunk-VC7QELR6.js → chunk-7HUGLMSW.js} +346 -305
  19. package/dist/chunk-7HUGLMSW.js.map +1 -0
  20. package/dist/{chunk-QCO2QFLZ.js → chunk-AM7YKD7V.js} +3 -2
  21. package/dist/{chunk-QCO2QFLZ.js.map → chunk-AM7YKD7V.js.map} +1 -1
  22. package/dist/{chunk-OZECYVGI.js → chunk-C7LDPZJJ.js} +6 -4
  23. package/dist/{chunk-OZECYVGI.js.map → chunk-C7LDPZJJ.js.map} +1 -1
  24. package/dist/{chunk-4J53ZVPQ.js → chunk-EZZUNKMJ.js} +3 -3
  25. package/dist/{chunk-H3WSZWHK.js → chunk-GFJ5ABKS.js} +7 -6
  26. package/dist/chunk-GFJ5ABKS.js.map +1 -0
  27. package/dist/{chunk-LD6A7XG5.js → chunk-JMMSVRF4.js} +4 -4
  28. package/dist/{chunk-7Y5KKMIP.js → chunk-NHJYUIII.js} +3 -2
  29. package/dist/{chunk-7Y5KKMIP.js.map → chunk-NHJYUIII.js.map} +1 -1
  30. package/dist/{chunk-DGVM5SFL.js → chunk-PHRLJFWZ.js} +3 -2
  31. package/dist/{chunk-DGVM5SFL.js.map → chunk-PHRLJFWZ.js.map} +1 -1
  32. package/dist/{chunk-M5KLYHHI.js → chunk-QUNH5UZH.js} +2 -2
  33. package/dist/{chunk-3TUDKDTQ.js → chunk-UEIF7RWQ.js} +3 -3
  34. package/dist/{chunk-773DPI6T.js → chunk-V5RVMULT.js} +11 -8
  35. package/dist/chunk-V5RVMULT.js.map +1 -0
  36. package/dist/{chunk-XPOOY35N.js → chunk-W4H5A3TJ.js} +3 -2
  37. package/dist/chunk-W4H5A3TJ.js.map +1 -0
  38. package/dist/{cli-BQW2xKdb.d.ts → cli-BEOLy1xo.d.ts} +2 -2
  39. package/dist/cli.d.ts +3 -3
  40. package/dist/cli.js +8 -8
  41. package/dist/explicit-capture.d.ts +1 -1
  42. package/dist/graph-recall.js +1 -1
  43. package/dist/index.d.ts +4 -4
  44. package/dist/index.js +13 -13
  45. package/dist/lcm/engine.js +2 -2
  46. package/dist/lcm/index.js +2 -2
  47. package/dist/lcm/summarizer.js +1 -1
  48. package/dist/mcp-memory-inspector-app.d.ts +2 -2
  49. package/dist/operator-toolkit.js +1 -1
  50. package/dist/{orchestrator-BOAFN_Yr.d.ts → orchestrator-Dr-1v5EL.d.ts} +58 -0
  51. package/dist/orchestrator.d.ts +1 -1
  52. package/dist/orchestrator.js +4 -4
  53. package/package.json +2 -2
  54. package/src/cli.ts +2 -1
  55. package/src/graph-recall.ts +2 -1
  56. package/src/lcm/engine.ts +9 -6
  57. package/src/lcm/summarizer.ts +2 -1
  58. package/src/operator-toolkit.ts +2 -1
  59. package/src/orchestration/qmd-result-resolver.ts +377 -0
  60. package/src/orchestrator.ts +14 -279
  61. package/src/procedural/procedure-stats.ts +2 -1
  62. package/src/wearables/pipeline.ts +5 -3
  63. package/dist/chunk-773DPI6T.js.map +0 -1
  64. package/dist/chunk-H3WSZWHK.js.map +0 -1
  65. package/dist/chunk-VC7QELR6.js.map +0 -1
  66. package/dist/chunk-XPOOY35N.js.map +0 -1
  67. /package/dist/{auto-sync-G3V5AN4C.js.map → auto-sync-XHSKKQAH.js.map} +0 -0
  68. /package/dist/{chunk-DBVBV2RF.js.map → chunk-7ADHJ6SU.js.map} +0 -0
  69. /package/dist/{chunk-4J53ZVPQ.js.map → chunk-EZZUNKMJ.js.map} +0 -0
  70. /package/dist/{chunk-LD6A7XG5.js.map → chunk-JMMSVRF4.js.map} +0 -0
  71. /package/dist/{chunk-M5KLYHHI.js.map → chunk-QUNH5UZH.js.map} +0 -0
  72. /package/dist/{chunk-3TUDKDTQ.js.map → chunk-UEIF7RWQ.js.map} +0 -0
@@ -13,7 +13,7 @@ import {
13
13
  summarizeProvenanceCoverage,
14
14
  summarizeTierDistribution,
15
15
  summarizeTombstoneStatus
16
- } from "./chunk-QCO2QFLZ.js";
16
+ } from "./chunk-AM7YKD7V.js";
17
17
  import "./chunk-UYYSOMS2.js";
18
18
  import "./chunk-7XH7VJN4.js";
19
19
  import "./chunk-YBPYIAA5.js";
@@ -1080,6 +1080,63 @@ declare class RecallRerankCoordinator {
1080
1080
  applyMmrToQmdResults(sectionId: string, results: QmdSearchResult[], caps?: CapabilitySet): QmdSearchResult[];
1081
1081
  }
1082
1082
 
1083
+ /**
1084
+ * QMD result resolver — extracted from the orchestrator (issue #1526 seam 11).
1085
+ *
1086
+ * Owns the subsystem that resolves a QMD search-result path back to the
1087
+ * concrete `MemoryFile` (and its owning namespace/storage). Three concerns
1088
+ * live here:
1089
+ *
1090
+ * - `readQmdResultMemory` — resolve a result path to a `MemoryFile`,
1091
+ * handling cold-tier collections, namespace-prefixed collections, and
1092
+ * absolute vs relative paths.
1093
+ * - `resolveColdQmdResultForRecall` — wrap a result with its owner
1094
+ * namespace so the recall pipeline can scope-filter candidates.
1095
+ * - `storageForAbsoluteQmdResultPath` — find the `StorageManager` whose
1096
+ * directory tree contains an absolute result path.
1097
+ *
1098
+ * Behavior-preserving move from orchestrator.ts — no logic changes. The
1099
+ * orchestrator constructs one instance and keeps thin delegating methods so
1100
+ * existing call sites (recallInternal, applyColdFallbackPipeline,
1101
+ * loadSearchResultMemoryMap, suggestLinksForMemory, and the
1102
+ * RecallRerankCoordinator callback) continue to work.
1103
+ *
1104
+ * Config, the storage router, and the orchestrator's namespace-resolution
1105
+ * helpers are accessed through getter callbacks (not captured at
1106
+ * construction) so that post-construction reassignment of the
1107
+ * orchestrator's live fields is honored. This mirrors the
1108
+ * RecallRerankCoordinator / ConversationIndexCoordinator accessor pattern.
1109
+ */
1110
+
1111
+ /**
1112
+ * Coordinator for resolving QMD search-result paths to concrete memory
1113
+ * files and their owning namespace/storage.
1114
+ */
1115
+ declare class QmdResultResolver {
1116
+ private readonly getConfig;
1117
+ private readonly storageFor;
1118
+ private readonly storageDirNamespace;
1119
+ private readonly qmdCollectionNamespaceFromPrefix;
1120
+ private readonly namespaceFromPath;
1121
+ constructor(options: {
1122
+ getConfig: () => PluginConfig;
1123
+ storageFor: (namespace: string) => Promise<StorageManager>;
1124
+ storageDirNamespace: (storageDir: string) => string;
1125
+ qmdCollectionNamespaceFromPrefix: (collectionPrefix: string) => string | null;
1126
+ namespaceFromPath: (p: string) => string;
1127
+ });
1128
+ readQmdResultMemory(resultPath: string, fallbackStorage: StorageManager, recallNamespaces?: readonly string[]): Promise<MemoryFile | null>;
1129
+ resolveColdQmdResultForRecall(result: QmdSearchResult, fallbackStorage: StorageManager, recallNamespaces?: readonly string[]): Promise<{
1130
+ namespace: string;
1131
+ result: QmdSearchResult;
1132
+ } | null>;
1133
+ storageForAbsoluteQmdResultPath(resultPath: string, fallbackStorage: StorageManager, recallNamespaces?: readonly string[]): Promise<{
1134
+ storage: StorageManager;
1135
+ dir: string;
1136
+ namespace: string;
1137
+ } | null>;
1138
+ }
1139
+
1083
1140
  /**
1084
1141
  * Pattern-reinforcement maintenance job (issue #687 PR 2/4).
1085
1142
  *
@@ -1672,6 +1729,7 @@ declare class Orchestrator {
1672
1729
  */
1673
1730
  readonly conversationIndexCoordinator: ConversationIndexCoordinator;
1674
1731
  readonly recallRerankCoordinator: RecallRerankCoordinator;
1732
+ readonly qmdResultResolver: QmdResultResolver;
1675
1733
  private heartbeatObserverChains;
1676
1734
  private recentExtractionFingerprints;
1677
1735
  private readonly consolidationObservers;
@@ -8,7 +8,7 @@ import './summarizer.js';
8
8
  import './local-llm.js';
9
9
  import './fallback-llm.js';
10
10
  import './orchestration/maintenance.js';
11
- export { L as BulkImportBatchIngestResult, M as BulkImportBatchPartialFailureError, N as GraphRecallRankedResult, Q as GraphRecallShadowComparison, G as GraphRecallSnapshot, I as IntentDebugSnapshot, O as Orchestrator, T as QmdRecallSnapshot, U as RecallInvocationOptions, V as RecallModeDecision, X as appendMemoryToGraphContext, Y as blendGraphExpandedRecallScore, Z as buildCompressionGuidelinesMarkdown, _ as buildMemoryPathById, $ as computeArtifactCandidateFetchLimit, a0 as computeArtifactRecallLimit, a1 as computeQmdHybridFetchLimit, q as defaultWorkspaceDir, a2 as deriveTopicsFromExtraction, a3 as filterRecallCandidates, a4 as graphPathRelativeToStorage, a5 as hasIdentityRecoveryIntent, a6 as isArtifactMemoryPath, a7 as lifecycleRecallScoreAdjustment, a8 as mergeArtifactRecallCandidates, a9 as mergeGraphExpandedResults, aa as resolveEffectiveIdentityInjectionMode, ab as resolveEffectiveRecallMode, ac as resolvePersistedMemoryRelativePath, ad as resolveRecallModeDecision, ae as resolveRecallModeDecisionAsync, af as resolveRecentThreadMemoryPaths, A as sanitizeSessionKeyForFilename, ag as shouldFilterLifecycleRecallCandidate, ah as summarizeGraphShadowComparison } from './orchestrator-BOAFN_Yr.js';
11
+ export { L as BulkImportBatchIngestResult, M as BulkImportBatchPartialFailureError, N as GraphRecallRankedResult, Q as GraphRecallShadowComparison, G as GraphRecallSnapshot, I as IntentDebugSnapshot, O as Orchestrator, T as QmdRecallSnapshot, U as RecallInvocationOptions, V as RecallModeDecision, X as appendMemoryToGraphContext, Y as blendGraphExpandedRecallScore, Z as buildCompressionGuidelinesMarkdown, _ as buildMemoryPathById, $ as computeArtifactCandidateFetchLimit, a0 as computeArtifactRecallLimit, a1 as computeQmdHybridFetchLimit, q as defaultWorkspaceDir, a2 as deriveTopicsFromExtraction, a3 as filterRecallCandidates, a4 as graphPathRelativeToStorage, a5 as hasIdentityRecoveryIntent, a6 as isArtifactMemoryPath, a7 as lifecycleRecallScoreAdjustment, a8 as mergeArtifactRecallCandidates, a9 as mergeGraphExpandedResults, aa as resolveEffectiveIdentityInjectionMode, ab as resolveEffectiveRecallMode, ac as resolvePersistedMemoryRelativePath, ad as resolveRecallModeDecision, ae as resolveRecallModeDecisionAsync, af as resolveRecentThreadMemoryPaths, A as sanitizeSessionKeyForFilename, ag as shouldFilterLifecycleRecallCandidate, ah as summarizeGraphShadowComparison } from './orchestrator-Dr-1v5EL.js';
12
12
  export { formatCompressionGuidelinesForRecall } from './orchestration/compression-guideline-coordinator.js';
13
13
  import './live-connectors-runner.js';
14
14
  import './model-registry.js';
@@ -26,8 +26,8 @@ import {
26
26
  sanitizeSessionKeyForFilename,
27
27
  shouldFilterLifecycleRecallCandidate,
28
28
  summarizeGraphShadowComparison
29
- } from "./chunk-VC7QELR6.js";
30
- import "./chunk-OZECYVGI.js";
29
+ } from "./chunk-7HUGLMSW.js";
30
+ import "./chunk-C7LDPZJJ.js";
31
31
  import {
32
32
  formatCompressionGuidelinesForRecall
33
33
  } from "./chunk-WIMKOTNY.js";
@@ -38,12 +38,12 @@ import "./chunk-7HYPN2GC.js";
38
38
  import "./chunk-666A3MOW.js";
39
39
  import "./chunk-HHLLAQGZ.js";
40
40
  import "./chunk-UWK5OXUJ.js";
41
- import "./chunk-773DPI6T.js";
41
+ import "./chunk-V5RVMULT.js";
42
42
  import "./chunk-7N4KAIGN.js";
43
43
  import "./chunk-TPDBFYEG.js";
44
44
  import "./chunk-KCYE2MZM.js";
45
45
  import "./chunk-KYYL4U6X.js";
46
- import "./chunk-XPOOY35N.js";
46
+ import "./chunk-W4H5A3TJ.js";
47
47
  import "./chunk-HW5EF6LV.js";
48
48
  import "./chunk-TECVW3JP.js";
49
49
  import "./chunk-O7GOFAM3.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remnic/core",
3
- "version": "9.3.741",
3
+ "version": "9.3.742",
4
4
  "description": "Framework-agnostic Remnic memory engine — orchestrator, storage, extraction, search, trust zones",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -3000,7 +3000,7 @@
3000
3000
  "core"
3001
3001
  ],
3002
3002
  "peerDependencies": {
3003
- "@remnic/coding-graph": "^9.3.741"
3003
+ "@remnic/coding-graph": "^9.3.742"
3004
3004
  },
3005
3005
  "peerDependenciesMeta": {
3006
3006
  "@remnic/coding-graph": {
package/src/cli.ts CHANGED
@@ -2430,9 +2430,10 @@ export async function runPolicyStatusCliCommand(
2430
2430
  const defaultWindowMs = Math.max(0, orchestrator.config.behaviorLoopLearningWindowDays) * 24 * 60 * 60 * 1000;
2431
2431
  const cutoffIso = defaultWindowMs > 0 ? new Date(now.getTime() - defaultWindowMs).toISOString() : undefined;
2432
2432
 
2433
+ const { behaviorLoopAutoTuneEnabled } = orchestrator.config;
2433
2434
  return {
2434
2435
  generatedAt: now.toISOString(),
2435
- autoTuneEnabled: orchestrator.config.behaviorLoopAutoTuneEnabled,
2436
+ autoTuneEnabled: behaviorLoopAutoTuneEnabled,
2436
2437
  current: current
2437
2438
  ? {
2438
2439
  ...current,
@@ -110,7 +110,8 @@ export function runGraphRecall(
110
110
  config: GraphRecallConfig,
111
111
  options: GraphRecallOptions,
112
112
  ): GraphRecallRun {
113
- if (!config.recallGraphEnabled) {
113
+ const { recallGraphEnabled } = config;
114
+ if (!recallGraphEnabled) {
114
115
  return {
115
116
  ran: false,
116
117
  results: [],
package/src/lcm/engine.ts CHANGED
@@ -117,6 +117,7 @@ export class LcmEngine {
117
117
  const db = openLcmDatabase(this.memoryDir);
118
118
  const archive = new LcmArchive(db);
119
119
  const dag = new LcmDag(db);
120
+ const { telemetryPrefilterEnabled } = this.config;
120
121
  const summarizer = new LcmSummarizer(
121
122
  archive,
122
123
  dag,
@@ -126,7 +127,7 @@ export class LcmEngine {
126
127
  rollupFanIn: this.config.rollupFanIn,
127
128
  maxDepth: this.config.maxDepth,
128
129
  deterministicMaxTokens: this.config.deterministicMaxTokens,
129
- telemetryPrefilterEnabled: this.config.telemetryPrefilterEnabled,
130
+ telemetryPrefilterEnabled,
130
131
  },
131
132
  );
132
133
  const observeQueue = new LcmWorkQueue({
@@ -229,17 +230,18 @@ export class LcmEngine {
229
230
  if (messages.length === 0) return;
230
231
 
231
232
  const currentMax = this.archive.getMaxTurnIndex(sessionId);
233
+ const { messagePartsEnabled } = this.config;
232
234
  const newMessages = messages.map((m, i) => ({
233
235
  turnIndex: currentMax + 1 + i,
234
236
  role: m.role,
235
237
  content: m.content,
236
- parts: this.config.messagePartsEnabled ? m.parts : undefined,
237
- rawContent: this.config.messagePartsEnabled ? m.rawContent : undefined,
238
- sourceFormat: this.config.messagePartsEnabled ? m.sourceFormat : undefined,
238
+ parts: messagePartsEnabled ? m.parts : undefined,
239
+ rawContent: messagePartsEnabled ? m.rawContent : undefined,
240
+ sourceFormat: messagePartsEnabled ? m.sourceFormat : undefined,
239
241
  }));
240
242
 
241
243
  this.archive.appendMessages(sessionId, newMessages, {
242
- messagePartsEnabled: this.config.messagePartsEnabled,
244
+ messagePartsEnabled,
243
245
  });
244
246
 
245
247
  // Trigger incremental summarization inside the worker, after append.
@@ -346,7 +348,8 @@ export class LcmEngine {
346
348
  query: string,
347
349
  limit = this.config.messagePartsRecallMaxResults,
348
350
  ): Promise<LcmStructuredRecallMatch[]> {
349
- if (!this.config.enabled || !this.config.messagePartsEnabled) return [];
351
+ const { messagePartsEnabled } = this.config;
352
+ if (!this.config.enabled || !messagePartsEnabled) return [];
350
353
  const normalizedSessionId = normalizeLcmSessionId(sessionId);
351
354
  if (!normalizedSessionId) return [];
352
355
  await this.ensureInitialized();
@@ -153,8 +153,9 @@ export class LcmSummarizer {
153
153
  text: string,
154
154
  targetTokens: number,
155
155
  ): Promise<{ text: string; escalation: number }> {
156
+ const { telemetryPrefilterEnabled } = this.config;
156
157
  if (
157
- this.config.telemetryPrefilterEnabled &&
158
+ telemetryPrefilterEnabled &&
158
159
  looksLikeMechanicalTelemetryTranscript(text)
159
160
  ) {
160
161
  return {
@@ -1617,7 +1617,8 @@ async function summarizeGraphEdgeDecayStatus(
1617
1617
  ): Promise<OperatorDoctorCheck> {
1618
1618
  // Lazy import to keep the doctor fast when the feature is disabled.
1619
1619
  const { readGraphEdgeDecayStatus } = await import("./maintenance/graph-edge-decay.js");
1620
- const enabled = config.graphEdgeDecayEnabled === true;
1620
+ const { graphEdgeDecayEnabled } = config;
1621
+ const enabled = graphEdgeDecayEnabled === true;
1621
1622
  if (!enabled) {
1622
1623
  return {
1623
1624
  key: "graph_edge_decay",
@@ -0,0 +1,377 @@
1
+ /**
2
+ * QMD result resolver — extracted from the orchestrator (issue #1526 seam 11).
3
+ *
4
+ * Owns the subsystem that resolves a QMD search-result path back to the
5
+ * concrete `MemoryFile` (and its owning namespace/storage). Three concerns
6
+ * live here:
7
+ *
8
+ * - `readQmdResultMemory` — resolve a result path to a `MemoryFile`,
9
+ * handling cold-tier collections, namespace-prefixed collections, and
10
+ * absolute vs relative paths.
11
+ * - `resolveColdQmdResultForRecall` — wrap a result with its owner
12
+ * namespace so the recall pipeline can scope-filter candidates.
13
+ * - `storageForAbsoluteQmdResultPath` — find the `StorageManager` whose
14
+ * directory tree contains an absolute result path.
15
+ *
16
+ * Behavior-preserving move from orchestrator.ts — no logic changes. The
17
+ * orchestrator constructs one instance and keeps thin delegating methods so
18
+ * existing call sites (recallInternal, applyColdFallbackPipeline,
19
+ * loadSearchResultMemoryMap, suggestLinksForMemory, and the
20
+ * RecallRerankCoordinator callback) continue to work.
21
+ *
22
+ * Config, the storage router, and the orchestrator's namespace-resolution
23
+ * helpers are accessed through getter callbacks (not captured at
24
+ * construction) so that post-construction reassignment of the
25
+ * orchestrator's live fields is honored. This mirrors the
26
+ * RecallRerankCoordinator / ConversationIndexCoordinator accessor pattern.
27
+ */
28
+
29
+ import path from "node:path";
30
+ import { log } from "../logger.js";
31
+ import { isPathInsideStorageRoot } from "../storage-paths.js";
32
+ import { namespaceIdentityFromToken } from "../namespaces/identity.js";
33
+ import { resolveNamespaceCapabilities } from "../capabilities.js";
34
+ import { SecureStoreLockedError } from "../secure-store/index.js";
35
+ import type { PluginConfig, QmdSearchResult, MemoryFile } from "../types.js";
36
+ import type { StorageManager } from "../index.js";
37
+
38
+ /**
39
+ * Split a relative QMD result path into its collection prefix and the
40
+ * remainder. Returns `null` for absolute paths, date-only prefixes, or
41
+ * paths with no collection separator.
42
+ */
43
+ export function qmdCollectionPathParts(resultPath: string): {
44
+ collection: string;
45
+ relativePath: string;
46
+ } | null {
47
+ if (!resultPath || path.isAbsolute(resultPath)) return null;
48
+ const normalized = resultPath.replace(/\\/g, "/").replace(/^\/+/, "");
49
+ const slashIndex = normalized.indexOf("/");
50
+ if (slashIndex <= 0 || slashIndex >= normalized.length - 1) return null;
51
+ const collection = normalized.slice(0, slashIndex);
52
+ if (/^\d{4}-\d{2}-\d{2}$/.test(collection)) return null;
53
+ return {
54
+ collection,
55
+ relativePath: normalized.slice(slashIndex + 1),
56
+ };
57
+ }
58
+
59
+ /**
60
+ * Build the set of on-disk candidate paths to probe for a QMD result,
61
+ * constrained to the storage root so a result path can never escape the
62
+ * namespace directory.
63
+ */
64
+ export function qmdResultPathCandidates(
65
+ storageDir: string,
66
+ resultPath: string,
67
+ ): string[] {
68
+ const candidates = new Set<string>();
69
+ const storageRoot = path.resolve(storageDir);
70
+ const addCandidate = (candidate: string) => {
71
+ const resolved = path.resolve(candidate);
72
+ if (isPathInsideStorageRoot(storageRoot, resolved)) {
73
+ candidates.add(resolved);
74
+ }
75
+ };
76
+ const addRelativeCandidates = (relativePath: string) => {
77
+ const normalized = relativePath.replace(/\\/g, "/").replace(/^\/+/, "");
78
+ if (!normalized) return;
79
+ addCandidate(path.join(storageRoot, normalized));
80
+ if (/^\d{4}-\d{2}-\d{2}\//.test(normalized)) {
81
+ addCandidate(path.join(storageRoot, "facts", normalized));
82
+ }
83
+ };
84
+
85
+ if (path.isAbsolute(resultPath)) {
86
+ addCandidate(resultPath);
87
+ } else {
88
+ addRelativeCandidates(resultPath);
89
+ }
90
+
91
+ return [...candidates];
92
+ }
93
+
94
+ /**
95
+ * Extract a non-empty `dir` from a storage, returning `null` when the
96
+ * property is absent or not a non-empty string at runtime. Proxy-backed
97
+ * fake storages used in recall paths may not provide a real `dir`, so
98
+ * every caller must handle the null case (skip or fall back to a direct
99
+ * `readMemoryByPath`).
100
+ */
101
+ function storageDirOrNull(storage: StorageManager): string | null {
102
+ const dir = storage.dir;
103
+ return typeof dir === "string" && dir.length > 0 ? dir : null;
104
+ }
105
+
106
+ /**
107
+ * Coordinator for resolving QMD search-result paths to concrete memory
108
+ * files and their owning namespace/storage.
109
+ */
110
+ export class QmdResultResolver {
111
+ private readonly getConfig: () => PluginConfig;
112
+ private readonly storageFor: (namespace: string) => Promise<StorageManager>;
113
+ private readonly storageDirNamespace: (storageDir: string) => string;
114
+ private readonly qmdCollectionNamespaceFromPrefix: (
115
+ collectionPrefix: string,
116
+ ) => string | null;
117
+ private readonly namespaceFromPath: (p: string) => string;
118
+
119
+ constructor(options: {
120
+ getConfig: () => PluginConfig;
121
+ storageFor: (namespace: string) => Promise<StorageManager>;
122
+ storageDirNamespace: (storageDir: string) => string;
123
+ qmdCollectionNamespaceFromPrefix: (
124
+ collectionPrefix: string,
125
+ ) => string | null;
126
+ namespaceFromPath: (p: string) => string;
127
+ }) {
128
+ this.getConfig = options.getConfig;
129
+ this.storageFor = options.storageFor;
130
+ this.storageDirNamespace = options.storageDirNamespace;
131
+ this.qmdCollectionNamespaceFromPrefix =
132
+ options.qmdCollectionNamespaceFromPrefix;
133
+ this.namespaceFromPath = options.namespaceFromPath;
134
+ }
135
+
136
+ async readQmdResultMemory(
137
+ resultPath: string,
138
+ fallbackStorage: StorageManager,
139
+ recallNamespaces: readonly string[] = [],
140
+ ): Promise<MemoryFile | null> {
141
+ const parts = qmdCollectionPathParts(resultPath);
142
+ const fallbackStorageDir = storageDirOrNull(fallbackStorage);
143
+ const config = this.getConfig();
144
+ const coldCollection = config.qmdColdCollection ?? "openclaw-engram-cold";
145
+ if (parts && parts.collection === coldCollection) {
146
+ const storages: StorageManager[] = [];
147
+ const seenStorageDirs = new Set<string>();
148
+ const addStorage = (storage: StorageManager): void => {
149
+ const storageDir = storageDirOrNull(storage);
150
+ const storageKey = storageDir
151
+ ? path.resolve(storageDir)
152
+ : `storage-without-dir-${storages.length}`;
153
+ if (seenStorageDirs.has(storageKey)) return;
154
+ seenStorageDirs.add(storageKey);
155
+ storages.push(storage);
156
+ };
157
+
158
+ const fallbackNamespace =
159
+ fallbackStorageDir !== null
160
+ ? this.storageDirNamespace(fallbackStorageDir)
161
+ : config.defaultNamespace;
162
+ if (
163
+ recallNamespaces.length === 0 ||
164
+ !resolveNamespaceCapabilities(config).namespaces ||
165
+ recallNamespaces.includes(fallbackNamespace)
166
+ ) {
167
+ addStorage(fallbackStorage);
168
+ }
169
+
170
+ if (recallNamespaces.length > 0) {
171
+ for (const namespace of recallNamespaces) {
172
+ try {
173
+ addStorage(await this.storageFor(namespace));
174
+ } catch (err) {
175
+ log.debug("qmd cold result namespace storage lookup skipped", {
176
+ path: resultPath,
177
+ namespace,
178
+ error: (err as Error).message,
179
+ });
180
+ }
181
+ }
182
+ }
183
+
184
+ for (const storage of storages) {
185
+ const storageDir = storageDirOrNull(storage);
186
+ if (!storageDir) {
187
+ const memory = await storage.readMemoryByPath(resultPath);
188
+ if (memory) return memory;
189
+ continue;
190
+ }
191
+ try {
192
+ const coldRoot = path.join(storageDir, "cold");
193
+ for (const candidate of qmdResultPathCandidates(
194
+ coldRoot,
195
+ parts.relativePath,
196
+ )) {
197
+ const memory = await storage.readMemoryByPath(candidate);
198
+ if (memory) return memory;
199
+ }
200
+ } catch (err) {
201
+ if (err instanceof SecureStoreLockedError) throw err;
202
+ log.debug("qmd cold result path lookup failed open", {
203
+ path: resultPath,
204
+ collection: coldCollection,
205
+ error: (err as Error).message,
206
+ });
207
+ }
208
+ }
209
+ return null;
210
+ }
211
+ const collectionNamespace = parts
212
+ ? this.qmdCollectionNamespaceFromPrefix(parts.collection)
213
+ : null;
214
+
215
+ if (parts && collectionNamespace) {
216
+ try {
217
+ const collectionStorage = await this.storageFor(collectionNamespace);
218
+ for (const candidate of qmdResultPathCandidates(
219
+ collectionStorage.dir,
220
+ parts.relativePath,
221
+ )) {
222
+ const memory = await collectionStorage.readMemoryByPath(candidate);
223
+ if (memory) return memory;
224
+ }
225
+ return null;
226
+ } catch (err) {
227
+ if (err instanceof SecureStoreLockedError) throw err;
228
+ log.debug("qmd result namespace path lookup failed open", {
229
+ path: resultPath,
230
+ namespace: collectionNamespace,
231
+ error: (err as Error).message,
232
+ });
233
+ return null;
234
+ }
235
+ }
236
+
237
+ if (path.isAbsolute(resultPath)) {
238
+ if (!fallbackStorageDir) {
239
+ return await fallbackStorage.readMemoryByPath(resultPath);
240
+ }
241
+ const ownerStorage = await this.storageForAbsoluteQmdResultPath(
242
+ resultPath,
243
+ fallbackStorage,
244
+ recallNamespaces,
245
+ );
246
+ if (!ownerStorage) return null;
247
+ for (const candidate of qmdResultPathCandidates(
248
+ ownerStorage.dir,
249
+ resultPath,
250
+ )) {
251
+ const memory = await ownerStorage.storage.readMemoryByPath(candidate);
252
+ if (memory) return memory;
253
+ }
254
+ return null;
255
+ }
256
+
257
+ if (!fallbackStorageDir) {
258
+ return await fallbackStorage.readMemoryByPath(resultPath);
259
+ }
260
+ for (const candidate of qmdResultPathCandidates(
261
+ fallbackStorageDir,
262
+ resultPath,
263
+ )) {
264
+ const memory = await fallbackStorage.readMemoryByPath(candidate);
265
+ if (memory) return memory;
266
+ }
267
+ return null;
268
+ }
269
+
270
+ async resolveColdQmdResultForRecall(
271
+ result: QmdSearchResult,
272
+ fallbackStorage: StorageManager,
273
+ recallNamespaces: readonly string[] = [],
274
+ ): Promise<{ namespace: string; result: QmdSearchResult } | null> {
275
+ const memory = await this.readQmdResultMemory(
276
+ result.path,
277
+ fallbackStorage,
278
+ recallNamespaces,
279
+ );
280
+ if (!memory) return null;
281
+
282
+ let ownerNamespace: string | null = null;
283
+ if (path.isAbsolute(memory.path)) {
284
+ const ownerStorage = await this.storageForAbsoluteQmdResultPath(
285
+ memory.path,
286
+ fallbackStorage,
287
+ recallNamespaces,
288
+ );
289
+ ownerNamespace = ownerStorage?.namespace ?? null;
290
+ if (!ownerNamespace && resolveNamespaceCapabilities(this.getConfig()).namespaces) return null;
291
+ }
292
+ ownerNamespace ??= this.namespaceFromPath(memory.path);
293
+ if (
294
+ recallNamespaces.length > 0 &&
295
+ !recallNamespaces.includes(ownerNamespace)
296
+ ) {
297
+ return null;
298
+ }
299
+
300
+ return {
301
+ namespace: ownerNamespace,
302
+ result: {
303
+ ...result,
304
+ docid: result.docid || memory.frontmatter.id,
305
+ path: memory.path,
306
+ snippet: result.snippet || memory.content.slice(0, 400),
307
+ },
308
+ };
309
+ }
310
+
311
+ async storageForAbsoluteQmdResultPath(
312
+ resultPath: string,
313
+ fallbackStorage: StorageManager,
314
+ recallNamespaces: readonly string[] = [],
315
+ ): Promise<{ storage: StorageManager; dir: string; namespace: string } | null> {
316
+ const resolvedPath = path.resolve(resultPath);
317
+ const config = this.getConfig();
318
+ const memoryRoot = path.resolve(config.memoryDir);
319
+ const namespacesRoot = path.join(memoryRoot, "namespaces");
320
+ const fallbackStorageDir = storageDirOrNull(fallbackStorage);
321
+ const matches: Array<{ storage: StorageManager; dir: string; namespace: string }> = [];
322
+ const seenDirs = new Set<string>();
323
+
324
+ const maybeAddStorage = (storage: StorageManager, namespace: string) => {
325
+ const storageDir = storageDirOrNull(storage);
326
+ if (!storageDir) return;
327
+ const candidateRoot = path.resolve(storageDir);
328
+ if (seenDirs.has(candidateRoot)) return;
329
+ if (!isPathInsideStorageRoot(candidateRoot, resolvedPath)) return;
330
+ if (
331
+ candidateRoot === memoryRoot &&
332
+ isPathInsideStorageRoot(namespacesRoot, resolvedPath)
333
+ ) {
334
+ return;
335
+ }
336
+ seenDirs.add(candidateRoot);
337
+ matches.push({ storage, dir: candidateRoot, namespace });
338
+ };
339
+
340
+ const fallbackNamespace =
341
+ fallbackStorageDir !== null
342
+ ? this.storageDirNamespace(fallbackStorageDir)
343
+ : config.defaultNamespace;
344
+ maybeAddStorage(fallbackStorage, fallbackNamespace);
345
+
346
+ const candidateNamespaces = new Set<string>();
347
+ candidateNamespaces.add(config.defaultNamespace);
348
+ candidateNamespaces.add(config.sharedNamespace);
349
+ for (const ns of recallNamespaces) {
350
+ candidateNamespaces.add(ns);
351
+ }
352
+ if (isPathInsideStorageRoot(namespacesRoot, resolvedPath)) {
353
+ const relativeToNamespaces = path.relative(namespacesRoot, resolvedPath);
354
+ const [namespaceSegment] = relativeToNamespaces.split(/[\\/]/);
355
+ if (namespaceSegment) {
356
+ candidateNamespaces.add(
357
+ namespaceIdentityFromToken(namespaceSegment) ?? namespaceSegment,
358
+ );
359
+ }
360
+ }
361
+ for (const policy of config.namespacePolicies ?? []) {
362
+ candidateNamespaces.add(policy.name);
363
+ }
364
+
365
+ for (const ns of candidateNamespaces) {
366
+ if (!ns) continue;
367
+ try {
368
+ maybeAddStorage(await this.storageFor(ns), ns);
369
+ } catch {
370
+ continue;
371
+ }
372
+ }
373
+
374
+ matches.sort((a, b) => b.dir.length - a.dir.length);
375
+ return matches[0] ?? null;
376
+ }
377
+ }