@remnic/core 9.3.745 → 9.3.747

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.
@@ -0,0 +1,709 @@
1
+ /**
2
+ * Recall-introspection coordinator — extracted from the orchestrator
3
+ * (issue #1526, seam 21).
4
+ *
5
+ * Owns the recall observability surfaces:
6
+ * - last-intent / last-graph-recall / last-QMD-recall snapshot
7
+ * recording, reading, and explain rendering
8
+ * - console faithfulness distribution
9
+ * - background direct-answer tier annotation (observation mode, #518)
10
+ *
11
+ * Behavior-preserving move from orchestrator.ts. The orchestrator keeps
12
+ * thin delegating methods; all member access flows back through
13
+ * RecallIntrospectionDeps live accessors/arrows (late-binding rule,
14
+ * seams 18–20).
15
+ */
16
+
17
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
18
+ import path from "node:path";
19
+ import { type CapabilitySet, resolveRecallAuxiliaryCapabilities } from "../capabilities.js";
20
+ import { type DirectAnswerSources, tryDirectAnswer } from "../direct-answer-wiring.js";
21
+ import type { FaithfulnessGateCounters } from "../extraction-faithfulness.js";
22
+ import { StorageManager } from "../index.js";
23
+ import { log } from "../logger.js";
24
+ import { NamespaceStorageRouter } from "../namespaces/storage.js";
25
+ import { GraphRecallCoordinator, type GraphRecallRankedResult, type GraphRecallShadowComparison } from "./graph-recall-coordinator.js";
26
+ import { buildRecallQueryPolicy } from "../recall-query-policy.js";
27
+ import { type GraphRecallExpandedEntry, LastRecallStore, clampGraphRecallExpandedEntries } from "../recall-state.js";
28
+ import { resolveScopePlan } from "../scopes/scope-plan.js";
29
+ import { DEFAULT_TAXONOMY } from "../taxonomy/index.js";
30
+ import { listTrustZoneRecords } from "../trust-zones.js";
31
+ import type { CodingContext, MemoryFile, MemoryIntent, PluginConfig, RecallPlanMode, RecallTierExplain } from "../types.js";
32
+ import {
33
+ parseGraphRecallRankedResults,
34
+ parseMemoryIntentSnapshot,
35
+ parseQmdRecallResults,
36
+ type GraphRecallSnapshot,
37
+ type IntentDebugSnapshot,
38
+ type QmdRecallSnapshot,
39
+ } from "../orchestrator.js";
40
+
41
+ export interface RecallIntrospectionDeps {
42
+ annotateDirectAnswerTier(
43
+ prompt: string,
44
+ sessionKey: string,
45
+ namespaces: string[],
46
+ expectedIdentity:
47
+ | { writeNonce?: string; traceId?: string; recordedAt?: string }
48
+ | undefined,
49
+ caps: CapabilitySet,
50
+ _parentAbortSignal?: AbortSignal,
51
+ ): Promise<void>;
52
+ readonly config: PluginConfig;
53
+ directAnswerObservationChain: Promise<void>;
54
+ effectiveCronRecallInstructionHeavyTokenCap(): number;
55
+ readonly faithfulnessCounters: FaithfulnessGateCounters;
56
+ getCodingContextForSession(sessionKey: string | undefined): CodingContext | null;
57
+ getLastGraphRecallSnapshot(
58
+ namespace?: string,
59
+ ): Promise<GraphRecallSnapshot | null>;
60
+ getLastIntentSnapshot(
61
+ namespace?: string,
62
+ ): Promise<IntentDebugSnapshot | null>;
63
+ getLastQmdRecallSnapshot(
64
+ namespace?: string,
65
+ ): Promise<QmdRecallSnapshot | null>;
66
+ getStorage(namespace?: string): Promise<StorageManager>;
67
+ readonly graphRecallCoordinator: GraphRecallCoordinator;
68
+ readonly lastRecall: LastRecallStore;
69
+ resolveStateDirForNamespace(
70
+ namespace: string,
71
+ ): Promise<string>;
72
+ readonly storageRouter: NamespaceStorageRouter;
73
+ }
74
+
75
+ export class RecallIntrospectionCoordinator {
76
+ constructor(
77
+ private readonly deps: RecallIntrospectionDeps,
78
+ ) {}
79
+
80
+ /**
81
+ * Faithfulness gate verdict distribution (issue #1576). Consumed by the
82
+ * console-state aggregator so `remnic doctor` can render how the gate is
83
+ * performing. Returns a fresh object so callers cannot mutate the
84
+ * internal counters. Returns `undefined` when the gate is off so the
85
+ * console-state snapshot omits the faithfulness block entirely (cursor
86
+ * review: an all-zero block would otherwise always be truthy and leak
87
+ * into snapshots that document it as absent-when-off).
88
+ */
89
+ getConsoleFaithfulnessDistribution(): FaithfulnessGateCounters | undefined {
90
+ if (this.deps.config.extractionFaithfulnessGate === "off") return undefined;
91
+ return { ...this.deps.faithfulnessCounters };
92
+ }
93
+
94
+ async getLastGraphRecallSnapshot(
95
+ namespace?: string,
96
+ ): Promise<GraphRecallSnapshot | null> {
97
+ const storage = await this.deps.getStorage(namespace);
98
+ const snapshotPath = path.join(
99
+ storage.dir,
100
+ "state",
101
+ "last_graph_recall.json",
102
+ );
103
+ try {
104
+ const raw = await readFile(snapshotPath, "utf-8");
105
+ const parsed = JSON.parse(raw) as Partial<GraphRecallSnapshot>;
106
+ if (!parsed || typeof parsed !== "object") return null;
107
+ return {
108
+ recordedAt:
109
+ typeof parsed.recordedAt === "string" ? parsed.recordedAt : "",
110
+ mode: typeof parsed.mode === "string" ? parsed.mode : "full",
111
+ queryHash: typeof parsed.queryHash === "string" ? parsed.queryHash : "",
112
+ queryLength:
113
+ typeof parsed.queryLength === "number" ? parsed.queryLength : 0,
114
+ namespaces: Array.isArray(parsed.namespaces)
115
+ ? parsed.namespaces.filter((v): v is string => typeof v === "string")
116
+ : [],
117
+ seedCount: typeof parsed.seedCount === "number" ? parsed.seedCount : 0,
118
+ expandedCount:
119
+ typeof parsed.expandedCount === "number" ? parsed.expandedCount : 0,
120
+ seeds: Array.isArray(parsed.seeds)
121
+ ? parsed.seeds.filter((v): v is string => typeof v === "string")
122
+ : [],
123
+ expanded: clampGraphRecallExpandedEntries(parsed.expanded, 64),
124
+ status:
125
+ parsed.status === "completed" ||
126
+ parsed.status === "skipped" ||
127
+ parsed.status === "aborted"
128
+ ? parsed.status
129
+ : undefined,
130
+ reason: typeof parsed.reason === "string" ? parsed.reason : undefined,
131
+ shadowMode: parsed.shadowMode === true,
132
+ queryIntent: parseMemoryIntentSnapshot(parsed.queryIntent),
133
+ seedResults: parseGraphRecallRankedResults(parsed.seedResults),
134
+ finalResults: parseGraphRecallRankedResults(parsed.finalResults),
135
+ shadowComparison:
136
+ parsed.shadowComparison && typeof parsed.shadowComparison === "object"
137
+ ? {
138
+ baselineCount:
139
+ typeof parsed.shadowComparison.baselineCount === "number"
140
+ ? parsed.shadowComparison.baselineCount
141
+ : 0,
142
+ graphCount:
143
+ typeof parsed.shadowComparison.graphCount === "number"
144
+ ? parsed.shadowComparison.graphCount
145
+ : 0,
146
+ overlapCount:
147
+ typeof parsed.shadowComparison.overlapCount === "number"
148
+ ? parsed.shadowComparison.overlapCount
149
+ : 0,
150
+ overlapRatio:
151
+ typeof parsed.shadowComparison.overlapRatio === "number"
152
+ ? parsed.shadowComparison.overlapRatio
153
+ : 0,
154
+ averageOverlapDelta:
155
+ typeof parsed.shadowComparison.averageOverlapDelta ===
156
+ "number"
157
+ ? parsed.shadowComparison.averageOverlapDelta
158
+ : 0,
159
+ }
160
+ : undefined,
161
+ };
162
+ } catch {
163
+ return null;
164
+ }
165
+ }
166
+
167
+ async getLastIntentSnapshot(
168
+ namespace?: string,
169
+ ): Promise<IntentDebugSnapshot | null> {
170
+ const storage = await this.deps.getStorage(namespace);
171
+ const snapshotPath = path.join(storage.dir, "state", "last_intent.json");
172
+ try {
173
+ const raw = await readFile(snapshotPath, "utf-8");
174
+ const parsed = JSON.parse(raw) as Partial<IntentDebugSnapshot>;
175
+ if (!parsed || typeof parsed !== "object") return null;
176
+ const graphDecision =
177
+ parsed.graphDecision && typeof parsed.graphDecision === "object"
178
+ ? parsed.graphDecision
179
+ : undefined;
180
+ return {
181
+ recordedAt:
182
+ typeof parsed.recordedAt === "string" ? parsed.recordedAt : "",
183
+ promptHash:
184
+ typeof parsed.promptHash === "string" ? parsed.promptHash : "",
185
+ promptLength:
186
+ typeof parsed.promptLength === "number" ? parsed.promptLength : 0,
187
+ retrievalQueryHash:
188
+ typeof parsed.retrievalQueryHash === "string"
189
+ ? parsed.retrievalQueryHash
190
+ : "",
191
+ retrievalQueryLength:
192
+ typeof parsed.retrievalQueryLength === "number"
193
+ ? parsed.retrievalQueryLength
194
+ : 0,
195
+ plannerEnabled: parsed.plannerEnabled !== false,
196
+ plannedMode:
197
+ parsed.plannedMode === "no_recall" ||
198
+ parsed.plannedMode === "minimal" ||
199
+ parsed.plannedMode === "full" ||
200
+ parsed.plannedMode === "graph_mode"
201
+ ? parsed.plannedMode
202
+ : "full",
203
+ effectiveMode:
204
+ parsed.effectiveMode === "no_recall" ||
205
+ parsed.effectiveMode === "minimal" ||
206
+ parsed.effectiveMode === "full" ||
207
+ parsed.effectiveMode === "graph_mode"
208
+ ? parsed.effectiveMode
209
+ : "full",
210
+ recallResultLimit:
211
+ typeof parsed.recallResultLimit === "number"
212
+ ? parsed.recallResultLimit
213
+ : 0,
214
+ queryIntent: parseMemoryIntentSnapshot(parsed.queryIntent),
215
+ graphExpandedIntentDetected:
216
+ parsed.graphExpandedIntentDetected === true,
217
+ graphDecision: {
218
+ status:
219
+ graphDecision?.status === "skipped" ||
220
+ graphDecision?.status === "completed" ||
221
+ graphDecision?.status === "aborted"
222
+ ? graphDecision.status
223
+ : "not_requested",
224
+ reason:
225
+ typeof graphDecision?.reason === "string"
226
+ ? graphDecision.reason
227
+ : undefined,
228
+ shadowMode: graphDecision?.shadowMode === true,
229
+ qmdAvailable: graphDecision?.qmdAvailable !== false,
230
+ graphRecallEnabled: graphDecision?.graphRecallEnabled !== false,
231
+ multiGraphMemoryEnabled:
232
+ graphDecision?.multiGraphMemoryEnabled !== false,
233
+ },
234
+ };
235
+ } catch {
236
+ return null;
237
+ }
238
+ }
239
+
240
+ async getLastQmdRecallSnapshot(
241
+ namespace?: string,
242
+ ): Promise<QmdRecallSnapshot | null> {
243
+ const storage = await this.deps.getStorage(namespace);
244
+ const snapshotPath = path.join(
245
+ storage.dir,
246
+ "state",
247
+ "last_qmd_recall.json",
248
+ );
249
+ try {
250
+ const raw = await readFile(snapshotPath, "utf-8");
251
+ const parsed = JSON.parse(raw) as Partial<QmdRecallSnapshot>;
252
+ if (!parsed || typeof parsed !== "object") return null;
253
+ return {
254
+ recordedAt:
255
+ typeof parsed.recordedAt === "string" ? parsed.recordedAt : "",
256
+ queryHash: typeof parsed.queryHash === "string" ? parsed.queryHash : "",
257
+ queryLength:
258
+ typeof parsed.queryLength === "number" ? parsed.queryLength : 0,
259
+ collection:
260
+ typeof parsed.collection === "string" ? parsed.collection : undefined,
261
+ namespaces: Array.isArray(parsed.namespaces)
262
+ ? parsed.namespaces.filter(
263
+ (value): value is string => typeof value === "string",
264
+ )
265
+ : [],
266
+ fetchLimit:
267
+ typeof parsed.fetchLimit === "number" ? parsed.fetchLimit : 0,
268
+ primaryResultCount:
269
+ typeof parsed.primaryResultCount === "number"
270
+ ? parsed.primaryResultCount
271
+ : 0,
272
+ hybridResultCount:
273
+ typeof parsed.hybridResultCount === "number"
274
+ ? parsed.hybridResultCount
275
+ : 0,
276
+ queryAwareSeedCount:
277
+ typeof parsed.queryAwareSeedCount === "number"
278
+ ? parsed.queryAwareSeedCount
279
+ : 0,
280
+ resultCount:
281
+ typeof parsed.resultCount === "number" ? parsed.resultCount : 0,
282
+ intentHint:
283
+ typeof parsed.intentHint === "string" ? parsed.intentHint : undefined,
284
+ explainEnabled: parsed.explainEnabled === true,
285
+ hybridTopUpUsed: parsed.hybridTopUpUsed === true,
286
+ hybridTopUpSkippedReason:
287
+ typeof parsed.hybridTopUpSkippedReason === "string"
288
+ ? parsed.hybridTopUpSkippedReason
289
+ : undefined,
290
+ results: parseQmdRecallResults(parsed.results),
291
+ };
292
+ } catch {
293
+ return null;
294
+ }
295
+ }
296
+
297
+ async explainLastGraphRecall(options?: {
298
+ namespace?: string;
299
+ maxExpanded?: number;
300
+ }): Promise<string> {
301
+ const snapshot = await this.deps.getLastGraphRecallSnapshot(options?.namespace);
302
+ if (!snapshot) return "No graph-recall snapshot found yet.";
303
+ const maxExpanded = Math.max(1, Math.min(50, options?.maxExpanded ?? 10));
304
+ const expanded = snapshot.expanded.slice(0, maxExpanded);
305
+ const seedResults = (snapshot.seedResults ?? []).slice(0, maxExpanded);
306
+ const finalResults = (snapshot.finalResults ?? []).slice(0, maxExpanded);
307
+ const queryIntent = snapshot.queryIntent ?? {
308
+ goal: "unknown",
309
+ actionType: "unknown",
310
+ entityTypes: [],
311
+ };
312
+ return [
313
+ "## Last Graph Recall",
314
+ "",
315
+ `Recorded at: ${snapshot.recordedAt || "unknown"}`,
316
+ `Mode: ${snapshot.mode}`,
317
+ `Status: ${snapshot.status ?? "completed"}${snapshot.shadowMode ? " (shadow)" : ""}`,
318
+ `Reason: ${snapshot.reason ?? "n/a"}`,
319
+ `Query hash: ${snapshot.queryHash || "unknown"} (len=${snapshot.queryLength})`,
320
+ `Query intent: goal=${queryIntent.goal}, action=${queryIntent.actionType}, entityTypes=${queryIntent.entityTypes.length > 0 ? queryIntent.entityTypes.join(", ") : "none"}`,
321
+ `Namespaces: ${snapshot.namespaces.length > 0 ? snapshot.namespaces.join(", ") : "none"}`,
322
+ `Seed results (${snapshot.seedResults?.length ?? 0}, showing ${seedResults.length}):`,
323
+ ...seedResults.map(
324
+ (entry) =>
325
+ `- ${entry.path} (score=${entry.score.toFixed(3)}, sources=${entry.sourceLabels.join(",") || "baseline"})`,
326
+ ),
327
+ `Seed paths (${snapshot.seedCount}):`,
328
+ ...snapshot.seeds.map((p) => `- ${p}`),
329
+ `Expanded paths (${snapshot.expandedCount}, showing ${expanded.length}):`,
330
+ ...expanded.map((e) => {
331
+ // Issue #681 PR 3/3 — surface per-edge confidence in the
332
+ // graph-explain document. Legacy snapshots without
333
+ // `edgeConfidence` render as `conf=n/a` so older payloads
334
+ // remain readable.
335
+ const confLabel =
336
+ typeof e.edgeConfidence === "number" && Number.isFinite(e.edgeConfidence)
337
+ ? e.edgeConfidence.toFixed(2)
338
+ : "n/a";
339
+ return `- ${e.path} (score=${e.score.toFixed(3)}, ns=${e.namespace}, seed=${e.seed || "unknown"}, hop=${e.hopDepth}, w=${e.decayedWeight.toFixed(3)}, type=${e.graphType}, conf=${confLabel})`;
340
+ }),
341
+ `Final ranked results (${snapshot.finalResults?.length ?? 0}, showing ${finalResults.length}):`,
342
+ ...finalResults.map(
343
+ (entry) =>
344
+ `- ${entry.path} (score=${entry.score.toFixed(3)}, sources=${entry.sourceLabels.join(",") || "baseline"})`,
345
+ ),
346
+ ...(snapshot.shadowComparison
347
+ ? [
348
+ `Shadow comparison: baseline=${snapshot.shadowComparison.baselineCount}, graph=${snapshot.shadowComparison.graphCount}, overlap=${snapshot.shadowComparison.overlapCount} (${snapshot.shadowComparison.overlapRatio.toFixed(2)}), avgDelta=${snapshot.shadowComparison.averageOverlapDelta.toFixed(3)}`,
349
+ ]
350
+ : []),
351
+ ].join("\n");
352
+ }
353
+
354
+ async explainLastIntent(options?: { namespace?: string }): Promise<string> {
355
+ const snapshot = await this.deps.getLastIntentSnapshot(options?.namespace);
356
+ if (!snapshot) return "No intent-debug snapshot found yet.";
357
+ return [
358
+ "## Last Intent Debug",
359
+ "",
360
+ `Recorded at: ${snapshot.recordedAt || "unknown"}`,
361
+ `Prompt hash: ${snapshot.promptHash || "unknown"} (len=${snapshot.promptLength})`,
362
+ `Retrieval query hash: ${snapshot.retrievalQueryHash || "unknown"} (len=${snapshot.retrievalQueryLength})`,
363
+ `Planner enabled: ${snapshot.plannerEnabled ? "yes" : "no"}`,
364
+ `Planned mode: ${snapshot.plannedMode}`,
365
+ `Effective mode: ${snapshot.effectiveMode}`,
366
+ `Recall result limit: ${snapshot.recallResultLimit}`,
367
+ `Query intent: goal=${snapshot.queryIntent.goal}, action=${snapshot.queryIntent.actionType}, entityTypes=${snapshot.queryIntent.entityTypes.length > 0 ? snapshot.queryIntent.entityTypes.join(", ") : "none"}`,
368
+ `Broad graph intent: ${snapshot.graphExpandedIntentDetected ? "yes" : "no"}`,
369
+ `Graph decision: status=${snapshot.graphDecision.status}, reason=${snapshot.graphDecision.reason ?? "n/a"}, shadow=${snapshot.graphDecision.shadowMode ? "yes" : "no"}, qmdAvailable=${snapshot.graphDecision.qmdAvailable ? "yes" : "no"}, graphRecallEnabled=${snapshot.graphDecision.graphRecallEnabled ? "yes" : "no"}, multiGraphMemoryEnabled=${snapshot.graphDecision.multiGraphMemoryEnabled ? "yes" : "no"}`,
370
+ ].join("\n");
371
+ }
372
+
373
+ async explainLastQmdRecall(options?: {
374
+ namespace?: string;
375
+ maxResults?: number;
376
+ }): Promise<string> {
377
+ const snapshot = await this.deps.getLastQmdRecallSnapshot(options?.namespace);
378
+ if (!snapshot) return "No QMD recall snapshot found yet.";
379
+ const maxResults = Math.max(1, Math.min(25, options?.maxResults ?? 10));
380
+ const shown = snapshot.results.slice(0, maxResults);
381
+ return [
382
+ "## Last QMD Recall",
383
+ "",
384
+ `Recorded at: ${snapshot.recordedAt || "unknown"}`,
385
+ `Query hash: ${snapshot.queryHash || "unknown"} (len=${snapshot.queryLength})`,
386
+ `Collection: ${snapshot.collection ?? "default"}`,
387
+ `Namespaces: ${snapshot.namespaces.length > 0 ? snapshot.namespaces.join(", ") : "none"}`,
388
+ `Fetch limit: ${snapshot.fetchLimit}`,
389
+ `Primary results: ${snapshot.primaryResultCount}`,
390
+ `Hybrid top-up results: ${snapshot.hybridResultCount}`,
391
+ `Query-aware seeds: ${snapshot.queryAwareSeedCount}`,
392
+ `Final results: ${snapshot.resultCount}`,
393
+ `Intent hint: ${snapshot.intentHint ?? "none"}`,
394
+ `Explain enabled: ${snapshot.explainEnabled ? "yes" : "no"}`,
395
+ `Hybrid top-up used: ${snapshot.hybridTopUpUsed ? "yes" : "no"}`,
396
+ `Hybrid top-up skipped reason: ${snapshot.hybridTopUpSkippedReason ?? "n/a"}`,
397
+ `Top results (${shown.length}):`,
398
+ ...shown.map((result) => {
399
+ const explainParts = [
400
+ typeof result.explain?.blendedScore === "number"
401
+ ? `blended=${result.explain.blendedScore.toFixed(3)}`
402
+ : null,
403
+ typeof result.explain?.rerankScore === "number"
404
+ ? `rerank=${result.explain.rerankScore.toFixed(3)}`
405
+ : null,
406
+ typeof result.explain?.rrf === "number"
407
+ ? `rrf=${result.explain.rrf.toFixed(3)}`
408
+ : null,
409
+ ].filter((entry): entry is string => Boolean(entry));
410
+ const explainText =
411
+ explainParts.length > 0 ? `, explain=${explainParts.join("/")}` : "";
412
+ return `- ${result.path} (score=${result.score.toFixed(3)}, transport=${result.transport ?? "unknown"}${explainText})`;
413
+ }),
414
+ ].join("\n");
415
+ }
416
+
417
+ /**
418
+ * Await the in-flight observation-mode direct-answer annotation chain.
419
+ * Resolves to true when settled, false on timeout.
420
+ */
421
+ async waitForDirectAnswerObservationIdle(
422
+ timeoutMs: number = 60_000,
423
+ ): Promise<boolean> {
424
+ let timeoutHandle: NodeJS.Timeout | null = null;
425
+ try {
426
+ const timeoutPromise = new Promise<"timeout">((resolve) => {
427
+ timeoutHandle = setTimeout(() => resolve("timeout"), timeoutMs);
428
+ });
429
+ const result = await Promise.race([
430
+ this.deps.directAnswerObservationChain.then(() => "ok" as const),
431
+ timeoutPromise,
432
+ ]);
433
+ if (result === "timeout") {
434
+ log.warn(
435
+ `waitForDirectAnswerObservationIdle timed out after ${timeoutMs}ms`,
436
+ );
437
+ return false;
438
+ }
439
+ return true;
440
+ } finally {
441
+ if (timeoutHandle) clearTimeout(timeoutHandle);
442
+ }
443
+ }
444
+
445
+ enqueueDirectAnswerObservation(
446
+ prompt: string,
447
+ sessionKey: string,
448
+ namespaceOverride: string | undefined,
449
+ principalOverride: string | undefined,
450
+ caps: CapabilitySet,
451
+ namespacesEnabled: boolean,
452
+ ): void {
453
+ const expectedSnapshot = this.deps.lastRecall.get(sessionKey);
454
+ if (expectedSnapshot === null) return;
455
+ if (expectedSnapshot.plannerMode === "no_recall") return;
456
+
457
+ // Resolve the observation namespace set through the SAME ScopePlan resolver
458
+ // the main recall path uses (#1521). The observe path does NOT throw on an
459
+ // unreadable override (it falls through to the coding/legacy branches), so
460
+ // we skip the readability gate the recall path enforces.
461
+ const observationScopePlan = resolveScopePlan({
462
+ config: this.deps.config,
463
+ sessionKey,
464
+ namespace: namespaceOverride,
465
+ principalOverride,
466
+ codingContext: sessionKey
467
+ ? this.deps.getCodingContextForSession(sessionKey)
468
+ : null,
469
+ namespacesEnabled,
470
+ });
471
+ const observationNamespaces = observationScopePlan.readNamespaces;
472
+ const observationQueryPolicy = buildRecallQueryPolicy(prompt, sessionKey, {
473
+ cronRecallPolicyEnabled: resolveRecallAuxiliaryCapabilities(this.deps.config).cronRecallPolicy,
474
+ cronRecallNormalizedQueryMaxChars:
475
+ this.deps.config.cronRecallNormalizedQueryMaxChars,
476
+ cronRecallInstructionHeavyTokenCap:
477
+ this.deps.effectiveCronRecallInstructionHeavyTokenCap(),
478
+ cronConversationRecallMode: this.deps.config.cronConversationRecallMode,
479
+ });
480
+ const observationQuery = observationQueryPolicy.retrievalQuery || prompt;
481
+ const expectedIdentity = {
482
+ writeNonce: expectedSnapshot.writeNonce,
483
+ traceId: expectedSnapshot.traceId,
484
+ recordedAt: expectedSnapshot.recordedAt,
485
+ };
486
+ const previous = this.deps.directAnswerObservationChain;
487
+ this.deps.directAnswerObservationChain = previous
488
+ .catch(() => undefined)
489
+ .then(async () => {
490
+ try {
491
+ await this.deps.annotateDirectAnswerTier(
492
+ observationQuery,
493
+ sessionKey,
494
+ observationNamespaces,
495
+ expectedIdentity,
496
+ caps,
497
+ undefined,
498
+ );
499
+ } catch (err) {
500
+ log.debug(`direct-answer observation chain error: ${err}`);
501
+ }
502
+ });
503
+ }
504
+
505
+ async annotateDirectAnswerTier(
506
+ prompt: string,
507
+ sessionKey: string,
508
+ namespaces: string[],
509
+ expectedIdentity:
510
+ | { writeNonce?: string; traceId?: string; recordedAt?: string }
511
+ | undefined,
512
+ caps: CapabilitySet,
513
+ _parentAbortSignal?: AbortSignal,
514
+ ): Promise<void> {
515
+ const tierStart = Date.now();
516
+ try {
517
+ if (namespaces.length === 0) return;
518
+
519
+ const trustZoneByNsAndRecordId = new Map<
520
+ string,
521
+ "quarantine" | "working" | "trusted"
522
+ >();
523
+ const trustZoneKey = (ns: string, recordId: string) =>
524
+ `${ns}\u0000${recordId}`;
525
+ const scopedStorages = new Map<
526
+ string,
527
+ Awaited<ReturnType<typeof this.deps.storageRouter.storageFor>>
528
+ >();
529
+
530
+ for (const ns of namespaces) {
531
+ const storage = await this.deps.storageRouter.storageFor(ns);
532
+ scopedStorages.set(ns, storage);
533
+ const trustZones = await listTrustZoneRecords({
534
+ memoryDir: storage.dir,
535
+ trustZoneStoreDir: this.deps.config.trustZoneStoreDir,
536
+ limit: 200,
537
+ }).catch(() => ({
538
+ allRecords: [] as Array<{
539
+ recordId: string;
540
+ zone: "quarantine" | "working" | "trusted";
541
+ }>,
542
+ }));
543
+ for (const record of trustZones.allRecords ?? []) {
544
+ trustZoneByNsAndRecordId.set(
545
+ trustZoneKey(ns, record.recordId),
546
+ record.zone,
547
+ );
548
+ }
549
+ }
550
+
551
+ const memoryNamespaceByPath = new Map<string, string>();
552
+ const memoryNamespaceById = new Map<string, string>();
553
+ let candidatesConsidered = 0;
554
+
555
+ const sources: DirectAnswerSources = {
556
+ taxonomy: DEFAULT_TAXONOMY,
557
+ listCandidateMemories: async (options: { namespace: string; abortSignal?: AbortSignal }) => {
558
+ const targetNs = options.namespace;
559
+ const storage =
560
+ scopedStorages.get(targetNs) ??
561
+ (await this.deps.storageRouter.storageFor(targetNs));
562
+ const all = await storage.readAllMemories();
563
+ const active: MemoryFile[] = [];
564
+ for (const m of all) {
565
+ if ((m.frontmatter.status ?? "active") === "active") {
566
+ active.push(m);
567
+ memoryNamespaceByPath.set(m.path, targetNs);
568
+ if (m.frontmatter.id) {
569
+ memoryNamespaceById.set(m.frontmatter.id, targetNs);
570
+ }
571
+ }
572
+ }
573
+ candidatesConsidered += active.length;
574
+ return active;
575
+ },
576
+ trustZoneFor: async (memoryId: string) => {
577
+ const ns = memoryNamespaceById.get(memoryId);
578
+ if (!ns) return null;
579
+ return (
580
+ trustZoneByNsAndRecordId.get(
581
+ trustZoneKey(ns, memoryId),
582
+ ) ?? null
583
+ );
584
+ },
585
+ importanceFor: (memory) =>
586
+ typeof memory.frontmatter.importance?.score === "number"
587
+ ? memory.frontmatter.importance.score
588
+ : 0,
589
+ };
590
+
591
+ let result: import("../direct-answer.js").DirectAnswerResult | undefined;
592
+ for (const ns of namespaces) {
593
+ const r = await tryDirectAnswer({
594
+ query: prompt,
595
+ namespace: ns,
596
+ config: this.deps.config,
597
+ enabled: caps.recallDirectAnswer,
598
+ sources,
599
+ });
600
+ if (r.eligible && r.winner) {
601
+ result = r;
602
+ break;
603
+ }
604
+ }
605
+
606
+ if (!result?.eligible || !result?.winner) return;
607
+
608
+ const explain: RecallTierExplain = {
609
+ tier: "direct-answer",
610
+ tierReason: result.narrative,
611
+ filteredBy: result.filteredBy,
612
+ candidatesConsidered,
613
+ latencyMs: Date.now() - tierStart,
614
+ sourceAnchors: [{ path: result.winner.memory.path }],
615
+ };
616
+
617
+ await this.deps.lastRecall.annotateTierExplain(
618
+ sessionKey,
619
+ explain,
620
+ expectedIdentity,
621
+ );
622
+ } catch (err) {
623
+ if (err instanceof Error && err.name === "AbortError") return;
624
+ log.debug(`direct-answer observation failed: ${err}`);
625
+ }
626
+ }
627
+
628
+ // Issue #1526 (seam 14): graph-recall snapshot moved to
629
+ // GraphRecallCoordinator. Thin delegation keeps the private API stable.
630
+ async recordLastGraphRecallSnapshot(options: {
631
+ storage: StorageManager;
632
+ prompt: string;
633
+ recallMode: RecallPlanMode;
634
+ recallNamespaces: string[];
635
+ seedPaths: string[];
636
+ expandedPaths: GraphRecallExpandedEntry[];
637
+ status: "completed" | "skipped" | "aborted";
638
+ reason?: string;
639
+ shadowMode?: boolean;
640
+ queryIntent: MemoryIntent;
641
+ seedResults?: GraphRecallRankedResult[];
642
+ finalResults?: GraphRecallRankedResult[];
643
+ shadowComparison?: GraphRecallShadowComparison;
644
+ }): Promise<void> {
645
+ return this.deps.graphRecallCoordinator.recordLastGraphRecallSnapshot(options);
646
+ }
647
+
648
+ async recordLastIntentSnapshot(options: {
649
+ storage: StorageManager;
650
+ snapshot: IntentDebugSnapshot;
651
+ }): Promise<void> {
652
+ try {
653
+ const snapshotPath = path.join(
654
+ options.storage.dir,
655
+ "state",
656
+ "last_intent.json",
657
+ );
658
+ await mkdir(path.dirname(snapshotPath), { recursive: true });
659
+ await writeFile(
660
+ snapshotPath,
661
+ JSON.stringify(options.snapshot, null, 2),
662
+ "utf-8",
663
+ );
664
+ } catch (err) {
665
+ log.debug(`last intent write failed: ${err}`);
666
+ }
667
+ }
668
+
669
+ async recordLastQmdRecallSnapshot(options: {
670
+ storage: StorageManager;
671
+ snapshot: QmdRecallSnapshot;
672
+ }): Promise<void> {
673
+ try {
674
+ const snapshotPath = path.join(
675
+ options.storage.dir,
676
+ "state",
677
+ "last_qmd_recall.json",
678
+ );
679
+ await mkdir(path.dirname(snapshotPath), { recursive: true });
680
+ await writeFile(
681
+ snapshotPath,
682
+ JSON.stringify(options.snapshot, null, 2),
683
+ "utf-8",
684
+ );
685
+ } catch (err) {
686
+ log.debug(`last qmd recall write failed: ${err}`);
687
+ }
688
+ }
689
+
690
+ async recordLastIntentSnapshotForNamespace(options: {
691
+ namespace: string;
692
+ snapshot: IntentDebugSnapshot;
693
+ }): Promise<void> {
694
+ try {
695
+ const stateDir = await this.deps.resolveStateDirForNamespace(
696
+ options.namespace,
697
+ );
698
+ const snapshotPath = path.join(stateDir, "last_intent.json");
699
+ await mkdir(path.dirname(snapshotPath), { recursive: true });
700
+ await writeFile(
701
+ snapshotPath,
702
+ JSON.stringify(options.snapshot, null, 2),
703
+ "utf-8",
704
+ );
705
+ } catch (err) {
706
+ log.debug(`last intent write failed: ${err}`);
707
+ }
708
+ }
709
+ }