@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
@@ -5,8 +5,18 @@
5
5
  * @module src/cli/commands/vsearch
6
6
  */
7
7
 
8
+ import type {
9
+ RetrievalTraceSession,
10
+ RetrievalTraceSurfaceMetadata,
11
+ } from "../../core/retrieval-trace-session";
12
+ import type { EmbeddingPort } from "../../llm/types";
8
13
  import type { SearchOptions, SearchResults } from "../../pipeline/types";
9
14
 
15
+ import {
16
+ finishRetrievalTraceAfterError,
17
+ retrievalTraceFilters,
18
+ startRetrievalTraceRequest,
19
+ } from "../../core/retrieval-trace-request";
10
20
  import { LlmAdapter } from "../../llm/nodeLlamaCpp/adapter";
11
21
  import { resolveModelUri } from "../../llm/registry";
12
22
  import { formatQueryForEmbedding } from "../../pipeline/contextual";
@@ -47,7 +57,11 @@ export type VsearchCommandOptions = SearchOptions & {
47
57
  };
48
58
 
49
59
  export type VsearchResult =
50
- | { success: true; data: SearchResults }
60
+ | {
61
+ success: true;
62
+ data: SearchResults;
63
+ metadata?: RetrievalTraceSurfaceMetadata;
64
+ }
51
65
  | { success: false; error: string };
52
66
 
53
67
  // ─────────────────────────────────────────────────────────────────────────────
@@ -78,6 +92,8 @@ export async function vsearch(
78
92
  }
79
93
 
80
94
  const { store, config } = initResult;
95
+ let embedPort: EmbeddingPort | null = null;
96
+ let traceSession: RetrievalTraceSession | undefined;
81
97
 
82
98
  try {
83
99
  // Get model URI from preset
@@ -87,67 +103,74 @@ export async function vsearch(
87
103
  options.model,
88
104
  options.collection
89
105
  );
106
+ const traceStart = await startRetrievalTraceRequest({
107
+ store,
108
+ config,
109
+ query,
110
+ filters: retrievalTraceFilters({ ...options, limit }),
111
+ pipeline: "vector",
112
+ indexName: options.indexName,
113
+ modelUris: [modelUri],
114
+ });
115
+ if (!traceStart.ok) {
116
+ return { success: false, error: traceStart.error.message };
117
+ }
118
+ traceSession = traceStart.value ?? undefined;
90
119
 
91
120
  // Create LLM adapter for embeddings
92
121
  const llm = new LlmAdapter(config);
93
122
  const embedResult = await llm.createEmbeddingPort(modelUri);
94
123
  if (!embedResult.ok) {
124
+ await traceSession?.finish("failed");
95
125
  return { success: false, error: embedResult.error.message };
96
126
  }
97
127
 
98
- const embedPort = embedResult.value;
99
-
100
- try {
101
- // Embed query with contextual formatting (also determines dimensions)
102
- const queryEmbedResult = await embedPort.embed(
103
- formatQueryForEmbedding(query, embedPort.modelUri)
104
- );
105
- if (!queryEmbedResult.ok) {
106
- return { success: false, error: queryEmbedResult.error.message };
107
- }
108
- const queryEmbedding = new Float32Array(queryEmbedResult.value);
109
- const dimensions = queryEmbedding.length;
110
-
111
- // Create vector index port
112
- const db = store.getRawDb();
113
- const vectorResult = await createVectorIndexPort(db, {
114
- model: modelUri,
115
- dimensions,
116
- });
117
-
118
- if (!vectorResult.ok) {
119
- return { success: false, error: vectorResult.error.message };
120
- }
121
-
122
- const vectorIndex = vectorResult.value;
123
-
124
- const deps: VectorSearchDeps = {
125
- store,
126
- vectorIndex,
127
- embedPort,
128
- config,
129
- };
130
-
131
- // Pass pre-computed embedding to avoid double-embed
132
- const result = await searchVectorWithEmbedding(
133
- deps,
134
- query,
135
- queryEmbedding,
136
- { ...options, limit }
137
- );
138
-
139
- if (!result.ok) {
140
- return { success: false, error: result.error.message };
141
- }
142
-
143
- return {
144
- success: true,
145
- data: decorateSearchResultsForIndex(result.value, options.indexName),
146
- };
147
- } finally {
148
- await embedPort.dispose();
128
+ embedPort = embedResult.value;
129
+ const queryEmbedResult = await embedPort.embed(
130
+ formatQueryForEmbedding(query, embedPort.modelUri)
131
+ );
132
+ if (!queryEmbedResult.ok) {
133
+ await traceSession?.finish("failed");
134
+ return { success: false, error: queryEmbedResult.error.message };
149
135
  }
136
+ const queryEmbedding = new Float32Array(queryEmbedResult.value);
137
+ const vectorResult = await createVectorIndexPort(store.getRawDb(), {
138
+ model: modelUri,
139
+ dimensions: queryEmbedding.length,
140
+ });
141
+ if (!vectorResult.ok) {
142
+ await traceSession?.finish("failed");
143
+ return { success: false, error: vectorResult.error.message };
144
+ }
145
+ const deps: VectorSearchDeps = {
146
+ store,
147
+ vectorIndex: vectorResult.value,
148
+ embedPort,
149
+ config,
150
+ };
151
+ const result = await searchVectorWithEmbedding(
152
+ deps,
153
+ query,
154
+ queryEmbedding,
155
+ { ...options, limit, traceSession }
156
+ );
157
+ if (!result.ok) {
158
+ await traceSession?.finish("failed");
159
+ return { success: false, error: result.error.message };
160
+ }
161
+ return {
162
+ success: true,
163
+ data: decorateSearchResultsForIndex(result.value, options.indexName),
164
+ metadata: traceSession?.metadata(),
165
+ };
166
+ } catch (cause) {
167
+ await finishRetrievalTraceAfterError(traceSession, cause);
168
+ return {
169
+ success: false,
170
+ error: cause instanceof Error ? cause.message : "Vector search failed",
171
+ };
150
172
  } finally {
173
+ await embedPort?.dispose();
151
174
  await store.close();
152
175
  }
153
176
  }
