@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,341 @@
1
+ /** SQLite primitives used by local retrieval-trace management surfaces. */
2
+
3
+ import type { Database } from "bun:sqlite";
4
+
5
+ import type {
6
+ RetrievalTraceAppendResult,
7
+ RetrievalTraceBoundedBundle,
8
+ RetrievalTraceExportBundle,
9
+ RetrievalTraceExportManifestInput,
10
+ RetrievalTraceExportManifestRow,
11
+ StoreResult,
12
+ } from "../types";
13
+
14
+ import { ok } from "../types";
15
+ import {
16
+ type DbRetrievalTraceEventRow,
17
+ type DbRetrievalTraceExportRow,
18
+ type DbRetrievalTraceJudgmentRow,
19
+ type DbRetrievalTraceRow,
20
+ type DbRetrievalTraceRunRow,
21
+ mapEventRow,
22
+ mapExportRow,
23
+ mapJudgmentRow,
24
+ mapRunRow,
25
+ mapTraceRow,
26
+ RetrievalTraceConflictError,
27
+ traceReadError,
28
+ traceWriteError,
29
+ validateTraceId,
30
+ } from "./retrieval-trace-rows";
31
+ import { getTrace } from "./retrieval-trace-store";
32
+
33
+ const SHA256_PATTERN = /^[a-f0-9]{64}$/;
34
+ const REDACTION_SECRET_KEY = "retrieval_trace_redaction_secret_v1";
35
+
36
+ const randomSecret = (): string => {
37
+ const bytes = crypto.getRandomValues(new Uint8Array(32));
38
+ return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join(
39
+ ""
40
+ );
41
+ };
42
+
43
+ const countForTrace = (db: Database, table: string, traceId: string): number =>
44
+ db
45
+ .query<{ count: number }, [string]>(
46
+ `SELECT COUNT(*) AS count FROM ${table} WHERE trace_id = ?`
47
+ )
48
+ .get(traceId)?.count ?? 0;
49
+
50
+ export const getBoundedTrace = (
51
+ db: Database,
52
+ traceId: string,
53
+ detailLimit: number
54
+ ): StoreResult<RetrievalTraceBoundedBundle | null> => {
55
+ try {
56
+ validateTraceId(traceId, "traceId");
57
+ if (
58
+ !Number.isSafeInteger(detailLimit) ||
59
+ detailLimit < 1 ||
60
+ detailLimit > 10_000
61
+ ) {
62
+ throw new RangeError("Trace detail limit must be from 1 to 10000");
63
+ }
64
+ const transaction = db.transaction(
65
+ (): RetrievalTraceBoundedBundle | null => {
66
+ const trace = db
67
+ .query<DbRetrievalTraceRow, [string]>(
68
+ "SELECT * FROM retrieval_traces WHERE trace_id = ?"
69
+ )
70
+ .get(traceId);
71
+ if (!trace) return null;
72
+ const runs = db
73
+ .query<DbRetrievalTraceRunRow, [string, number]>(
74
+ `SELECT * FROM retrieval_trace_runs WHERE trace_id = ?
75
+ ORDER BY created_at_ms, run_id LIMIT ?`
76
+ )
77
+ .all(traceId, detailLimit);
78
+ const events = db
79
+ .query<DbRetrievalTraceEventRow, [string, number]>(
80
+ `SELECT * FROM retrieval_trace_events WHERE trace_id = ?
81
+ ORDER BY created_at_ms, event_id LIMIT ?`
82
+ )
83
+ .all(traceId, detailLimit);
84
+ const judgments = db
85
+ .query<DbRetrievalTraceJudgmentRow, [string, number]>(
86
+ `SELECT * FROM retrieval_trace_judgments WHERE trace_id = ?
87
+ ORDER BY created_at_ms, judgment_id LIMIT ?`
88
+ )
89
+ .all(traceId, detailLimit);
90
+ const exports = db
91
+ .query<DbRetrievalTraceExportRow, [string, number]>(
92
+ `SELECT x.*, et.trace_id FROM retrieval_trace_exports x
93
+ JOIN retrieval_trace_export_traces et USING (export_id)
94
+ WHERE et.trace_id = ?
95
+ ORDER BY x.created_at_ms, x.export_id LIMIT ?`
96
+ )
97
+ .all(traceId, detailLimit);
98
+ return {
99
+ bundle: {
100
+ trace: mapTraceRow(trace),
101
+ runs: runs.map(mapRunRow),
102
+ events: events.map(mapEventRow),
103
+ judgments: judgments.map(mapJudgmentRow),
104
+ exports: exports.map(mapExportRow),
105
+ },
106
+ totals: {
107
+ runs: countForTrace(db, "retrieval_trace_runs", traceId),
108
+ events: countForTrace(db, "retrieval_trace_events", traceId),
109
+ judgments: countForTrace(db, "retrieval_trace_judgments", traceId),
110
+ exports: countForTrace(
111
+ db,
112
+ "retrieval_trace_export_traces",
113
+ traceId
114
+ ),
115
+ },
116
+ };
117
+ }
118
+ );
119
+ return ok(transaction());
120
+ } catch (cause) {
121
+ return traceReadError(cause, "Failed to get bounded retrieval trace");
122
+ }
123
+ };
124
+
125
+ const normalizeManifest = (
126
+ input: RetrievalTraceExportManifestInput
127
+ ): RetrievalTraceExportManifestInput => {
128
+ validateTraceId(input.exportId, "exportId");
129
+ if (!["agentic-receipt", "qrels"].includes(input.format)) {
130
+ throw new RangeError("Invalid retrieval trace export format");
131
+ }
132
+ if (!SHA256_PATTERN.test(input.artifactHash)) {
133
+ throw new RangeError("artifactHash must be a lowercase SHA-256 digest");
134
+ }
135
+ if (!Number.isSafeInteger(input.createdAtMs) || input.createdAtMs < 0) {
136
+ throw new RangeError("createdAtMs must be epoch milliseconds");
137
+ }
138
+ if (input.traceIds.length < 1 || input.traceIds.length > 10_000) {
139
+ throw new RangeError("Export manifests require from 1 to 10000 traces");
140
+ }
141
+ const traceIds = [...new Set(input.traceIds)].sort();
142
+ for (const traceId of traceIds) validateTraceId(traceId, "traceId");
143
+ return { ...input, traceIds };
144
+ };
145
+
146
+ const readManifest = (
147
+ db: Database,
148
+ exportId: string
149
+ ): RetrievalTraceExportManifestRow | null => {
150
+ const row = db
151
+ .query<
152
+ {
153
+ export_id: string;
154
+ format: RetrievalTraceExportManifestRow["format"];
155
+ artifact_hash: string;
156
+ created_at_ms: number;
157
+ },
158
+ [string]
159
+ >(
160
+ `SELECT export_id, format, artifact_hash, created_at_ms
161
+ FROM retrieval_trace_exports WHERE export_id = ?`
162
+ )
163
+ .get(exportId);
164
+ if (!row) return null;
165
+ const traceIds = db
166
+ .query<{ trace_id: string }, [string]>(
167
+ `SELECT trace_id FROM retrieval_trace_export_traces
168
+ WHERE export_id = ? ORDER BY trace_id`
169
+ )
170
+ .all(exportId)
171
+ .map(({ trace_id }) => trace_id);
172
+ return {
173
+ exportId: row.export_id,
174
+ traceIds,
175
+ format: row.format,
176
+ artifactHash: row.artifact_hash,
177
+ createdAtMs: row.created_at_ms,
178
+ };
179
+ };
180
+
181
+ export const getOrCreateRedactionSecret = (
182
+ db: Database
183
+ ): StoreResult<string> => {
184
+ try {
185
+ const candidate = randomSecret();
186
+ const transaction = db.transaction((): string => {
187
+ db.run(
188
+ `INSERT OR IGNORE INTO schema_meta (key, value, updated_at)
189
+ VALUES (?, ?, datetime('now'))`,
190
+ [REDACTION_SECRET_KEY, candidate]
191
+ );
192
+ const stored = db
193
+ .query<{ value: string }, [string]>(
194
+ "SELECT value FROM schema_meta WHERE key = ?"
195
+ )
196
+ .get(REDACTION_SECRET_KEY)?.value;
197
+ if (!stored || !SHA256_PATTERN.test(stored)) {
198
+ throw new RetrievalTraceConflictError(
199
+ "Invalid retrieval trace redaction secret"
200
+ );
201
+ }
202
+ return stored;
203
+ });
204
+ return ok(transaction());
205
+ } catch (cause) {
206
+ return traceWriteError(
207
+ cause,
208
+ "Failed to load retrieval trace redaction secret"
209
+ );
210
+ }
211
+ };
212
+
213
+ export const appendExportManifest = (
214
+ db: Database,
215
+ input: RetrievalTraceExportManifestInput
216
+ ): StoreResult<RetrievalTraceAppendResult> => {
217
+ try {
218
+ const manifest = normalizeManifest(input);
219
+ const transaction = db.transaction((): RetrievalTraceAppendResult => {
220
+ const hashOwner = db
221
+ .query<{ export_id: string }, [string, string]>(
222
+ `SELECT export_id FROM retrieval_trace_exports
223
+ WHERE format = ? AND artifact_hash = ?`
224
+ )
225
+ .get(manifest.format, manifest.artifactHash)?.export_id;
226
+ if (hashOwner && hashOwner !== manifest.exportId) {
227
+ throw new RetrievalTraceConflictError(
228
+ "Export artifact already belongs to a different manifest"
229
+ );
230
+ }
231
+ for (const traceId of manifest.traceIds) {
232
+ const trace = db
233
+ .query<{ status: string }, [string]>(
234
+ "SELECT status FROM retrieval_traces WHERE trace_id = ?"
235
+ )
236
+ .get(traceId);
237
+ if (!trace) {
238
+ throw new RetrievalTraceConflictError(
239
+ "Export manifest references a missing retrieval trace"
240
+ );
241
+ }
242
+ if (trace.status === "open") {
243
+ throw new RetrievalTraceConflictError(
244
+ "Open retrieval traces cannot be exported"
245
+ );
246
+ }
247
+ }
248
+ const inserted = db.run(
249
+ `INSERT OR IGNORE INTO retrieval_trace_exports
250
+ (export_id, format, artifact_hash, created_at_ms) VALUES (?, ?, ?, ?)`,
251
+ [
252
+ manifest.exportId,
253
+ manifest.format,
254
+ manifest.artifactHash,
255
+ manifest.createdAtMs,
256
+ ]
257
+ );
258
+ const existing = readManifest(db, manifest.exportId);
259
+ if (
260
+ !existing ||
261
+ existing.format !== manifest.format ||
262
+ existing.artifactHash !== manifest.artifactHash
263
+ ) {
264
+ throw new RetrievalTraceConflictError(
265
+ "Export ID already exists with different content"
266
+ );
267
+ }
268
+ for (const traceId of manifest.traceIds) {
269
+ db.run(
270
+ `INSERT OR IGNORE INTO retrieval_trace_export_traces
271
+ (export_id, trace_id) VALUES (?, ?)`,
272
+ [manifest.exportId, traceId]
273
+ );
274
+ }
275
+ const stored = readManifest(db, manifest.exportId);
276
+ if (
277
+ !stored ||
278
+ stored.traceIds.length !== manifest.traceIds.length ||
279
+ stored.traceIds.some(
280
+ (traceId, index) => traceId !== manifest.traceIds[index]
281
+ )
282
+ ) {
283
+ throw new RetrievalTraceConflictError(
284
+ "Export manifest membership conflicts with stored content"
285
+ );
286
+ }
287
+ return inserted.changes > 0 ? "inserted" : "duplicate";
288
+ });
289
+ return ok(transaction());
290
+ } catch (cause) {
291
+ return traceWriteError(
292
+ cause,
293
+ "Failed to append retrieval trace export manifest"
294
+ );
295
+ }
296
+ };
297
+
298
+ export const getExportManifest = (
299
+ db: Database,
300
+ exportId: string
301
+ ): StoreResult<RetrievalTraceExportManifestRow | null> => {
302
+ try {
303
+ validateTraceId(exportId, "exportId");
304
+ return ok(readManifest(db, exportId));
305
+ } catch (cause) {
306
+ return traceReadError(cause, "Failed to get retrieval trace export");
307
+ }
308
+ };
309
+
310
+ export const getExportBundle = (
311
+ db: Database,
312
+ exportId: string
313
+ ): StoreResult<RetrievalTraceExportBundle | null> => {
314
+ try {
315
+ validateTraceId(exportId, "exportId");
316
+ const transaction = db.transaction(
317
+ (): StoreResult<RetrievalTraceExportBundle | null> => {
318
+ const manifest = readManifest(db, exportId);
319
+ if (!manifest) return ok(null);
320
+ const traces = [];
321
+ for (const traceId of manifest.traceIds) {
322
+ const trace = getTrace(db, traceId);
323
+ if (!trace.ok) return trace;
324
+ if (!trace.value) {
325
+ throw new RetrievalTraceConflictError(
326
+ "Export manifest references a missing retrieval trace"
327
+ );
328
+ }
329
+ traces.push(trace.value);
330
+ }
331
+ return ok({ manifest, traces });
332
+ }
333
+ );
334
+ return transaction();
335
+ } catch (cause) {
336
+ return traceReadError(
337
+ cause,
338
+ "Failed to get complete retrieval trace export"
339
+ );
340
+ }
341
+ };
@@ -0,0 +1,349 @@
1
+ /** Secure deletion, full purge, and deterministic trace retention for SQLite. */
2
+
3
+ import type { Database } from "bun:sqlite";
4
+
5
+ import type {
6
+ RetrievalTraceDeleteCounts,
7
+ RetrievalTracePurgeResult,
8
+ RetrievalTraceRetentionPolicy,
9
+ RetrievalTraceRetentionResult,
10
+ StoreResult,
11
+ } from "../types";
12
+
13
+ import { ok } from "../types";
14
+ import {
15
+ countTraceContent,
16
+ traceWriteError,
17
+ validateTraceId,
18
+ } from "./retrieval-trace-rows";
19
+
20
+ const DAY_MS = 86_400_000;
21
+ const compareCodeUnits = (left: string, right: string): number =>
22
+ left < right ? -1 : left > right ? 1 : 0;
23
+
24
+ interface StorageRow {
25
+ trace_id: string;
26
+ created_at_ms: number;
27
+ expires_at_ms: number;
28
+ total_bytes: number;
29
+ record_count: number;
30
+ }
31
+
32
+ const storageRows = (db: Database): StorageRow[] =>
33
+ db
34
+ .query<StorageRow, []>(
35
+ `SELECT
36
+ t.trace_id,
37
+ t.created_at_ms,
38
+ t.expires_at_ms,
39
+ (
40
+ length(CAST(t.trace_id AS BLOB))
41
+ + length(CAST(t.schema_version AS BLOB))
42
+ + length(CAST(t.redaction_mode AS BLOB))
43
+ + length(CAST(COALESCE(t.query_text, '') AS BLOB))
44
+ + length(CAST(COALESCE(t.query_digest, '') AS BLOB))
45
+ + length(CAST(t.query_shape_json AS BLOB))
46
+ + length(CAST(COALESCE(t.goal_text, '') AS BLOB))
47
+ + length(CAST(COALESCE(t.goal_digest, '') AS BLOB))
48
+ + length(CAST(t.goal_shape_json AS BLOB))
49
+ + length(CAST(t.filters_json AS BLOB))
50
+ + length(CAST(t.pipeline_fingerprint AS BLOB))
51
+ + length(CAST(t.model_fingerprint AS BLOB))
52
+ + length(CAST(t.config_fingerprint AS BLOB))
53
+ + length(CAST(t.index_fingerprint AS BLOB))
54
+ + length(CAST(t.status AS BLOB))
55
+ + length(CAST(t.creation_digest AS BLOB))
56
+ + 40
57
+ )
58
+ + COALESCE((
59
+ SELECT SUM(
60
+ length(CAST(r.run_id AS BLOB))
61
+ + length(CAST(r.trace_id AS BLOB))
62
+ + length(CAST(r.idempotency_key AS BLOB))
63
+ + length(CAST(r.kind AS BLOB))
64
+ + r.payload_bytes
65
+ + length(CAST(r.canonical_digest AS BLOB))
66
+ + 8
67
+ ) FROM retrieval_trace_runs r
68
+ WHERE r.trace_id = t.trace_id
69
+ ), 0)
70
+ + COALESCE((
71
+ SELECT SUM(
72
+ length(CAST(e.event_id AS BLOB))
73
+ + length(CAST(e.trace_id AS BLOB))
74
+ + length(CAST(COALESCE(e.run_id, '') AS BLOB))
75
+ + length(CAST(e.idempotency_key AS BLOB))
76
+ + length(CAST(e.kind AS BLOB))
77
+ + e.payload_bytes
78
+ + length(CAST(e.canonical_digest AS BLOB))
79
+ + 8
80
+ ) FROM retrieval_trace_events e
81
+ WHERE e.trace_id = t.trace_id
82
+ ), 0)
83
+ + COALESCE((
84
+ SELECT SUM(
85
+ length(CAST(j.judgment_id AS BLOB))
86
+ + length(CAST(j.trace_id AS BLOB))
87
+ + length(CAST(COALESCE(j.run_id, '') AS BLOB))
88
+ + length(CAST(j.idempotency_key AS BLOB))
89
+ + length(CAST(j.label AS BLOB))
90
+ + length(CAST(j.target_kind AS BLOB))
91
+ + length(CAST(j.target_ref AS BLOB))
92
+ + j.target_bytes
93
+ + length(CAST(j.canonical_digest AS BLOB))
94
+ + 8
95
+ ) FROM retrieval_trace_judgments j
96
+ WHERE j.trace_id = t.trace_id
97
+ ), 0)
98
+ + COALESCE((
99
+ SELECT SUM(
100
+ length(CAST(x.export_id AS BLOB))
101
+ + length(CAST(et.trace_id AS BLOB))
102
+ + length(CAST(x.format AS BLOB))
103
+ + length(CAST(x.artifact_hash AS BLOB))
104
+ + 8
105
+ )
106
+ FROM retrieval_trace_export_traces et
107
+ JOIN retrieval_trace_exports x USING (export_id)
108
+ WHERE et.trace_id = t.trace_id
109
+ ), 0) AS total_bytes,
110
+ (
111
+ SELECT COUNT(*) FROM retrieval_trace_runs r
112
+ WHERE r.trace_id = t.trace_id
113
+ ) + (
114
+ SELECT COUNT(*) FROM retrieval_trace_events e
115
+ WHERE e.trace_id = t.trace_id
116
+ ) + (
117
+ SELECT COUNT(*) FROM retrieval_trace_judgments j
118
+ WHERE j.trace_id = t.trace_id
119
+ ) + (
120
+ SELECT COUNT(*) FROM retrieval_trace_export_traces et
121
+ WHERE et.trace_id = t.trace_id
122
+ ) AS record_count
123
+ FROM retrieval_traces t
124
+ ORDER BY t.created_at_ms ASC, t.trace_id ASC`
125
+ )
126
+ .all();
127
+
128
+ const validatePolicy = (
129
+ policy: RetrievalTraceRetentionPolicy,
130
+ nowMs: number
131
+ ): void => {
132
+ if (
133
+ !Number.isSafeInteger(nowMs) ||
134
+ nowMs < 0 ||
135
+ !Number.isSafeInteger(policy.maxAgeDays) ||
136
+ policy.maxAgeDays < 1 ||
137
+ policy.maxAgeDays > 3650 ||
138
+ !Number.isSafeInteger(policy.maxTraces) ||
139
+ policy.maxTraces < 1 ||
140
+ policy.maxTraces > 1_000_000 ||
141
+ !Number.isSafeInteger(policy.maxRecordsPerTrace) ||
142
+ policy.maxRecordsPerTrace < 1 ||
143
+ policy.maxRecordsPerTrace > 100_000 ||
144
+ !Number.isSafeInteger(policy.maxBytes) ||
145
+ policy.maxBytes < 65_536 ||
146
+ policy.maxBytes > 1024 * 1024 * 1024
147
+ ) {
148
+ throw new RangeError("Invalid retrieval trace retention policy");
149
+ }
150
+ };
151
+
152
+ const subtractCounts = (
153
+ before: RetrievalTraceDeleteCounts,
154
+ after: RetrievalTraceDeleteCounts
155
+ ): RetrievalTraceDeleteCounts => ({
156
+ traces: before.traces - after.traces,
157
+ runs: before.runs - after.runs,
158
+ events: before.events - after.events,
159
+ judgments: before.judgments - after.judgments,
160
+ exports: before.exports - after.exports,
161
+ exportLinks: before.exportLinks - after.exportLinks,
162
+ });
163
+
164
+ const readSecureDelete = (db: Database): number =>
165
+ db.query<{ secure_delete: number }, []>("PRAGMA secure_delete").get()
166
+ ?.secure_delete ?? 0;
167
+
168
+ const restoreSecureDelete = (db: Database, prior: number): void => {
169
+ const mode = prior === 0 ? "OFF" : prior === 2 ? "FAST" : "ON";
170
+ db.exec(`PRAGMA secure_delete = ${mode}`);
171
+ };
172
+
173
+ export const deleteTrace = (
174
+ db: Database,
175
+ traceId: string
176
+ ): StoreResult<RetrievalTraceDeleteCounts> => {
177
+ const prior = readSecureDelete(db);
178
+ try {
179
+ validateTraceId(traceId, "traceId");
180
+ db.exec("PRAGMA secure_delete = ON");
181
+ const transaction = db.transaction(() => {
182
+ const counts = countTraceContent(db, traceId);
183
+ db.run("DELETE FROM retrieval_traces WHERE trace_id = ?", [traceId]);
184
+ return counts;
185
+ });
186
+ return ok(transaction());
187
+ } catch (cause) {
188
+ return traceWriteError(cause, "Failed to delete retrieval trace");
189
+ } finally {
190
+ restoreSecureDelete(db, prior);
191
+ }
192
+ };
193
+
194
+ export const purgeTraces = (
195
+ db: Database
196
+ ): StoreResult<RetrievalTracePurgeResult> => {
197
+ const prior = readSecureDelete(db);
198
+ try {
199
+ db.exec("PRAGMA secure_delete = ON");
200
+ const transaction = db.transaction(() => {
201
+ const counts = countTraceContent(db);
202
+ db.run("DELETE FROM retrieval_traces");
203
+ db.run(
204
+ `DELETE FROM retrieval_trace_exports WHERE NOT EXISTS (
205
+ SELECT 1 FROM retrieval_trace_export_traces et
206
+ WHERE et.export_id = retrieval_trace_exports.export_id
207
+ )`
208
+ );
209
+ return counts;
210
+ });
211
+ const counts = transaction();
212
+ let checkpoint:
213
+ | { busy: number; log: number; checkpointed: number }
214
+ | undefined;
215
+ try {
216
+ checkpoint =
217
+ db
218
+ .query<{ busy: number; log: number; checkpointed: number }, []>(
219
+ "PRAGMA wal_checkpoint(TRUNCATE)"
220
+ )
221
+ .get() ?? undefined;
222
+ } catch {
223
+ return ok({
224
+ ...counts,
225
+ physicalCleanup: "failed",
226
+ checkpointedFrames: 0,
227
+ remainingWalFrames: -1,
228
+ });
229
+ }
230
+ const physicalCleanup =
231
+ checkpoint?.busy === 0 && (checkpoint.log === 0 || checkpoint.log === -1)
232
+ ? "completed"
233
+ : "wal_busy";
234
+ return ok({
235
+ ...counts,
236
+ physicalCleanup,
237
+ checkpointedFrames: checkpoint?.checkpointed ?? 0,
238
+ remainingWalFrames: checkpoint?.log ?? -1,
239
+ });
240
+ } catch (cause) {
241
+ return traceWriteError(cause, "Failed to purge retrieval traces");
242
+ } finally {
243
+ restoreSecureDelete(db, prior);
244
+ }
245
+ };
246
+
247
+ export const enforceRetention = (
248
+ db: Database,
249
+ policy: RetrievalTraceRetentionPolicy,
250
+ nowMs: number
251
+ ): StoreResult<RetrievalTraceRetentionResult> => {
252
+ const prior = readSecureDelete(db);
253
+ try {
254
+ validatePolicy(policy, nowMs);
255
+ db.exec("PRAGMA secure_delete = ON");
256
+ db.exec("BEGIN IMMEDIATE");
257
+ try {
258
+ const before = countTraceContent(db);
259
+ const rows = storageRows(db);
260
+ const ageMs = policy.maxAgeDays * DAY_MS;
261
+ const byExpiry = [...rows].sort((left, right) => {
262
+ const leftExpiry = Math.min(
263
+ left.expires_at_ms,
264
+ left.created_at_ms + ageMs
265
+ );
266
+ const rightExpiry = Math.min(
267
+ right.expires_at_ms,
268
+ right.created_at_ms + ageMs
269
+ );
270
+ return (
271
+ leftExpiry - rightExpiry ||
272
+ left.created_at_ms - right.created_at_ms ||
273
+ compareCodeUnits(left.trace_id, right.trace_id)
274
+ );
275
+ });
276
+ const deleted = new Set<string>();
277
+ const deletedTraceIds: string[] = [];
278
+ let remainingTraces = rows.length;
279
+ let remainingBytes = rows.reduce(
280
+ (total, row) => total + row.total_bytes,
281
+ 0
282
+ );
283
+
284
+ for (const row of byExpiry) {
285
+ const effectiveExpiry = Math.min(
286
+ row.expires_at_ms,
287
+ row.created_at_ms + ageMs
288
+ );
289
+ if (effectiveExpiry > nowMs) break;
290
+ db.run("DELETE FROM retrieval_traces WHERE trace_id = ?", [
291
+ row.trace_id,
292
+ ]);
293
+ deleted.add(row.trace_id);
294
+ deletedTraceIds.push(row.trace_id);
295
+ remainingTraces -= 1;
296
+ remainingBytes -= row.total_bytes;
297
+ }
298
+
299
+ for (const row of rows) {
300
+ if (
301
+ deleted.has(row.trace_id) ||
302
+ row.record_count <= policy.maxRecordsPerTrace
303
+ ) {
304
+ continue;
305
+ }
306
+ db.run("DELETE FROM retrieval_traces WHERE trace_id = ?", [
307
+ row.trace_id,
308
+ ]);
309
+ deleted.add(row.trace_id);
310
+ deletedTraceIds.push(row.trace_id);
311
+ remainingTraces -= 1;
312
+ remainingBytes -= row.total_bytes;
313
+ }
314
+
315
+ for (const row of rows) {
316
+ if (
317
+ remainingTraces <= policy.maxTraces &&
318
+ remainingBytes <= policy.maxBytes
319
+ ) {
320
+ break;
321
+ }
322
+ if (deleted.has(row.trace_id)) continue;
323
+ db.run("DELETE FROM retrieval_traces WHERE trace_id = ?", [
324
+ row.trace_id,
325
+ ]);
326
+ deleted.add(row.trace_id);
327
+ deletedTraceIds.push(row.trace_id);
328
+ remainingTraces -= 1;
329
+ remainingBytes -= row.total_bytes;
330
+ }
331
+
332
+ const after = countTraceContent(db);
333
+ db.exec("COMMIT");
334
+ return ok({
335
+ deleted: subtractCounts(before, after),
336
+ deletedTraceIds,
337
+ remainingTraces,
338
+ remainingBytes,
339
+ });
340
+ } catch (cause) {
341
+ db.exec("ROLLBACK");
342
+ throw cause;
343
+ }
344
+ } catch (cause) {
345
+ return traceWriteError(cause, "Failed to enforce trace retention");
346
+ } finally {
347
+ restoreSecureDelete(db, prior);
348
+ }
349
+ };