@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.
- package/dist/access-boundary.d.ts +2 -2
- package/dist/access-cli.js +1 -1
- package/dist/access-http.d.ts +2 -2
- package/dist/access-mcp.d.ts +2 -2
- package/dist/access-operations.d.ts +2 -2
- package/dist/{access-service-CdY88L86.d.ts → access-service-CH1aqqb1.d.ts} +1 -1
- package/dist/access-service.d.ts +2 -2
- package/dist/access-surface-catalog.d.ts +2 -2
- package/dist/bootstrap.d.ts +1 -1
- package/dist/{chunk-YQX63P3W.js → chunk-ZWGXZ7ID.js} +13015 -12267
- package/dist/chunk-ZWGXZ7ID.js.map +1 -0
- package/dist/{cli-BNJtn0R0.d.ts → cli-CE5olss2.d.ts} +2 -2
- package/dist/cli.d.ts +3 -3
- package/dist/explicit-capture.d.ts +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +1 -1
- package/dist/mcp-memory-inspector-app.d.ts +2 -2
- package/dist/{orchestrator-BBGXLd20.d.ts → orchestrator-CIOqdNzF.d.ts} +88 -90
- package/dist/orchestrator.d.ts +1 -1
- package/dist/orchestrator.js +27 -3
- package/package.json +2 -2
- package/src/orchestration/consolidation-run.ts +633 -0
- package/src/orchestration/orchestrator-init.ts +712 -0
- package/src/orchestration/recall-internal.ts +5479 -0
- package/src/orchestration/recall-introspection.ts +709 -0
- package/src/orchestration/recall-search-pipeline.ts +1710 -0
- package/src/orchestration/turn-ingestion.ts +792 -0
- package/src/orchestrator.ts +472 -8625
- package/dist/chunk-YQX63P3W.js.map +0 -1
|
@@ -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
|
+
}
|