@gmickel/gno 1.18.0 → 1.19.0

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 (129) hide show
  1. package/README.md +5 -3
  2. package/assets/skill/SKILL.md +27 -0
  3. package/package.json +2 -1
  4. package/spec/AGENTS.md +83 -0
  5. package/spec/CLAUDE.md +83 -0
  6. package/spec/bench-fixture.schema.json +137 -0
  7. package/spec/cli.md +2894 -0
  8. package/spec/db/schema.sql +442 -0
  9. package/spec/evals-agentic.md +510 -0
  10. package/spec/evals.md +1106 -0
  11. package/spec/mcp.md +2229 -0
  12. package/spec/output-schemas/activation-verification.schema.json +515 -0
  13. package/spec/output-schemas/ask.schema.json +366 -0
  14. package/spec/output-schemas/backlinks.schema.json +131 -0
  15. package/spec/output-schemas/bench-result.schema.json +120 -0
  16. package/spec/output-schemas/capture-receipt.schema.json +143 -0
  17. package/spec/output-schemas/collection-list.schema.json +45 -0
  18. package/spec/output-schemas/context-capsule-v1.schema.json +691 -0
  19. package/spec/output-schemas/context-capsule-verification.schema.json +1338 -0
  20. package/spec/output-schemas/context-list.schema.json +21 -0
  21. package/spec/output-schemas/doctor.schema.json +313 -0
  22. package/spec/output-schemas/error.schema.json +30 -0
  23. package/spec/output-schemas/expansion.schema.json +37 -0
  24. package/spec/output-schemas/get.schema.json +140 -0
  25. package/spec/output-schemas/graph-query.schema.json +99 -0
  26. package/spec/output-schemas/graph.schema.json +371 -0
  27. package/spec/output-schemas/links-list.schema.json +186 -0
  28. package/spec/output-schemas/mcp-add-collection-result.schema.json +23 -0
  29. package/spec/output-schemas/mcp-capture-result.schema.json +152 -0
  30. package/spec/output-schemas/mcp-http-error.schema.json +30 -0
  31. package/spec/output-schemas/mcp-job-list.schema.json +58 -0
  32. package/spec/output-schemas/mcp-job-status.schema.json +224 -0
  33. package/spec/output-schemas/mcp-remove-result.schema.json +39 -0
  34. package/spec/output-schemas/mcp-sync-result.schema.json +41 -0
  35. package/spec/output-schemas/mcp-tag-result.schema.json +33 -0
  36. package/spec/output-schemas/models-list.schema.json +93 -0
  37. package/spec/output-schemas/multi-get.schema.json +103 -0
  38. package/spec/output-schemas/process-status.schema.json +119 -0
  39. package/spec/output-schemas/query-diagnose.schema.json +123 -0
  40. package/spec/output-schemas/resident-status.schema.json +154 -0
  41. package/spec/output-schemas/retrieval-trace-common.schema.json +492 -0
  42. package/spec/output-schemas/retrieval-trace-delete.schema.json +16 -0
  43. package/spec/output-schemas/retrieval-trace-export.schema.json +61 -0
  44. package/spec/output-schemas/retrieval-trace-filters.schema.json +139 -0
  45. package/spec/output-schemas/retrieval-trace-judgment.schema.json +15 -0
  46. package/spec/output-schemas/retrieval-trace-list.schema.json +18 -0
  47. package/spec/output-schemas/retrieval-trace-payloads.schema.json +178 -0
  48. package/spec/output-schemas/retrieval-trace-purge.schema.json +31 -0
  49. package/spec/output-schemas/retrieval-trace-qrels.schema.json +303 -0
  50. package/spec/output-schemas/retrieval-trace-replay.schema.json +286 -0
  51. package/spec/output-schemas/retrieval-trace-show.schema.json +69 -0
  52. package/spec/output-schemas/retrieval-trace-summary.schema.json +65 -0
  53. package/spec/output-schemas/search-result.schema.json +154 -0
  54. package/spec/output-schemas/search-results.schema.json +338 -0
  55. package/spec/output-schemas/similar.schema.json +84 -0
  56. package/spec/output-schemas/status.schema.json +676 -0
  57. package/spec/output-schemas/tags-list.schema.json +48 -0
  58. package/src/app/context-runtime-types.ts +3 -0
  59. package/src/app/context-runtime.ts +15 -1
  60. package/src/cli/commands/ask.ts +106 -36
  61. package/src/cli/commands/context-build.ts +56 -9
  62. package/src/cli/commands/get.ts +64 -3
  63. package/src/cli/commands/query.ts +62 -23
  64. package/src/cli/commands/replay.ts +140 -0
  65. package/src/cli/commands/search.ts +48 -3
  66. package/src/cli/commands/shared.ts +3 -1
  67. package/src/cli/commands/trace.ts +200 -0
  68. package/src/cli/commands/vsearch.ts +75 -53
  69. package/src/cli/program.ts +255 -0
  70. package/src/config/index.ts +9 -0
  71. package/src/config/retrieval-traces.ts +56 -0
  72. package/src/config/types.ts +4 -0
  73. package/src/core/context-compiler.ts +11 -4
  74. package/src/core/retrieval-qrels.ts +405 -0
  75. package/src/core/retrieval-replay-candidate.ts +368 -0
  76. package/src/core/retrieval-replay-types.ts +109 -0
  77. package/src/core/retrieval-replay-validation.ts +89 -0
  78. package/src/core/retrieval-replay.ts +441 -0
  79. package/src/core/retrieval-trace-evidence-origin.ts +175 -0
  80. package/src/core/retrieval-trace-export.ts +113 -0
  81. package/src/core/retrieval-trace-filter-normalization.ts +27 -0
  82. package/src/core/retrieval-trace-filters.ts +19 -0
  83. package/src/core/retrieval-trace-management-helpers.ts +247 -0
  84. package/src/core/retrieval-trace-management-types.ts +132 -0
  85. package/src/core/retrieval-trace-management.ts +422 -0
  86. package/src/core/retrieval-trace-request.ts +141 -0
  87. package/src/core/retrieval-trace-session.ts +494 -0
  88. package/src/core/retrieval-trace.ts +472 -0
  89. package/src/mcp/tools/context.ts +59 -8
  90. package/src/mcp/tools/get.ts +35 -1
  91. package/src/mcp/tools/index.ts +74 -0
  92. package/src/mcp/tools/query.ts +95 -64
  93. package/src/mcp/tools/search.ts +36 -13
  94. package/src/mcp/tools/trace.ts +143 -0
  95. package/src/mcp/tools/vsearch.ts +71 -38
  96. package/src/pipeline/answer.ts +167 -26
  97. package/src/pipeline/graph-retrieval.ts +15 -1
  98. package/src/pipeline/hybrid.ts +151 -43
  99. package/src/pipeline/search.ts +36 -3
  100. package/src/pipeline/trace-metadata.ts +47 -0
  101. package/src/pipeline/types.ts +43 -0
  102. package/src/pipeline/vsearch.ts +101 -38
  103. package/src/sdk/client.ts +380 -71
  104. package/src/sdk/documents.ts +48 -1
  105. package/src/sdk/index.ts +17 -0
  106. package/src/sdk/types.ts +28 -0
  107. package/src/serve/context-capsule.ts +67 -8
  108. package/src/serve/public/app.tsx +12 -1
  109. package/src/serve/public/globals.built.css +1 -1
  110. package/src/serve/public/lib/workspace-tabs.ts +2 -0
  111. package/src/serve/public/pages/Dashboard.tsx +10 -0
  112. package/src/serve/public/pages/TraceHistory.tsx +478 -0
  113. package/src/serve/public/pages/trace-history-detail.tsx +224 -0
  114. package/src/serve/retrieval-trace.ts +28 -0
  115. package/src/serve/routes/api.ts +366 -72
  116. package/src/serve/routes/traces.ts +156 -0
  117. package/src/serve/server.ts +87 -2
  118. package/src/store/index.ts +31 -0
  119. package/src/store/migrations/014-retrieval-traces.ts +303 -0
  120. package/src/store/migrations/index.ts +2 -0
  121. package/src/store/retrieval-trace-codec.ts +384 -0
  122. package/src/store/sqlite/adapter.ts +153 -1
  123. package/src/store/sqlite/retrieval-trace-management-store.ts +341 -0
  124. package/src/store/sqlite/retrieval-trace-retention.ts +349 -0
  125. package/src/store/sqlite/retrieval-trace-rows.ts +267 -0
  126. package/src/store/sqlite/retrieval-trace-store.ts +515 -0
  127. package/src/store/types.ts +297 -0
  128. package/src/store/vector/sqlite-vec.ts +76 -1
  129. package/src/store/vector/types.ts +1 -1