@@ -194,7 +217,6 @@ export function formatVsearch(
194
217
  })
195
218
  : `Error: ${result.error}`;
196
219
  }
197
-
198
220
  const formatOpts: FormatOptions = {
199
221
  format: getFormatType(options),
200
222
  full: options.full,
@@ -164,6 +164,15 @@ async function writeOutput(
164
164
  }
165
165
  }
166
166
 
167
+ /** Emit opt-in trace identity without changing command stdout payloads. */
168
+ export function writeRetrievalTraceReceipt(
169
+ metadata: { traceId: string } | undefined
170
+ ): void {
171
+ if (metadata) {
172
+ process.stderr.write(`Trace: ${metadata.traceId}\n`);
173
+ }
174
+ }
175
+
167
176
  async function resolveTerminalLinkPolicy(
168
177
  format: "terminal" | "json" | "files" | "csv" | "md" | "xml"
169
178
  ): Promise<
@@ -292,6 +301,7 @@ export function createProgram(): Command {
292
301
  wireOnboardingCommands(program);
293
302
  wireCaptureCommand(program);
294
303
  wireManagementCommands(program);
304
+ wireTraceCommands(program);
295
305
  wirePublishCommand(program);
296
306
  wireVecCommands(program);
297
307
  wireRetrievalCommands(program);
@@ -315,6 +325,244 @@ Report issues: ${ISSUES_URL}`
315
325
  return program;
316
326
  }
317
327
 
328
+ function wireTraceCommands(program: Command): void {
329
+ const traceCmd = program
330
+ .command("trace")
331
+ .description("Inspect and manage private local retrieval traces");
332
+
333
+ const outputFormat = (options: Record<string, unknown>): "json" | "md" =>
334
+ getFormat(options) === "json" ? "json" : "md";
335
+
336
+ traceCmd
337
+ .command("list")
338
+ .description("List bounded, redacted retrieval trace summaries")
339
+ .option("-n, --limit <num>", "maximum traces", "50")
340
+ .option("--cursor <cursor>", "continue from a previous list receipt")
341
+ .option("--json", "JSON output")
342
+ .option("--md", "Markdown output")
343
+ .action(async (cmdOpts: Record<string, unknown>) => {
344
+ const globals = getGlobals();
345
+ const { traceList } = await import("./commands/trace");
346
+ const output = await traceList(
347
+ {
348
+ limit: parsePositiveInt("limit", cmdOpts.limit),
349
+ cursor: cmdOpts.cursor as string | undefined,
350
+ },
351
+ {
352
+ configPath: globals.config,
353
+ indexName: globals.index,
354
+ format: outputFormat(cmdOpts),
355
+ }
356
+ );
357
+ process.stdout.write(output);
358
+ });
359
+
360
+ traceCmd
361
+ .command("show <trace-id>")
362
+ .description("Inspect one bounded retrieval trace receipt")
363
+ .option("--detail-limit <num>", "maximum records per detail section", "500")
364
+ .option("--json", "JSON output")
365
+ .option("--md", "Markdown output")
366
+ .action(async (traceId: string, cmdOpts: Record<string, unknown>) => {
367
+ const globals = getGlobals();
368
+ const { traceShow } = await import("./commands/trace");
369
+ const output = await traceShow(
370
+ traceId,
371
+ {
372
+ detailLimit: parsePositiveInt("detail-limit", cmdOpts.detailLimit),
373
+ },
374
+ {
375
+ configPath: globals.config,
376
+ indexName: globals.index,
377
+ format: outputFormat(cmdOpts),
378
+ }
379
+ );
380
+ process.stdout.write(output);
381
+ });
382
+
383
+ traceCmd
384
+ .command("label <trace-id>")
385
+ .description("Append an explicit retrieval relevance judgment")
386
+ .requiredOption(
387
+ "--label <label>",
388
+ "relevant, irrelevant, or missing-expected"
389
+ )
390
+ .requiredOption("--target <ref>", "evidence or expected-document reference")
391
+ .option("--target-kind <kind>", "document, chunk, or span")
392
+ .option("--from-line <num>", "exact evidence start line")
393
+ .option("--to-line <num>", "exact evidence end line")
394
+ .option("--source-hash <sha256>", "expected immutable source hash")
395
+ .option("--docid <docid>", "expected document ID")
396
+ .option("--idempotency-key <key>", "caller retry key")
397
+ .option("--json", "JSON output")
398
+ .option("--md", "Markdown output")
399
+ .action(async (traceId: string, cmdOpts: Record<string, unknown>) => {
400
+ const globals = getGlobals();
401
+ const rawLabel = String(cmdOpts.label).replace("-", "_");
402
+ if (!["relevant", "irrelevant", "missing_expected"].includes(rawLabel)) {
403
+ throw new CliError(
404
+ "VALIDATION",
405
+ "--label must be relevant, irrelevant, or missing-expected"
406
+ );
407
+ }
408
+ const rawTargetKind = cmdOpts.targetKind;
409
+ if (
410
+ rawTargetKind !== undefined &&
411
+ (typeof rawTargetKind !== "string" ||
412
+ !["document", "chunk", "span"].includes(rawTargetKind))
413
+ ) {
414
+ throw new CliError(
415
+ "VALIDATION",
416
+ "--target-kind must be document, chunk, or span"
417
+ );
418
+ }
419
+ const { traceLabel } = await import("./commands/trace");
420
+ const output = await traceLabel(
421
+ {
422
+ traceId,
423
+ label: rawLabel as "relevant" | "irrelevant" | "missing_expected",
424
+ targetRef: String(cmdOpts.target),
425
+ targetKind: rawTargetKind as
426
+ | "document"
427
+ | "chunk"
428
+ | "span"
429
+ | undefined,
430
+ startLine:
431
+ cmdOpts.fromLine === undefined
432
+ ? undefined
433
+ : parsePositiveInt("from-line", cmdOpts.fromLine),
434
+ endLine:
435
+ cmdOpts.toLine === undefined
436
+ ? undefined
437
+ : parsePositiveInt("to-line", cmdOpts.toLine),
438
+ sourceHash: cmdOpts.sourceHash as string | undefined,
439
+ docid: cmdOpts.docid as string | undefined,
440
+ idempotencyKey: cmdOpts.idempotencyKey as string | undefined,
441
+ },
442
+ {
443
+ configPath: globals.config,
444
+ indexName: globals.index,
445
+ format: outputFormat(cmdOpts),
446
+ }
447
+ );
448
+ process.stdout.write(output);
449
+ });
450
+
451
+ traceCmd
452
+ .command("export <trace-ids...>")
453
+ .description("Export immutable terminal traces as one local receipt")
454
+ .option("--format <format>", "agentic-receipt or qrels", "agentic-receipt")
455
+ .option("--output <path>", "write canonical artifact atomically")
456
+ .option("--json", "JSON output")
457
+ .action(async (traceIds: string[], cmdOpts: Record<string, unknown>) => {
458
+ const globals = getGlobals();
459
+ const exportFormat = String(cmdOpts.format);
460
+ if (!["agentic-receipt", "qrels"].includes(exportFormat)) {
461
+ throw new CliError(
462
+ "VALIDATION",
463
+ "--format must be agentic-receipt or qrels"
464
+ );
465
+ }
466
+ const { traceExport } = await import("./commands/trace");
467
+ process.stdout.write(
468
+ await traceExport(traceIds, {
469
+ configPath: globals.config,
470
+ indexName: globals.index,
471
+ format: "json",
472
+ output: cmdOpts.output as string | undefined,
473
+ exportFormat: exportFormat as "agentic-receipt" | "qrels",
474
+ })
475
+ );
476
+ });
477
+
478
+ traceCmd
479
+ .command("replay <export-id>")
480
+ .description("Compare one immutable qrels baseline with a candidate")
481
+ .requiredOption("--candidate <type>", "bm25, vector, or hybrid")
482
+ .option("-n, --limit <num>", "result cutoff")
483
+ .option("--candidate-limit <num>", "candidate pool size")
484
+ .option("--no-expand", "disable query expansion")
485
+ .option("--no-rerank", "disable reranking")
486
+ .option("--json", "JSON output")
487
+ .option("--md", "Markdown output")
488
+ .action(async (exportId: string, cmdOpts: Record<string, unknown>) => {
489
+ const globals = getGlobals();
490
+ const candidateType = String(cmdOpts.candidate);
491
+ if (!["bm25", "vector", "hybrid"].includes(candidateType)) {
492
+ throw new CliError(
493
+ "VALIDATION",
494
+ "--candidate must be bm25, vector, or hybrid"
495
+ );
496
+ }
497
+ const { traceReplay } = await import("./commands/replay");
498
+ process.stdout.write(
499
+ await traceReplay(
500
+ exportId,
501
+ {
502
+ id: `cli-${candidateType}`,
503
+ type: candidateType as "bm25" | "vector" | "hybrid",
504
+ limit:
505
+ cmdOpts.limit === undefined
506
+ ? undefined
507
+ : parsePositiveInt("limit", cmdOpts.limit),
508
+ candidateLimit:
509
+ cmdOpts.candidateLimit === undefined
510
+ ? undefined
511
+ : parsePositiveInt("candidate-limit", cmdOpts.candidateLimit),
512
+ noExpand: cmdOpts.expand === false,
513
+ noRerank: cmdOpts.rerank === false,
514
+ },
515
+ {
516
+ configPath: globals.config,
517
+ indexName: globals.index,
518
+ format: outputFormat(cmdOpts),
519
+ offline: globals.offline,
520
+ }
521
+ )
522
+ );
523
+ });
524
+
525
+ traceCmd
526
+ .command("delete <trace-id>")
527
+ .description("Delete one trace and every owned local record")
528
+ .option("--json", "JSON output")
529
+ .option("--md", "Markdown output")
530
+ .action(async (traceId: string, cmdOpts: Record<string, unknown>) => {
531
+ const globals = getGlobals();
532
+ const { traceDelete } = await import("./commands/trace");
533
+ process.stdout.write(
534
+ await traceDelete(traceId, {
535
+ configPath: globals.config,
536
+ indexName: globals.index,
537
+ format: outputFormat(cmdOpts),
538
+ })
539
+ );
540
+ });
541
+
542
+ traceCmd
543
+ .command("purge")
544
+ .description("Delete every local retrieval trace receipt")
545
+ .option("--json", "JSON output")
546
+ .option("--md", "Markdown output")
547
+ .action(async (cmdOpts: Record<string, unknown>) => {
548
+ const globals = getGlobals();
549
+ if (!globals.yes) {
550
+ throw new CliError(
551
+ "VALIDATION",
552
+ "Trace purge requires the global --yes confirmation"
553
+ );
554
+ }
555
+ const { tracePurge } = await import("./commands/trace");
556
+ process.stdout.write(
557
+ await tracePurge({
558
+ configPath: globals.config,
559
+ indexName: globals.index,
560
+ format: outputFormat(cmdOpts),
561
+ })
562
+ );
563
+ });
564
+ }
565
+
318
566
  // ─────────────────────────────────────────────────────────────────────────────
319
567
  // Search Commands (search, vsearch, query, ask)
320
568
  // ─────────────────────────────────────────────────────────────────────────────
@@ -435,6 +683,7 @@ function wireSearchCommands(program: Command): void {
435
683
  terminalLinks: await resolveTerminalLinkPolicy(format),
436
684
  });
437
685
  await writeOutput(output, format);
686
+ writeRetrievalTraceReceipt(result.metadata);
438
687
  });
439
688
 
440
689
  // vsearch - Vector similarity search
@@ -547,6 +796,7 @@ function wireSearchCommands(program: Command): void {
547
796
  terminalLinks: await resolveTerminalLinkPolicy(format),
548
797
  });
549
798
  await writeOutput(output, format);
799
+ writeRetrievalTraceReceipt(result.metadata);
550
800
  });
551
801
 
552
802
  // query - Hybrid search with expansion and reranking
@@ -768,6 +1018,7 @@ function wireSearchCommands(program: Command): void {
768
1018
  terminalLinks: await resolveTerminalLinkPolicy(format),
769
1019
  });
770
1020
  await writeOutput(output, format);
1021
+ writeRetrievalTraceReceipt(result.metadata);
771
1022
  });
772
1023
 
773
1024
  // bench - Retrieval benchmark fixture runner
@@ -960,6 +1211,7 @@ function wireSearchCommands(program: Command): void {
960
1211
  showSources,
961
1212
  });
962
1213
  await writeOutput(output, format);
1214
+ writeRetrievalTraceReceipt(result.metadata);
963
1215
  });
964
1216
  }
965
1217
 
@@ -1191,6 +1443,7 @@ function wireRetrievalCommands(program: Command): void {
1191
1443
  parsePositiveInt.bind(null, "limit")
1192
1444
  )
1193
1445
  .option("--line-numbers", "Prefix lines with numbers")
1446
+ .option("--trace-id <id>", "Continue an open retrieval trace")
1194
1447
  .option("--source", "Include source metadata")
1195
1448
  .option("--json", "JSON output")
1196
1449
  .option("--md", "Markdown output")
@@ -1209,6 +1462,7 @@ function wireRetrievalCommands(program: Command): void {
1209
1462
  source: Boolean(cmdOpts.source),
1210
1463
  json: format === "json",
1211
1464
  md: format === "md",
1465
+ traceId: cmdOpts.traceId as string | undefined,
1212
1466
  });
1213
1467
 
1214
1468
  if (!result.success) {
@@ -1225,6 +1479,7 @@ function wireRetrievalCommands(program: Command): void {
1225
1479
  md: format === "md",
1226
1480
  })}\n`
1227
1481
  );
