@gmickel/gno 1.17.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.
- package/README.md +18 -4
- package/assets/skill/SKILL.md +44 -1
- package/assets/skill/mcp-reference.md +21 -0
- package/package.json +3 -2
- package/spec/AGENTS.md +83 -0
- package/spec/CLAUDE.md +83 -0
- package/spec/bench-fixture.schema.json +137 -0
- package/spec/cli.md +2894 -0
- package/spec/db/schema.sql +442 -0
- package/spec/evals-agentic.md +510 -0
- package/spec/evals.md +1106 -0
- package/spec/mcp.md +2229 -0
- package/spec/output-schemas/activation-verification.schema.json +515 -0
- package/spec/output-schemas/ask.schema.json +366 -0
- package/spec/output-schemas/backlinks.schema.json +131 -0
- package/spec/output-schemas/bench-result.schema.json +120 -0
- package/spec/output-schemas/capture-receipt.schema.json +143 -0
- package/spec/output-schemas/collection-list.schema.json +45 -0
- package/spec/output-schemas/context-capsule-v1.schema.json +691 -0
- package/spec/output-schemas/context-capsule-verification.schema.json +1338 -0
- package/spec/output-schemas/context-list.schema.json +21 -0
- package/spec/output-schemas/doctor.schema.json +313 -0
- package/spec/output-schemas/error.schema.json +30 -0
- package/spec/output-schemas/expansion.schema.json +37 -0
- package/spec/output-schemas/get.schema.json +140 -0
- package/spec/output-schemas/graph-query.schema.json +99 -0
- package/spec/output-schemas/graph.schema.json +371 -0
- package/spec/output-schemas/links-list.schema.json +186 -0
- package/spec/output-schemas/mcp-add-collection-result.schema.json +23 -0
- package/spec/output-schemas/mcp-capture-result.schema.json +152 -0
- package/spec/output-schemas/mcp-http-error.schema.json +30 -0
- package/spec/output-schemas/mcp-job-list.schema.json +58 -0
- package/spec/output-schemas/mcp-job-status.schema.json +224 -0
- package/spec/output-schemas/mcp-remove-result.schema.json +39 -0
- package/spec/output-schemas/mcp-sync-result.schema.json +41 -0
- package/spec/output-schemas/mcp-tag-result.schema.json +33 -0
- package/spec/output-schemas/models-list.schema.json +93 -0
- package/spec/output-schemas/multi-get.schema.json +103 -0
- package/spec/output-schemas/process-status.schema.json +119 -0
- package/spec/output-schemas/query-diagnose.schema.json +123 -0
- package/spec/output-schemas/resident-status.schema.json +154 -0
- package/spec/output-schemas/retrieval-trace-common.schema.json +492 -0
- package/spec/output-schemas/retrieval-trace-delete.schema.json +16 -0
- package/spec/output-schemas/retrieval-trace-export.schema.json +61 -0
- package/spec/output-schemas/retrieval-trace-filters.schema.json +139 -0
- package/spec/output-schemas/retrieval-trace-judgment.schema.json +15 -0
- package/spec/output-schemas/retrieval-trace-list.schema.json +18 -0
- package/spec/output-schemas/retrieval-trace-payloads.schema.json +178 -0
- package/spec/output-schemas/retrieval-trace-purge.schema.json +31 -0
- package/spec/output-schemas/retrieval-trace-qrels.schema.json +303 -0
- package/spec/output-schemas/retrieval-trace-replay.schema.json +286 -0
- package/spec/output-schemas/retrieval-trace-show.schema.json +69 -0
- package/spec/output-schemas/retrieval-trace-summary.schema.json +65 -0
- package/spec/output-schemas/search-result.schema.json +154 -0
- package/spec/output-schemas/search-results.schema.json +338 -0
- package/spec/output-schemas/similar.schema.json +84 -0
- package/spec/output-schemas/status.schema.json +676 -0
- package/spec/output-schemas/tags-list.schema.json +48 -0
- package/src/app/context-runtime-types.ts +3 -0
- package/src/app/context-runtime.ts +15 -1
- package/src/cli/commands/ask.ts +106 -36
- package/src/cli/commands/context-build.ts +56 -9
- package/src/cli/commands/daemon.ts +69 -2
- package/src/cli/commands/get.ts +64 -3
- package/src/cli/commands/models/pull.ts +13 -3
- package/src/cli/commands/query.ts +62 -23
- package/src/cli/commands/replay.ts +140 -0
- package/src/cli/commands/search.ts +48 -3
- package/src/cli/commands/shared.ts +3 -1
- package/src/cli/commands/status.ts +2 -0
- package/src/cli/commands/trace.ts +200 -0
- package/src/cli/commands/vsearch.ts +75 -53
- package/src/cli/detach.ts +37 -20
- package/src/cli/program.ts +329 -27
- package/src/config/index.ts +12 -0
- package/src/config/retrieval-traces.ts +56 -0
- package/src/config/types.ts +41 -0
- package/src/core/context-compiler.ts +11 -4
- package/src/core/job-manager.ts +19 -0
- package/src/core/mutation-generations.ts +33 -0
- package/src/core/retrieval-qrels.ts +405 -0
- package/src/core/retrieval-replay-candidate.ts +368 -0
- package/src/core/retrieval-replay-types.ts +109 -0
- package/src/core/retrieval-replay-validation.ts +89 -0
- package/src/core/retrieval-replay.ts +441 -0
- package/src/core/retrieval-trace-evidence-origin.ts +175 -0
- package/src/core/retrieval-trace-export.ts +113 -0
- package/src/core/retrieval-trace-filter-normalization.ts +27 -0
- package/src/core/retrieval-trace-filters.ts +19 -0
- package/src/core/retrieval-trace-management-helpers.ts +247 -0
- package/src/core/retrieval-trace-management-types.ts +132 -0
- package/src/core/retrieval-trace-management.ts +422 -0
- package/src/core/retrieval-trace-request.ts +141 -0
- package/src/core/retrieval-trace-session.ts +494 -0
- package/src/core/retrieval-trace.ts +472 -0
- package/src/llm/cache.ts +13 -3
- package/src/llm/nodeLlamaCpp/adapter.ts +10 -1
- package/src/llm/nodeLlamaCpp/lifecycle.ts +71 -0
- package/src/mcp/context.ts +161 -0
- package/src/mcp/http-security.ts +477 -0
- package/src/mcp/http-session.ts +272 -0
- package/src/mcp/http-transport.ts +370 -0
- package/src/mcp/resources/index.ts +141 -134
- package/src/mcp/server.ts +19 -79
- package/src/mcp/tools/add-collection.ts +3 -1
- package/src/mcp/tools/capture.ts +3 -0
- package/src/mcp/tools/clear-collection-embeddings.ts +2 -0
- package/src/mcp/tools/context.ts +68 -16
- package/src/mcp/tools/embed.ts +62 -52
- package/src/mcp/tools/get.ts +35 -1
- package/src/mcp/tools/index-cmd.ts +88 -74
- package/src/mcp/tools/index.ts +96 -2
- package/src/mcp/tools/query.ts +95 -64
- package/src/mcp/tools/remove-collection.ts +2 -0
- package/src/mcp/tools/search.ts +36 -13
- package/src/mcp/tools/status.ts +11 -0
- package/src/mcp/tools/sync.ts +16 -14
- package/src/mcp/tools/trace.ts +143 -0
- package/src/mcp/tools/vsearch.ts +71 -38
- package/src/mcp/tools/workspace-write.ts +7 -3
- package/src/pipeline/answer.ts +167 -26
- package/src/pipeline/graph-retrieval.ts +15 -1
- package/src/pipeline/hybrid.ts +151 -43
- package/src/pipeline/search.ts +36 -3
- package/src/pipeline/trace-metadata.ts +47 -0
- package/src/pipeline/types.ts +43 -0
- package/src/pipeline/vsearch.ts +101 -38
- package/src/sdk/client.ts +380 -71
- package/src/sdk/documents.ts +48 -1
- package/src/sdk/index.ts +17 -0
- package/src/sdk/types.ts +28 -0
- package/src/serve/background-runtime.ts +12 -212
- package/src/serve/context-capsule.ts +67 -8
- package/src/serve/embed-scheduler.ts +74 -43
- package/src/serve/index.ts +9 -0
- package/src/serve/jobs.ts +78 -80
- package/src/serve/public/app.tsx +12 -1
- package/src/serve/public/components/HealthCenter.tsx +74 -1
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/lib/workspace-tabs.ts +2 -0
- package/src/serve/public/pages/Dashboard.tsx +11 -0
- package/src/serve/public/pages/TraceHistory.tsx +478 -0
- package/src/serve/public/pages/trace-history-detail.tsx +224 -0
- package/src/serve/resident-admission.ts +159 -0
- package/src/serve/resident-background-work.ts +39 -0
- package/src/serve/resident-request.ts +55 -0
- package/src/serve/resident-runtime.ts +490 -0
- package/src/serve/resident-status.ts +96 -0
- package/src/serve/retrieval-trace.ts +28 -0
- package/src/serve/routes/api.ts +629 -239
- package/src/serve/routes/mcp.ts +69 -0
- package/src/serve/routes/traces.ts +156 -0
- package/src/serve/server.ts +276 -37
- package/src/serve/status-model.ts +51 -0
- package/src/serve/status.ts +5 -0
- package/src/store/index.ts +31 -0
- package/src/store/migrations/014-retrieval-traces.ts +303 -0
- package/src/store/migrations/index.ts +2 -0
- package/src/store/retrieval-trace-codec.ts +384 -0
- package/src/store/sqlite/adapter.ts +179 -10
- package/src/store/sqlite/retrieval-trace-management-store.ts +341 -0
- package/src/store/sqlite/retrieval-trace-retention.ts +349 -0
- package/src/store/sqlite/retrieval-trace-rows.ts +267 -0
- package/src/store/sqlite/retrieval-trace-store.ts +515 -0
- package/src/store/types.ts +297 -0
- package/src/store/vector/sqlite-vec.ts +76 -1
- 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
|
+
);
|
package/src/llm/cache.ts
CHANGED
|
@@ -46,6 +46,7 @@ const HF_PATH_PATTERN = /^([^/]+)\/([^/]+)\/(.+\.gguf)$/;
|
|
|
46
46
|
const GGUF_MAGIC = new Uint8Array([0x47, 0x47, 0x55, 0x46]);
|
|
47
47
|
|
|
48
48
|
type ModelFileOwner = "cache" | "user";
|
|
49
|
+
type ResolveModelFile = typeof import("node-llama-cpp").resolveModelFile;
|
|
49
50
|
|
|
50
51
|
type ValidatedCachedPath =
|
|
51
52
|
| { ok: true; path: string }
|
|
@@ -268,11 +269,16 @@ const MANIFEST_VERSION = "1.0" as const;
|
|
|
268
269
|
export class ModelCache {
|
|
269
270
|
readonly dir: string;
|
|
270
271
|
private readonly manifestPath: string;
|
|
272
|
+
private readonly resolveModelFileFn?: ResolveModelFile;
|
|
271
273
|
private manifest: Manifest | null = null;
|
|
272
274
|
|
|
273
|
-
constructor(
|
|
275
|
+
constructor(
|
|
276
|
+
cacheDir?: string,
|
|
277
|
+
deps?: { resolveModelFile?: ResolveModelFile }
|
|
278
|
+
) {
|
|
274
279
|
this.dir = cacheDir ?? getModelsCachePath();
|
|
275
280
|
this.manifestPath = join(this.dir, "manifest.json");
|
|
281
|
+
this.resolveModelFileFn = deps?.resolveModelFile;
|
|
276
282
|
}
|
|
277
283
|
|
|
278
284
|
/**
|
|
@@ -324,7 +330,8 @@ export class ModelCache {
|
|
|
324
330
|
uri: string,
|
|
325
331
|
type: ModelType,
|
|
326
332
|
onProgress?: ProgressCallback,
|
|
327
|
-
force?: boolean
|
|
333
|
+
force?: boolean,
|
|
334
|
+
signal?: AbortSignal
|
|
328
335
|
): Promise<LlmResult<string>> {
|
|
329
336
|
const parsed = parseModelUri(uri);
|
|
330
337
|
if (!parsed.ok) {
|
|
@@ -364,7 +371,9 @@ export class ModelCache {
|
|
|
364
371
|
}
|
|
365
372
|
|
|
366
373
|
try {
|
|
367
|
-
const
|
|
374
|
+
const resolveModelFile =
|
|
375
|
+
this.resolveModelFileFn ??
|
|
376
|
+
(await import("node-llama-cpp")).resolveModelFile;
|
|
368
377
|
|
|
369
378
|
// Convert to node-llama-cpp format (handles quantization shorthand)
|
|
370
379
|
// node-llama-cpp needs hf: prefix to identify HuggingFace models
|
|
@@ -399,6 +408,7 @@ export class ModelCache {
|
|
|
399
408
|
}
|
|
400
409
|
}
|
|
401
410
|
: undefined,
|
|
411
|
+
signal,
|
|
402
412
|
});
|
|
403
413
|
|
|
404
414
|
const validation = await validateGgufFile(resolvedPath, uri, "cache");
|
|
@@ -27,7 +27,11 @@ import {
|
|
|
27
27
|
} from "../registry";
|
|
28
28
|
import { NodeLlamaCppEmbedding } from "./embedding";
|
|
29
29
|
import { NodeLlamaCppGeneration } from "./generation";
|
|
30
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
getModelManager,
|
|
32
|
+
type ModelLease,
|
|
33
|
+
type ModelManager,
|
|
34
|
+
} from "./lifecycle";
|
|
31
35
|
import { NodeLlamaCppRerank } from "./rerank";
|
|
32
36
|
|
|
33
37
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -218,6 +222,11 @@ export class LlmAdapter {
|
|
|
218
222
|
return this.manager;
|
|
219
223
|
}
|
|
220
224
|
|
|
225
|
+
/** Acquire an idempotent request lease without transferring manager ownership. */
|
|
226
|
+
acquireModelLease(): ModelLease {
|
|
227
|
+
return this.manager.acquireLease();
|
|
228
|
+
}
|
|
229
|
+
|
|
221
230
|
/**
|
|
222
231
|
* Dispose all resources.
|
|
223
232
|
*/
|
|
@@ -35,6 +35,21 @@ interface CachedModel {
|
|
|
35
35
|
loadedAt: number;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
export interface ModelLease {
|
|
39
|
+
release(): void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ModelLifecycleStats {
|
|
43
|
+
activeLeases: number;
|
|
44
|
+
leaseAcquisitions: number;
|
|
45
|
+
leaseReleases: number;
|
|
46
|
+
loadedModels: number;
|
|
47
|
+
loadAttempts: number;
|
|
48
|
+
loadSuccesses: number;
|
|
49
|
+
loadFailures: number;
|
|
50
|
+
inflightLoads: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
38
53
|
let invalidGpuModeWarned = false;
|
|
39
54
|
let invalidBuildModeWarned = false;
|
|
40
55
|
let gpuFallbackWarned = false;
|
|
@@ -134,7 +149,14 @@ export class ModelManager {
|
|
|
134
149
|
new Map();
|
|
135
150
|
private readonly inflightLoads: Map<string, Promise<LlmResult<LoadedModel>>> =
|
|
136
151
|
new Map();
|
|
152
|
+
private readonly leaseDrainWaiters = new Set<() => void>();
|
|
137
153
|
private readonly config: ModelConfig;
|
|
154
|
+
private activeLeases = 0;
|
|
155
|
+
private leaseAcquisitions = 0;
|
|
156
|
+
private leaseReleases = 0;
|
|
157
|
+
private loadAttempts = 0;
|
|
158
|
+
private loadSuccesses = 0;
|
|
159
|
+
private loadFailures = 0;
|
|
138
160
|
|
|
139
161
|
constructor(config: ModelConfig) {
|
|
140
162
|
this.config = config;
|
|
@@ -263,6 +285,7 @@ export class ModelManager {
|
|
|
263
285
|
uri: string,
|
|
264
286
|
type: ModelType
|
|
265
287
|
): Promise<LlmResult<LoadedModel>> {
|
|
288
|
+
this.loadAttempts += 1;
|
|
266
289
|
const timeoutMs = this.config.loadTimeout;
|
|
267
290
|
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
268
291
|
let timedOut = false;
|
|
@@ -299,6 +322,7 @@ export class ModelManager {
|
|
|
299
322
|
|
|
300
323
|
this.models.set(uri, cachedModel);
|
|
301
324
|
this.setDisposalTimer(uri);
|
|
325
|
+
this.loadSuccesses += 1;
|
|
302
326
|
|
|
303
327
|
return {
|
|
304
328
|
ok: true,
|
|
@@ -310,6 +334,7 @@ export class ModelManager {
|
|
|
310
334
|
},
|
|
311
335
|
};
|
|
312
336
|
} catch (e) {
|
|
337
|
+
this.loadFailures += 1;
|
|
313
338
|
// Clear timeout on error
|
|
314
339
|
if (timeoutId) {
|
|
315
340
|
clearTimeout(timeoutId);
|
|
@@ -356,6 +381,42 @@ export class ModelManager {
|
|
|
356
381
|
return model;
|
|
357
382
|
}
|
|
358
383
|
|
|
384
|
+
/** Keep warm models alive while one request owns model-backed ports. */
|
|
385
|
+
acquireLease(): ModelLease {
|
|
386
|
+
this.activeLeases += 1;
|
|
387
|
+
this.leaseAcquisitions += 1;
|
|
388
|
+
for (const timer of this.disposalTimers.values()) clearTimeout(timer);
|
|
389
|
+
this.disposalTimers.clear();
|
|
390
|
+
|
|
391
|
+
let released = false;
|
|
392
|
+
return {
|
|
393
|
+
release: () => {
|
|
394
|
+
if (released) return;
|
|
395
|
+
released = true;
|
|
396
|
+
this.activeLeases = Math.max(0, this.activeLeases - 1);
|
|
397
|
+
this.leaseReleases += 1;
|
|
398
|
+
if (this.activeLeases === 0) {
|
|
399
|
+
for (const resolve of this.leaseDrainWaiters) resolve();
|
|
400
|
+
this.leaseDrainWaiters.clear();
|
|
401
|
+
for (const uri of this.models.keys()) this.setDisposalTimer(uri);
|
|
402
|
+
}
|
|
403
|
+
},
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
getLifecycleStats(): ModelLifecycleStats {
|
|
408
|
+
return {
|
|
409
|
+
activeLeases: this.activeLeases,
|
|
410
|
+
leaseAcquisitions: this.leaseAcquisitions,
|
|
411
|
+
leaseReleases: this.leaseReleases,
|
|
412
|
+
loadedModels: this.models.size,
|
|
413
|
+
loadAttempts: this.loadAttempts,
|
|
414
|
+
loadSuccesses: this.loadSuccesses,
|
|
415
|
+
loadFailures: this.loadFailures,
|
|
416
|
+
inflightLoads: this.inflightLoads.size,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
359
420
|
/**
|
|
360
421
|
* Check if a model is loaded.
|
|
361
422
|
*/
|
|
@@ -367,6 +428,10 @@ export class ModelManager {
|
|
|
367
428
|
* Dispose a specific model.
|
|
368
429
|
*/
|
|
369
430
|
async dispose(uri: string): Promise<void> {
|
|
431
|
+
if (this.activeLeases > 0) {
|
|
432
|
+
this.resetDisposalTimer(uri);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
370
435
|
const cached = this.models.get(uri);
|
|
371
436
|
if (!cached) {
|
|
372
437
|
return;
|
|
@@ -393,6 +458,11 @@ export class ModelManager {
|
|
|
393
458
|
* Dispose all loaded models.
|
|
394
459
|
*/
|
|
395
460
|
async disposeAll(): Promise<void> {
|
|
461
|
+
if (this.activeLeases > 0) {
|
|
462
|
+
await new Promise<void>((resolve) => this.leaseDrainWaiters.add(resolve));
|
|
463
|
+
}
|
|
464
|
+
await Promise.allSettled(this.inflightLoads.values());
|
|
465
|
+
|
|
396
466
|
// Clear all timers
|
|
397
467
|
for (const timer of this.disposalTimers.values()) {
|
|
398
468
|
clearTimeout(timer);
|
|
@@ -429,6 +499,7 @@ export class ModelManager {
|
|
|
429
499
|
// ───────────────────────────────────────────────────────────────────────────
|
|
430
500
|
|
|
431
501
|
private setDisposalTimer(uri: string): void {
|
|
502
|
+
if (this.activeLeases > 0) return;
|
|
432
503
|
const timer = setTimeout(() => {
|
|
433
504
|
this.dispose(uri).catch(() => {
|
|
434
505
|
// Ignore disposal errors in timer callback
|