@@ -0,0 +1,472 @@
1
+ /** Opt-in, local-only retrieval trace recording with privacy projections. */
2
+
3
+ import { z } from "zod";
4
+
5
+ import type {
6
+ RetrievalTraceConfig,
7
+ RetrievalTraceRedactionMode,
8
+ } from "../config/retrieval-traces";
9
+ import type {
10
+ RetrievalTraceAppendResult,
11
+ RetrievalTraceEventInput,
12
+ RetrievalTraceFingerprints,
13
+ RetrievalTraceJudgmentInput,
14
+ RetrievalTraceRunInput,
15
+ RetrievalTraceTerminalStatus,
16
+ StorePort,
17
+ StoreResult,
18
+ } from "../store/types";
19
+
20
+ import {
21
+ parseRetrievalTraceEventInput,
22
+ parseRetrievalTraceJudgmentInput,
23
+ parseRetrievalTraceRunInput,
24
+ } from "../store/retrieval-trace-codec";
25
+ import { err, ok } from "../store/types";
26
+ import { canonicalizeRetrievalTraceFilters } from "./retrieval-trace-filter-normalization";
27
+
28
+ const sha256Schema = z.string().regex(/^[a-f0-9]{64}$/);
29
+ const gnoUriSchema = z
30
+ .string()
31
+ .max(4096)
32
+ .refine((value) => value.startsWith("gno://"), {
33
+ message: "Evidence URI must use the canonical gno:// reader identity",
34
+ });
35
+ const queryModeSchema = z
36
+ .object({
37
+ mode: z.enum(["term", "intent", "hyde"]),
38
+ text: z.string().max(8192),
39
+ })
40
+ .strict();
41
+ export const traceFiltersSchema = z
42
+ .object({
43
+ limit: z.number().int().positive().optional(),
44
+ minScore: z.number().min(0).max(1).optional(),
45
+ collection: z.string().max(256).optional(),
46
+ collections: z.array(z.string().min(1).max(256)).max(1000).optional(),
47
+ lang: z.string().max(64).optional(),
48
+ full: z.boolean().optional(),
49
+ lineNumbers: z.boolean().optional(),
50
+ tagsAll: z.array(z.string().max(512)).max(1000).optional(),
51
+ tagsAny: z.array(z.string().max(512)).max(1000).optional(),
52
+ since: z.string().max(128).optional(),
53
+ until: z.string().max(128).optional(),
54
+ categories: z.array(z.string().max(512)).max(1000).optional(),
55
+ author: z.string().max(1024).optional(),
56
+ intent: z.string().max(8192).optional(),
57
+ exclude: z.array(z.string().max(1024)).max(1000).optional(),
58
+ noExpand: z.boolean().optional(),
59
+ noRerank: z.boolean().optional(),
60
+ candidateLimit: z.number().int().positive().optional(),
61
+ explain: z.boolean().optional(),
62
+ graph: z.boolean().optional(),
63
+ noGraph: z.boolean().optional(),
64
+ queryLanguageHint: z.string().max(64).optional(),
65
+ queryModes: z.array(queryModeSchema).max(100).optional(),
66
+ uriPrefix: z.string().max(4096).optional(),
67
+ })
68
+ .strict();
69
+ const startTraceSchema = z
70
+ .object({
71
+ traceId: z.string().min(1).max(128).optional(),
72
+ query: z.string().min(1).max(8192),
73
+ goal: z.string().max(8192).optional(),
74
+ filters: traceFiltersSchema.default({}),
75
+ fingerprints: z
76
+ .object({
77
+ pipeline: sha256Schema,
78
+ model: sha256Schema,
79
+ config: sha256Schema,
80
+ index: sha256Schema,
81
+ })
82
+ .strict(),
83
+ })
84
+ .strict();
85
+
86
+ const evidenceRefBaseSchema = z
87
+ .object({
88
+ docid: z.string().min(1).max(256).optional(),
89
+ sourceHash: sha256Schema.optional(),
90
+ mirrorHash: sha256Schema.optional(),
91
+ uri: gnoUriSchema.optional(),
92
+ seq: z.number().int().nonnegative().optional(),
93
+ startLine: z.number().int().positive().optional(),
94
+ endLine: z.number().int().positive().optional(),
95
+ score: z.number().finite().optional(),
96
+ rank: z.number().int().positive().optional(),
97
+ plannerRank: z.number().int().positive().optional(),
98
+ passageHash: sha256Schema.optional(),
99
+ sources: z.array(z.string().min(1).max(128)).max(16).optional(),
100
+ graphExpanded: z.boolean().optional(),
101
+ })
102
+ .strict();
103
+ const refineEvidenceRef = (
104
+ value: z.infer<typeof evidenceRefBaseSchema>,
105
+ context: z.RefinementCtx
106
+ ): void => {
107
+ if (!(value.docid || value.sourceHash || value.mirrorHash || value.uri)) {
108
+ context.addIssue({
109
+ code: "custom",
110
+ message: "Evidence references require a stable document identity",
111
+ });
112
+ }
113
+ if ((value.startLine === undefined) !== (value.endLine === undefined)) {
114
+ context.addIssue({
115
+ code: "custom",
116
+ message: "Evidence line ranges require both startLine and endLine",
117
+ });
118
+ }
119
+ if (
120
+ value.startLine !== undefined &&
121
+ value.endLine !== undefined &&
122
+ value.startLine > value.endLine
123
+ ) {
124
+ context.addIssue({
125
+ code: "custom",
126
+ path: ["endLine"],
127
+ message: "Evidence endLine must not precede startLine",
128
+ });
129
+ }
130
+ };
131
+ const evidenceRefSchema = evidenceRefBaseSchema.superRefine(refineEvidenceRef);
132
+ const evidencePayloadSchema = z
133
+ .object({
134
+ evidence: z.array(evidenceRefSchema).max(10_000),
135
+ latencyMs: z.number().finite().nonnegative().optional(),
136
+ })
137
+ .strict();
138
+ const retrievalPayloadSchema = z
139
+ .object({
140
+ ranked: z.array(evidenceRefSchema).max(10_000),
141
+ latencyMs: z.number().finite().nonnegative().optional(),
142
+ capabilities: z.array(z.string().min(1).max(128)).max(100).optional(),
143
+ fallbackCodes: z.array(z.string().min(1).max(128)).max(100).optional(),
144
+ })
145
+ .strict();
146
+ const contextPayloadSchema = evidencePayloadSchema.extend({
147
+ capsuleId: z.string().min(1).max(256),
148
+ });
149
+ const runPayloadSchemas = {
150
+ retrieval: retrievalPayloadSchema,
151
+ context: contextPayloadSchema,
152
+ get: evidencePayloadSchema,
153
+ } as const;
154
+ const eventPayloadSchemas = {
155
+ query: z.object({ filterFingerprint: sha256Schema.optional() }).strict(),
156
+ retrieval: retrievalPayloadSchema,
157
+ context: contextPayloadSchema,
158
+ get: evidencePayloadSchema,
159
+ open: evidencePayloadSchema,
160
+ cite: evidencePayloadSchema,
161
+ pin: evidencePayloadSchema,
162
+ capability: z
163
+ .object({
164
+ capability: z.string().min(1).max(128),
165
+ status: z.enum(["attempted", "used", "unavailable", "failed"]),
166
+ reasonCode: z.string().min(1).max(128).optional(),
167
+ })
168
+ .strict(),
169
+ complete: z
170
+ .object({
171
+ outcome: z.enum(["completed", "partial", "failed", "cancelled"]),
172
+ latencyMs: z.number().finite().nonnegative().optional(),
173
+ })
174
+ .strict(),
175
+ } as const;
176
+ const judgmentTargetSchema = evidenceRefBaseSchema
177
+ .omit({
178
+ score: true,
179
+ rank: true,
180
+ })
181
+ .superRefine(refineEvidenceRef);
182
+
183
+ export interface StartRetrievalTraceInput {
184
+ traceId?: string;
185
+ query: string;
186
+ goal?: string;
187
+ filters?: z.input<typeof traceFiltersSchema>;
188
+ fingerprints: RetrievalTraceFingerprints;
189
+ }
190
+
191
+ export type RetrievalTraceWriteResult =
192
+ | {
193
+ recorded: false;
194
+ traceId: null;
195
+ replayCapable: false;
196
+ result: "disabled";
197
+ }
198
+ | {
199
+ recorded: true;
200
+ traceId: string;
201
+ replayCapable: boolean;
202
+ result: RetrievalTraceAppendResult;
203
+ };
204
+
205
+ interface RetrievalTraceRecorderDeps {
206
+ clock?: () => number;
207
+ idFactory?: () => string;
208
+ /** Stable local secret loaded by the caller; required for metadata labels. */
209
+ redactionSecret?: string;
210
+ }
211
+
212
+ /** Query + retrieval run + retrieval event + terminal event. */
213
+ export const MIN_RETRIEVAL_TRACE_RECORDS = 4;
214
+
215
+ const normalizeText = (value: string): string =>
216
+ value.replace(/\r\n/g, "\n").replace(/\r/g, "\n").normalize("NFC");
217
+
218
+ const textShape = (value: string | undefined) => {
219
+ const normalized = normalizeText(value ?? "");
220
+ return {
221
+ characters: Array.from(normalized).length,
222
+ terms: normalized.trim() ? normalized.trim().split(/\s+/u).length : 0,
223
+ };
224
+ };
225
+
226
+ const sha256 = (value: string): string =>
227
+ new Bun.CryptoHasher("sha256").update(value).digest("hex");
228
+
229
+ const valueShape = (value: unknown): unknown => {
230
+ if (value === null) return { type: "null" };
231
+ if (Array.isArray(value)) {
232
+ const itemTypes = [
233
+ ...new Set(
234
+ value.map((item) =>
235
+ item === null ? "null" : Array.isArray(item) ? "array" : typeof item
236
+ )
237
+ ),
238
+ ].sort();
239
+ return { type: "array", count: value.length, itemTypes };
240
+ }
241
+ if (typeof value === "object") {
242
+ const fields: Record<string, unknown> = {};
243
+ for (const key of Object.keys(value as Record<string, unknown>).sort()) {
244
+ fields[key] = valueShape((value as Record<string, unknown>)[key]);
245
+ }
246
+ return { type: "object", fields };
247
+ }
248
+ return { type: typeof value };
249
+ };
250
+
251
+ const projectMetadataObject = (
252
+ value: Record<string, unknown>
253
+ ): Record<string, unknown> => ({ shape: valueShape(value) });
254
+
255
+ const projectPayload = (
256
+ _mode: RetrievalTraceRedactionMode,
257
+ value: Record<string, unknown>
258
+ ): Record<string, unknown> => value;
259
+
260
+ export class RetrievalTraceRecorder {
261
+ private readonly clock: () => number;
262
+ private readonly idFactory: () => string;
263
+ private readonly redactionSecret: string | undefined;
264
+
265
+ constructor(
266
+ private readonly store: StorePort,
267
+ private readonly config: RetrievalTraceConfig | undefined,
268
+ deps: RetrievalTraceRecorderDeps = {}
269
+ ) {
270
+ this.clock = deps.clock ?? Date.now;
271
+ this.idFactory = deps.idFactory ?? (() => crypto.randomUUID());
272
+ this.redactionSecret = deps.redactionSecret;
273
+ }
274
+
275
+ isEnabled(): boolean {
276
+ return this.config?.enabled === true;
277
+ }
278
+
279
+ async start(
280
+ input: StartRetrievalTraceInput
281
+ ): Promise<StoreResult<RetrievalTraceWriteResult>> {
282
+ if (
283
+ !this.config?.enabled ||
284
+ this.config.retention.maxRecordsPerTrace < MIN_RETRIEVAL_TRACE_RECORDS
285
+ ) {
286
+ return ok({
287
+ recorded: false,
288
+ traceId: null,
289
+ replayCapable: false,
290
+ result: "disabled",
291
+ });
292
+ }
293
+ const parsed = startTraceSchema.safeParse(input);
294
+ if (!parsed.success) {
295
+ return err("INVALID_INPUT", parsed.error.message, parsed.error);
296
+ }
297
+ const nowMs = this.clock();
298
+ if (!Number.isSafeInteger(nowMs) || nowMs < 0) {
299
+ return err("INVALID_INPUT", "Trace clock must return epoch milliseconds");
300
+ }
301
+ const traceId = parsed.data.traceId ?? this.idFactory();
302
+ const query = normalizeText(parsed.data.query);
303
+ const goal =
304
+ parsed.data.goal === undefined
305
+ ? undefined
306
+ : normalizeText(parsed.data.goal);
307
+ const filters = canonicalizeRetrievalTraceFilters(parsed.data.filters);
308
+ const replay = this.config.redactionMode === "replay";
309
+ const create = await this.store.createRetrievalTrace({
310
+ traceId,
311
+ schemaVersion: "1.0",
312
+ redactionMode: this.config.redactionMode,
313
+ replayCapable: replay,
314
+ queryText: replay ? query : null,
315
+ queryDigest: replay ? sha256(query) : null,
316
+ queryShape: textShape(query),
317
+ goalText: replay ? (goal ?? null) : null,
318
+ goalDigest: replay && goal !== undefined ? sha256(goal) : null,
319
+ goalShape: textShape(goal),
320
+ filters: replay ? filters : projectMetadataObject(filters),
321
+ fingerprints: parsed.data.fingerprints,
322
+ status: "open",
323
+ createdAtMs: nowMs,
324
+ updatedAtMs: nowMs,
325
+ expiresAtMs: nowMs + this.config.retention.maxAgeDays * 86_400_000,
326
+ });
327
+ if (!create.ok) return create;
328
+ const retention = await this.store.enforceRetrievalTraceRetention(
329
+ this.config.retention,
330
+ nowMs
331
+ );
332
+ if (!retention.ok) return retention;
333
+ const retained = await this.store.getRetrievalTrace(traceId);
334
+ if (!retained.ok) return retained;
335
+ if (!retained.value) {
336
+ return err(
337
+ "CONSTRAINT_VIOLATION",
338
+ `Retrieval trace ${traceId} exceeded retention limits and was evicted`
339
+ );
340
+ }
341
+ return ok({
342
+ recorded: true,
343
+ traceId,
344
+ replayCapable: replay,
345
+ result: create.value,
346
+ });
347
+ }
348
+
349
+ async appendRun(
350
+ input: RetrievalTraceRunInput
351
+ ): Promise<StoreResult<RetrievalTraceAppendResult | "disabled">> {
352
+ if (!this.config?.enabled) return ok("disabled");
353
+ const payload = runPayloadSchemas[input.kind]?.safeParse(input.payload);
354
+ if (!payload?.success) {
355
+ return err(
356
+ "INVALID_INPUT",
357
+ payload?.error.message ?? `Unknown retrieval run kind: ${input.kind}`
358
+ );
359
+ }
360
+ try {
361
+ const parsed = parseRetrievalTraceRunInput({
362
+ ...input,
363
+ payload: projectPayload(this.config.redactionMode, payload.data),
364
+ });
365
+ return await this.enforceAfterWrite(
366
+ input.traceId,
367
+ await this.store.appendRetrievalTraceRun(parsed)
368
+ );
369
+ } catch (cause) {
370
+ return invalidTraceInput(cause, "Invalid retrieval trace run");
371
+ }
372
+ }
373
+
374
+ async appendEvent(
375
+ input: RetrievalTraceEventInput
376
+ ): Promise<StoreResult<RetrievalTraceAppendResult | "disabled">> {
377
+ if (!this.config?.enabled) return ok("disabled");
378
+ const payload = eventPayloadSchemas[input.kind]?.safeParse(input.payload);
379
+ if (!payload?.success) {
380
+ return err(
381
+ "INVALID_INPUT",
382
+ payload?.error.message ?? `Unknown retrieval event kind: ${input.kind}`
383
+ );
384
+ }
385
+ try {
386
+ const parsed = parseRetrievalTraceEventInput({
387
+ ...input,
388
+ payload: projectPayload(this.config.redactionMode, payload.data),
389
+ });
390
+ return await this.enforceAfterWrite(
391
+ input.traceId,
392
+ await this.store.appendRetrievalTraceEvent(parsed)
393
+ );
394
+ } catch (cause) {
395
+ return invalidTraceInput(cause, "Invalid retrieval trace event");
396
+ }
397
+ }
398
+
399
+ async appendJudgment(
400
+ input: RetrievalTraceJudgmentInput
401
+ ): Promise<StoreResult<RetrievalTraceAppendResult | "disabled">> {
402
+ if (!this.config?.enabled) return ok("disabled");
403
+ const target = judgmentTargetSchema.safeParse(input.target);
404
+ if (!target.success) {
405
+ return err("INVALID_INPUT", target.error.message, target.error);
406
+ }
407
+ const metadata = this.config.redactionMode === "metadata";
408
+ if (metadata && !this.redactionSecret) {
409
+ return err(
410
+ "INVALID_INPUT",
411
+ "Metadata judgments require a stable local redaction secret"
412
+ );
413
+ }
414
+ try {
415
+ const parsed = parseRetrievalTraceJudgmentInput({
416
+ ...input,
417
+ targetRef: metadata
418
+ ? `redacted:${sha256(`${this.redactionSecret}\0${input.targetRef}`)}`
419
+ : input.targetRef,
420
+ target: projectPayload(this.config.redactionMode, target.data),
421
+ });
422
+ return await this.enforceAfterWrite(
423
+ input.traceId,
424
+ await this.store.appendRetrievalTraceJudgment(parsed)
425
+ );
426
+ } catch (cause) {
427
+ return invalidTraceInput(cause, "Invalid retrieval trace judgment");
428
+ }
429
+ }
430
+
431
+ async finalize(
432
+ traceId: string,
433
+ status: RetrievalTraceTerminalStatus
434
+ ): Promise<StoreResult<RetrievalTraceAppendResult | "disabled">> {
435
+ if (!this.config?.enabled) return ok("disabled");
436
+ return await this.enforceAfterWrite(
437
+ traceId,
438
+ await this.store.finalizeRetrievalTrace(traceId, status, this.clock())
439
+ );
440
+ }
441
+
442
+ private async enforceAfterWrite(
443
+ traceId: string,
444
+ result: StoreResult<RetrievalTraceAppendResult>
445
+ ): Promise<StoreResult<RetrievalTraceAppendResult>> {
446
+ if (!result.ok || !this.config?.enabled) return result;
447
+ const retained = await this.store.enforceRetrievalTraceRetention(
448
+ this.config.retention,
449
+ this.clock()
450
+ );
451
+ if (!retained.ok) return retained;
452
+ const stored = await this.store.getRetrievalTrace(traceId);
453
+ if (!stored.ok) return stored;
454
+ if (!stored.value) {
455
+ return err(
456
+ "CONSTRAINT_VIOLATION",
457
+ `Retrieval trace ${traceId} exceeded retention limits and was evicted`
458
+ );
459
+ }
460
+ return result;
461
+ }
462
+ }
463
+
464
+ const invalidTraceInput = <T>(
465
+ cause: unknown,
466
+ fallback: string
467
+ ): StoreResult<T> =>
468
+ err(
469
+ "INVALID_INPUT",
470
+ cause instanceof Error ? cause.message : fallback,
471
+ cause
472
+ );
@@ -1,5 +1,6 @@
1
1
  /** MCP Context Capsule tools over the shared application runtime. */