1482
+ writeRetrievalTraceReceipt(result.metadata);
1228
1483
  });
1229
1484
 
1230
1485
  // multi-get - Retrieve multiple documents
@@ -70,3 +70,12 @@ export {
70
70
  ScopeTypeSchema,
71
71
  } from "./types";
72
72
  export type { HttpGatewayConfig } from "./types";
73
+ export {
74
+ RETRIEVAL_TRACE_DEFAULT_RETENTION,
75
+ type RetrievalTraceConfig,
76
+ RetrievalTraceConfigSchema,
77
+ type RetrievalTraceRedactionMode,
78
+ RetrievalTraceRedactionModeSchema,
79
+ type RetrievalTraceRetention,
80
+ RetrievalTraceRetentionSchema,
81
+ } from "./retrieval-traces";
@@ -0,0 +1,56 @@
1
+ /** Opt-in retrieval trace configuration and bounded-retention defaults. */
2
+
3
+ import { z } from "zod";
4
+
5
+ export const RETRIEVAL_TRACE_DEFAULT_RETENTION = {
6
+ maxAgeDays: 30,
7
+ maxTraces: 1_000,
8
+ maxRecordsPerTrace: 10_000,
9
+ maxBytes: 16 * 1024 * 1024,
10
+ } as const;
11
+
12
+ export const RetrievalTraceRetentionSchema = z
13
+ .object({
14
+ maxAgeDays: z.number().int().min(1).max(3_650),
15
+ maxTraces: z.number().int().min(1).max(1_000_000),
16
+ maxRecordsPerTrace: z.number().int().min(1).max(100_000),
17
+ maxBytes: z
18
+ .number()
19
+ .int()
20
+ .min(64 * 1024)
21
+ .max(1024 * 1024 * 1024),
22
+ })
23
+ .strict();
24
+
25
+ export const RetrievalTraceRedactionModeSchema = z.enum(["metadata", "replay"]);
26
+
27
+ const DisabledRetrievalTraceConfigSchema = z
28
+ .object({
29
+ enabled: z.literal(false),
30
+ })
31
+ .strict();
32
+
33
+ const EnabledRetrievalTraceConfigSchema = z
34
+ .object({
35
+ enabled: z.literal(true),
36
+ /**
37
+ * `replay` stores replay inputs such as the raw query and filters. Choosing
38
+ * it is explicit consent; the safer metadata-only projection is recommended.
39
+ */
40
+ redactionMode: RetrievalTraceRedactionModeSchema,
41
+ retention: RetrievalTraceRetentionSchema,
42
+ })
43
+ .strict();
44
+
45
+ export const RetrievalTraceConfigSchema = z.discriminatedUnion("enabled", [
46
+ DisabledRetrievalTraceConfigSchema,
47
+ EnabledRetrievalTraceConfigSchema,
48
+ ]);
49
+
50
+ export type RetrievalTraceConfig = z.infer<typeof RetrievalTraceConfigSchema>;
51
+ export type RetrievalTraceRetention = z.infer<
52
+ typeof RetrievalTraceRetentionSchema
53
+ >;
54
+ export type RetrievalTraceRedactionMode = z.infer<
55
+ typeof RetrievalTraceRedactionModeSchema
56
+ >;
@@ -8,6 +8,7 @@
8
8
  import { z } from "zod";