2
2
 
3
+ import type { RetrievalTraceSession } from "../../core/retrieval-trace-session";
3
4
  import type { ModelLease } from "../../llm/nodeLlamaCpp/lifecycle";
4
5
  import type { EmbeddingPort, RerankPort } from "../../llm/types";
5
6
  import type { VectorIndexPort } from "../../store/vector";
@@ -20,6 +21,10 @@ import {
20
21
  parseContextVerifySurfaceInput,
21
22
  } from "../../app/context-surface";
22
23
  import { createNonTtyProgressRenderer } from "../../cli/progress";
24
+ import {
25
+ finishRetrievalTraceAfterError,
26
+ startRetrievalTraceRequest,
27
+ } from "../../core/retrieval-trace-request";
23
28
  import { LlmAdapter } from "../../llm/nodeLlamaCpp/adapter";
24
29
  import { resolveDownloadPolicy } from "../../llm/policy";
25
30
  import { resolveModelUri } from "../../llm/registry";
@@ -28,11 +33,15 @@ import { createVectorIndexPort } from "../../store/vector";
28
33
  interface ContextToolResultData {
29
34
  structuredContent: Record<string, unknown>;
30
35
  text: string;
36
+ traceId?: string;
31
37
  }
32
38
 
33
39
  const asToolResult = (data: ContextToolResultData): ToolResult => ({
34
40
  content: [{ type: "text", text: data.text }],
35
41
  structuredContent: data.structuredContent,
42
+ ...(data.traceId
43
+ ? { _meta: { gno: { retrievalTrace: { traceId: data.traceId } } } }
44
+ : {}),
36
45
  });