9
9
 
10
10
  import { URI_PREFIX } from "../app/constants";
11
+ import { RetrievalTraceConfigSchema } from "./retrieval-traces";
11
12
 
12
13
  // ─────────────────────────────────────────────────────────────────────────────
13
14
  // Constants
@@ -338,6 +339,9 @@ export const ConfigSchema = z.object({
338
339
 
339
340
  /** Resident Streamable HTTP MCP gateway configuration */
340
341
  gateway: HttpGatewayConfigSchema.optional(),
342
+
343
+ /** Private local retrieval trace recording. Absent means recording off. */
344
+ retrievalTraces: RetrievalTraceConfigSchema.optional(),
341
345
  });
342
346
 
343
347
  export type Config = Omit<z.infer<typeof ConfigSchema>, "contentTypes"> & {
@@ -29,6 +29,7 @@ import type { ContextConfiguredGuidance } from "./context-guidance";
29
29
  import { decorateUriForIndex } from "../app/constants";
30
30
  import { canonicalizeIndexName } from "../app/index-name";
31
31
  import { resolveTemporalRange } from "../pipeline/temporal";
32
+ import { attachSearchResultPlannerMetadata } from "../pipeline/trace-metadata";
32
33
  import {
33
34
  SEARCH_RESULT_PLANNER_METADATA,
34
35
  type SearchResultPlannerMetadata,
@@ -363,10 +364,16 @@ export const planContextEvidence = async <T, P>(
363
364
  }
364
365
  const decoratedResults = responses
365
366
  .flatMap((response) => response.results)
366
- .map((result) => ({
367
- ...result,
368
- uri: decorateUriForIndex(result.uri, indexName),
369
- }));
367
+ .map((result) => {
368
+ const decorated = {
369
+ ...result,
370
+ uri: decorateUriForIndex(result.uri, indexName),
371
+ };
372
+ const metadata = result[SEARCH_RESULT_PLANNER_METADATA];
373
+ return metadata
374
+ ? attachSearchResultPlannerMetadata(decorated, metadata)
375
+ : decorated;
376
+ });
370
377
  const results = decoratedResults
371
378
  .map((result, index) => ({
372
379
  result,