37
46
 
38
47
  const asToolError = (error: unknown): ToolResult => {
@@ -180,15 +189,50 @@ export const handleContext = (
180
189
  context.config.collections.map((collection) => collection.name)
181
190
  );
182
191
  const useModels = parsed.input.depthPolicy !== "fast";
183
- const modelPorts = useModels
184
- ? await createMcpModelPorts(
185
- context,
186
- parsed.input.collections?.length === 1
187
- ? parsed.input.collections[0]
188
- : undefined
189
- )
190
- : null;
192
+ const collection =
193
+ parsed.input.collections?.length === 1
194
+ ? parsed.input.collections[0]
195
+ : undefined;
196
+ const modelUris = useModels
197
+ ? [
198
+ resolveModelUri(context.config, "embed", undefined, collection),
199
+ resolveModelUri(context.config, "rerank", undefined, collection),
200
+ ]
201
+ : [];
202
+ let modelPorts: Awaited<ReturnType<typeof createMcpModelPorts>> | null =
203
+ null;
204
+ let traceSession: RetrievalTraceSession | undefined;
191
205
  try {
206
+ const traceStart = await startRetrievalTraceRequest({
207
+ store: context.store,
208
+ config: context.config,
209
+ query: parsed.input.query ?? parsed.input.goal,
210
+ goal: parsed.input.goal,
211
+ filters: {
212
+ limit: parsed.input.limit,
213
+ collection,
214
+ collections: [...(parsed.input.collections ?? [])].sort(),
215
+ lang: parsed.input.lang,
216
+ tagsAll: parsed.input.tagsAll,
217
+ tagsAny: parsed.input.tagsAny,
218
+ since: parsed.input.since,
219
+ until: parsed.input.until,
220
+ categories: parsed.input.categories,
221
+ author: parsed.input.author,
222
+ graph: parsed.input.graph,
223
+ candidateLimit: parsed.input.candidateLimit,
224
+ queryModes: parsed.input.queryModes,
225
+ uriPrefix: parsed.input.uriPrefix ?? undefined,
226
+ },
227
+ pipeline: "context",
228
+ indexName: context.indexName,
229
+ modelUris,
230
+ });
231
+ if (!traceStart.ok) throw new Error(traceStart.error.message);
232
+ traceSession = traceStart.value ?? undefined;
233
+ modelPorts = useModels
234
+ ? await createMcpModelPorts(context, collection)
235
+ : null;
192
236
  const capsule = await buildContextCapsule(parsed.input, {
193
237
  store: context.store,
194
238
  config: context.config,
@@ -196,14 +240,21 @@ export const handleContext = (
196
240
  vectorIndex: modelPorts?.vectorIndex ?? null,
197
241
  embedPort: modelPorts?.embedPort ?? null,
198
242
  rerankPort: modelPorts?.rerankPort ?? null,
243
+ traceSession,
199
244
  });
245
+ const finalized = await traceSession?.finish("completed");
246
+ if (finalized && !finalized.ok) throw new Error(finalized.error.message);
200
247
  return {
201
248
  structuredContent: capsule as unknown as Record<string, unknown>,
202
249
  // MCP model context receives this projection exactly once. The full
203
250
  // canonical capsule remains available to application clients through
204
251
  // structuredContent and is deliberately not duplicated in text.
205
252
  text: formatContextCapsuleAgentJson(capsule),
253
+ traceId: traceSession?.metadata()?.traceId,
206
254
  };
255
+ } catch (cause) {
256
+ await finishRetrievalTraceAfterError(traceSession, cause);
257
+ throw cause;
207
258
  } finally {
208
259
  await modelPorts?.dispose();
209
260
  }
@@ -16,6 +16,11 @@ import {
16
16
  } from "../../core/document-capabilities";
17
17
  import { resolveEffectiveIndex } from "../../core/indexed-reference";
18
18
  import { parseRef } from "../../core/ref-parser";
19
+ import {
20
+ attachRetrievalTraceMetadata,
21
+ evidenceFromExactDocument,
22
+ RetrievalTraceSession,
23
+ } from "../../core/retrieval-trace-session";
19
24
  import { openScopedIndexStore } from "../../store/sqlite/scoped-index";
20
25
  import { runTool, type ToolResult } from "./index";
21
26
 
@@ -24,6 +29,7 @@ interface GetInput {
24
29
  fromLine?: number;
25
30
  lineCount?: number;
26
31
  lineNumbers?: boolean;
32
+ traceId?: string;
27
33
  }
28
34
 
29
35
  interface GetResponse {
@@ -165,6 +171,7 @@ export function handleGet(
165
171
 
166
172
  // Apply line range if specified
167
173
  let content = fullContent;
174
+ let exactContent = fullContent;
168
175
  let returnedLines: { start: number; end: number } | undefined;
169
176
 
170
177
  // lineNumbers defaults to true per spec
@@ -176,12 +183,14 @@ export function handleGet(
176
183
  if (startLine > totalLines) {
177
184
  // Return empty content for out-of-range request
178
185
  content = "";
186
+ exactContent = "";
179
187
  returnedLines = undefined;
180
188
  } else {
181
189
  const count = args.lineCount ?? totalLines - startLine + 1;
182
190
  const endLine = Math.min(startLine + count - 1, totalLines);
183
191
 
184
192
  const slicedLines = contentLines.slice(startLine - 1, endLine);
193
+ exactContent = slicedLines.join("\n");
185
194
 
186
195
  if (showLineNumbers) {
187
196
  content = slicedLines
@@ -242,7 +251,32 @@ export function handleGet(
242
251
  }),
243
252
  };
244
253
 
245
- return response;
254
+ if (!args.traceId) return response;
255
+ const resumed = await RetrievalTraceSession.resume({
256
+ store: scoped.store,
257
+ config: ctx.config.retrievalTraces,
258
+ traceId: args.traceId,
259
+ });
260
+ if (!resumed.ok) throw new Error(resumed.error.message);
261
+ const traceSession = resumed.value;
262
+ if (!traceSession) return response;
263
+ const lines = returnedLines ?? { start: 1, end: totalLines };
264
+ const evidence = evidenceFromExactDocument({
265
+ docid: response.docid,
266
+ uri: response.uri,
267
+ sourceHash: response.source.sourceHash,
268
+ mirrorHash: response.conversion?.mirrorHash,
269
+ content: exactContent,
270
+ startLine: lines.start,
271
+ endLine: lines.end,
272
+ });
273
+ if (evidence) {
274
+ const got = await traceSession.recordEvidence("get", [evidence]);
275
+ if (!got.ok) throw new Error(got.error.message);
276
+ const opened = await traceSession.recordEvidence("open", [evidence]);
277
+ if (!opened.ok) throw new Error(opened.error.message);
278
+ }
279
+ return attachRetrievalTraceMetadata(response, traceSession);
246
280
  } finally {
247
281
  await scoped.close();
248
282
  }