@agenr/agenr-plugin 1.7.4 → 1.8.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/dist/{chunk-NXCCTZ4G.js → chunk-6CEKKEFZ.js} +2176 -544
- package/dist/chunk-GUDCFFRV.js +1517 -0
- package/dist/chunk-LVDQXSHP.js +5122 -0
- package/dist/index.js +334 -261
- package/openclaw.plugin.json +31 -8
- package/package.json +2 -2
- package/dist/chunk-7WL5EAQZ.js +0 -758
- package/dist/chunk-IZDGXMTQ.js +0 -839
- package/dist/chunk-NIQKTINU.js +0 -2545
package/dist/chunk-NIQKTINU.js
DELETED
|
@@ -1,2545 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
buildLexicalPlan,
|
|
3
|
-
cosineSimilarity
|
|
4
|
-
} from "./chunk-7WL5EAQZ.js";
|
|
5
|
-
|
|
6
|
-
// src/adapters/db/client.ts
|
|
7
|
-
import fs from "fs/promises";
|
|
8
|
-
import path from "path";
|
|
9
|
-
import { createClient } from "@libsql/client";
|
|
10
|
-
|
|
11
|
-
// src/adapters/db/episode-queries.ts
|
|
12
|
-
import { createHash, randomUUID } from "crypto";
|
|
13
|
-
|
|
14
|
-
// src/adapters/db/row-mapping.ts
|
|
15
|
-
var DEFAULT_QUALITY_SCORE = 0.5;
|
|
16
|
-
var ACTIVE_ENTRY_CLAUSE = "retired = 0 AND superseded_by IS NULL";
|
|
17
|
-
var ACTIVE_EPISODE_CLAUSE = "retired = 0 AND superseded_by IS NULL";
|
|
18
|
-
function buildActiveEntryClause(alias) {
|
|
19
|
-
if (!alias) {
|
|
20
|
-
return ACTIVE_ENTRY_CLAUSE;
|
|
21
|
-
}
|
|
22
|
-
return `${alias}.retired = 0 AND ${alias}.superseded_by IS NULL`;
|
|
23
|
-
}
|
|
24
|
-
function buildActiveEpisodeClause(alias) {
|
|
25
|
-
if (!alias) {
|
|
26
|
-
return ACTIVE_EPISODE_CLAUSE;
|
|
27
|
-
}
|
|
28
|
-
return `${alias}.retired = 0 AND ${alias}.superseded_by IS NULL`;
|
|
29
|
-
}
|
|
30
|
-
function serializeEmbeddingForVector(embedding) {
|
|
31
|
-
if (embedding.length === 0) {
|
|
32
|
-
return null;
|
|
33
|
-
}
|
|
34
|
-
return JSON.stringify(embedding.map((value) => Number.isFinite(value) ? value : 0));
|
|
35
|
-
}
|
|
36
|
-
function serializeTags(tags) {
|
|
37
|
-
const normalizedTags = tags.map((tag) => tag.trim()).filter((tag) => tag.length > 0);
|
|
38
|
-
return JSON.stringify(normalizedTags);
|
|
39
|
-
}
|
|
40
|
-
function deserializeTags(value) {
|
|
41
|
-
if (typeof value !== "string" || value.trim().length === 0) {
|
|
42
|
-
return [];
|
|
43
|
-
}
|
|
44
|
-
const normalized = value.trim();
|
|
45
|
-
if (normalized.startsWith("|") && normalized.endsWith("|")) {
|
|
46
|
-
return normalized.slice(1, -1).split("|").map((tag) => tag.trim()).filter((tag) => tag.length > 0);
|
|
47
|
-
}
|
|
48
|
-
try {
|
|
49
|
-
const parsed = JSON.parse(normalized);
|
|
50
|
-
if (!Array.isArray(parsed)) {
|
|
51
|
-
return [];
|
|
52
|
-
}
|
|
53
|
-
return parsed.filter((tag) => typeof tag === "string");
|
|
54
|
-
} catch {
|
|
55
|
-
return [];
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
function readRequiredString(row, key) {
|
|
59
|
-
const value = row[key];
|
|
60
|
-
if (typeof value !== "string") {
|
|
61
|
-
throw new Error(`Expected string column "${key}" in database row.`);
|
|
62
|
-
}
|
|
63
|
-
return value;
|
|
64
|
-
}
|
|
65
|
-
function readOptionalString(row, key) {
|
|
66
|
-
const value = row[key];
|
|
67
|
-
return typeof value === "string" ? value : void 0;
|
|
68
|
-
}
|
|
69
|
-
function readNumber(row, key, fallback = 0) {
|
|
70
|
-
const value = row[key];
|
|
71
|
-
if (typeof value === "number") {
|
|
72
|
-
return Number.isFinite(value) ? value : fallback;
|
|
73
|
-
}
|
|
74
|
-
if (typeof value === "bigint") {
|
|
75
|
-
const converted = Number(value);
|
|
76
|
-
return Number.isFinite(converted) ? converted : fallback;
|
|
77
|
-
}
|
|
78
|
-
if (typeof value === "string" && value.trim().length > 0) {
|
|
79
|
-
const converted = Number(value);
|
|
80
|
-
return Number.isFinite(converted) ? converted : fallback;
|
|
81
|
-
}
|
|
82
|
-
return fallback;
|
|
83
|
-
}
|
|
84
|
-
function readBoolean(row, key) {
|
|
85
|
-
return readNumber(row, key, 0) !== 0;
|
|
86
|
-
}
|
|
87
|
-
function readEmbedding(row, key) {
|
|
88
|
-
const value = row[key];
|
|
89
|
-
if (value instanceof ArrayBuffer) {
|
|
90
|
-
return Array.from(new Float32Array(value));
|
|
91
|
-
}
|
|
92
|
-
if (ArrayBuffer.isView(value)) {
|
|
93
|
-
const length = Math.floor(value.byteLength / Float32Array.BYTES_PER_ELEMENT);
|
|
94
|
-
if (length === 0) {
|
|
95
|
-
return [];
|
|
96
|
-
}
|
|
97
|
-
if (value.byteOffset % Float32Array.BYTES_PER_ELEMENT !== 0) {
|
|
98
|
-
const copy = new Uint8Array(value.byteLength);
|
|
99
|
-
copy.set(new Uint8Array(value.buffer, value.byteOffset, value.byteLength));
|
|
100
|
-
return Array.from(new Float32Array(copy.buffer, 0, length));
|
|
101
|
-
}
|
|
102
|
-
return Array.from(new Float32Array(value.buffer, value.byteOffset, length));
|
|
103
|
-
}
|
|
104
|
-
return [];
|
|
105
|
-
}
|
|
106
|
-
function mapEntryRow(row) {
|
|
107
|
-
const type = readRequiredString(row, "type");
|
|
108
|
-
const expiry = readRequiredString(row, "expiry");
|
|
109
|
-
return {
|
|
110
|
-
id: readRequiredString(row, "id"),
|
|
111
|
-
type,
|
|
112
|
-
subject: readRequiredString(row, "subject"),
|
|
113
|
-
content: readRequiredString(row, "content"),
|
|
114
|
-
importance: readNumber(row, "importance", 0),
|
|
115
|
-
expiry,
|
|
116
|
-
tags: deserializeTags(row.tags),
|
|
117
|
-
source_file: readOptionalString(row, "source_file"),
|
|
118
|
-
source_context: readOptionalString(row, "source_context"),
|
|
119
|
-
embedding: readEmbedding(row, "embedding"),
|
|
120
|
-
content_hash: readOptionalString(row, "content_hash"),
|
|
121
|
-
norm_content_hash: readOptionalString(row, "norm_content_hash"),
|
|
122
|
-
quality_score: readNumber(row, "quality_score", DEFAULT_QUALITY_SCORE),
|
|
123
|
-
recall_count: readNumber(row, "recall_count", 0),
|
|
124
|
-
last_recalled_at: readOptionalString(row, "last_recalled_at"),
|
|
125
|
-
superseded_by: readOptionalString(row, "superseded_by"),
|
|
126
|
-
valid_from: readOptionalString(row, "valid_from"),
|
|
127
|
-
valid_to: readOptionalString(row, "valid_to"),
|
|
128
|
-
claim_key: readOptionalString(row, "claim_key"),
|
|
129
|
-
supersession_kind: readOptionalString(row, "supersession_kind"),
|
|
130
|
-
supersession_reason: readOptionalString(row, "supersession_reason"),
|
|
131
|
-
cluster_id: readOptionalString(row, "cluster_id"),
|
|
132
|
-
user_id: readOptionalString(row, "user_id"),
|
|
133
|
-
project: readOptionalString(row, "project"),
|
|
134
|
-
retired: readBoolean(row, "retired"),
|
|
135
|
-
retired_at: readOptionalString(row, "retired_at"),
|
|
136
|
-
retired_reason: readOptionalString(row, "retired_reason"),
|
|
137
|
-
created_at: readRequiredString(row, "created_at"),
|
|
138
|
-
updated_at: readRequiredString(row, "updated_at")
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
function mapEpisodeRow(row) {
|
|
142
|
-
const source = readRequiredString(row, "source");
|
|
143
|
-
const activityLevel = readOptionalString(row, "activity_level");
|
|
144
|
-
return {
|
|
145
|
-
id: readRequiredString(row, "id"),
|
|
146
|
-
source,
|
|
147
|
-
sourceId: readOptionalString(row, "source_id"),
|
|
148
|
-
sourceRef: readOptionalString(row, "source_ref"),
|
|
149
|
-
transcriptHash: readOptionalString(row, "transcript_hash"),
|
|
150
|
-
summaryHash: readOptionalString(row, "summary_hash"),
|
|
151
|
-
agentId: readOptionalString(row, "agent_id"),
|
|
152
|
-
surface: readOptionalString(row, "surface"),
|
|
153
|
-
startedAt: readRequiredString(row, "started_at"),
|
|
154
|
-
endedAt: readOptionalString(row, "ended_at"),
|
|
155
|
-
summary: readRequiredString(row, "summary"),
|
|
156
|
-
tags: deserializeTags(row.tags),
|
|
157
|
-
activityLevel,
|
|
158
|
-
userId: readOptionalString(row, "user_id"),
|
|
159
|
-
project: readOptionalString(row, "project"),
|
|
160
|
-
genModel: readOptionalString(row, "gen_model"),
|
|
161
|
-
genVersion: readOptionalString(row, "gen_version"),
|
|
162
|
-
messageCount: readOptionalNumber(row, "message_count"),
|
|
163
|
-
embedding: readEmbedding(row, "embedding"),
|
|
164
|
-
retired: readBoolean(row, "retired"),
|
|
165
|
-
retiredAt: readOptionalString(row, "retired_at"),
|
|
166
|
-
retiredReason: readOptionalString(row, "retired_reason"),
|
|
167
|
-
supersededBy: readOptionalString(row, "superseded_by"),
|
|
168
|
-
createdAt: readRequiredString(row, "created_at"),
|
|
169
|
-
updatedAt: readRequiredString(row, "updated_at")
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
|
-
function readOptionalNumber(row, key) {
|
|
173
|
-
if (row[key] === null || row[key] === void 0) {
|
|
174
|
-
return void 0;
|
|
175
|
-
}
|
|
176
|
-
return readNumber(row, key, 0);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// src/adapters/db/episode-queries.ts
|
|
180
|
-
var EPISODE_SELECT_COLUMNS = `
|
|
181
|
-
id,
|
|
182
|
-
source,
|
|
183
|
-
source_id,
|
|
184
|
-
source_ref,
|
|
185
|
-
transcript_hash,
|
|
186
|
-
summary_hash,
|
|
187
|
-
agent_id,
|
|
188
|
-
surface,
|
|
189
|
-
started_at,
|
|
190
|
-
ended_at,
|
|
191
|
-
summary,
|
|
192
|
-
tags,
|
|
193
|
-
activity_level,
|
|
194
|
-
user_id,
|
|
195
|
-
project,
|
|
196
|
-
gen_model,
|
|
197
|
-
gen_version,
|
|
198
|
-
message_count,
|
|
199
|
-
embedding,
|
|
200
|
-
retired,
|
|
201
|
-
retired_at,
|
|
202
|
-
retired_reason,
|
|
203
|
-
superseded_by,
|
|
204
|
-
created_at,
|
|
205
|
-
updated_at
|
|
206
|
-
`;
|
|
207
|
-
async function getEpisodeBySourceId(executor, source, sourceId) {
|
|
208
|
-
const normalizedSourceId = normalizeOptionalString(sourceId);
|
|
209
|
-
if (!normalizedSourceId) {
|
|
210
|
-
return null;
|
|
211
|
-
}
|
|
212
|
-
const result = await executor.execute({
|
|
213
|
-
sql: `
|
|
214
|
-
SELECT ${EPISODE_SELECT_COLUMNS}
|
|
215
|
-
FROM episodes
|
|
216
|
-
WHERE source = ?
|
|
217
|
-
AND source_id = ?
|
|
218
|
-
LIMIT 1
|
|
219
|
-
`,
|
|
220
|
-
args: [source, normalizedSourceId]
|
|
221
|
-
});
|
|
222
|
-
const row = result.rows[0];
|
|
223
|
-
return row ? mapEpisodeRow(row) : null;
|
|
224
|
-
}
|
|
225
|
-
async function getEpisodeByTranscriptHash(executor, source, transcriptHash) {
|
|
226
|
-
const normalizedTranscriptHash = normalizeOptionalString(transcriptHash);
|
|
227
|
-
if (!normalizedTranscriptHash) {
|
|
228
|
-
return null;
|
|
229
|
-
}
|
|
230
|
-
const result = await executor.execute({
|
|
231
|
-
sql: `
|
|
232
|
-
SELECT ${EPISODE_SELECT_COLUMNS}
|
|
233
|
-
FROM episodes
|
|
234
|
-
WHERE source = ?
|
|
235
|
-
AND transcript_hash = ?
|
|
236
|
-
LIMIT 1
|
|
237
|
-
`,
|
|
238
|
-
args: [source, normalizedTranscriptHash]
|
|
239
|
-
});
|
|
240
|
-
const row = result.rows[0];
|
|
241
|
-
return row ? mapEpisodeRow(row) : null;
|
|
242
|
-
}
|
|
243
|
-
async function upsertEpisode(executor, input) {
|
|
244
|
-
const payload = normalizeEpisodePayload(input);
|
|
245
|
-
const summaryHash = createEpisodePayloadHash(payload);
|
|
246
|
-
const existing = payload.sourceId !== void 0 ? await getEpisodeBySourceId(executor, payload.source, payload.sourceId) : await getEpisodeByTranscriptHash(executor, payload.source, readRequiredIdentityHash(payload));
|
|
247
|
-
if (!existing) {
|
|
248
|
-
const id = randomUUID();
|
|
249
|
-
const now2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
250
|
-
const vectorJson2 = serializeEmbeddingForVector(payload.embedding ?? []);
|
|
251
|
-
await executor.execute({
|
|
252
|
-
sql: `
|
|
253
|
-
INSERT INTO episodes (
|
|
254
|
-
id,
|
|
255
|
-
source,
|
|
256
|
-
source_id,
|
|
257
|
-
source_ref,
|
|
258
|
-
transcript_hash,
|
|
259
|
-
summary_hash,
|
|
260
|
-
agent_id,
|
|
261
|
-
surface,
|
|
262
|
-
started_at,
|
|
263
|
-
ended_at,
|
|
264
|
-
summary,
|
|
265
|
-
tags,
|
|
266
|
-
activity_level,
|
|
267
|
-
user_id,
|
|
268
|
-
project,
|
|
269
|
-
gen_model,
|
|
270
|
-
gen_version,
|
|
271
|
-
message_count,
|
|
272
|
-
embedding,
|
|
273
|
-
retired,
|
|
274
|
-
retired_at,
|
|
275
|
-
retired_reason,
|
|
276
|
-
superseded_by,
|
|
277
|
-
created_at,
|
|
278
|
-
updated_at
|
|
279
|
-
)
|
|
280
|
-
VALUES (
|
|
281
|
-
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
|
282
|
-
CASE WHEN ? IS NULL THEN NULL ELSE vector32(?) END,
|
|
283
|
-
0, NULL, NULL, NULL, ?, ?
|
|
284
|
-
)
|
|
285
|
-
`,
|
|
286
|
-
args: [
|
|
287
|
-
id,
|
|
288
|
-
payload.source,
|
|
289
|
-
toNullableString(payload.sourceId),
|
|
290
|
-
toNullableString(payload.sourceRef),
|
|
291
|
-
toNullableString(payload.transcriptHash),
|
|
292
|
-
summaryHash,
|
|
293
|
-
toNullableString(payload.agentId),
|
|
294
|
-
toNullableString(payload.surface),
|
|
295
|
-
payload.startedAt,
|
|
296
|
-
toNullableString(payload.endedAt),
|
|
297
|
-
payload.summary,
|
|
298
|
-
serializeTags(payload.tags),
|
|
299
|
-
toNullableString(payload.activityLevel),
|
|
300
|
-
toNullableString(payload.userId),
|
|
301
|
-
toNullableString(payload.project),
|
|
302
|
-
toNullableString(payload.genModel),
|
|
303
|
-
toNullableString(payload.genVersion),
|
|
304
|
-
toNullableInteger(payload.messageCount),
|
|
305
|
-
vectorJson2,
|
|
306
|
-
vectorJson2,
|
|
307
|
-
now2,
|
|
308
|
-
now2
|
|
309
|
-
]
|
|
310
|
-
});
|
|
311
|
-
return {
|
|
312
|
-
episode: await getEpisodeById(executor, id),
|
|
313
|
-
action: "inserted"
|
|
314
|
-
};
|
|
315
|
-
}
|
|
316
|
-
const existingSummaryHash = existing.summaryHash ?? createEpisodePayloadHash(normalizeEpisodePayload(fromStoredEpisode(existing)));
|
|
317
|
-
if (existingSummaryHash === summaryHash) {
|
|
318
|
-
return {
|
|
319
|
-
episode: existing,
|
|
320
|
-
action: "unchanged"
|
|
321
|
-
};
|
|
322
|
-
}
|
|
323
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
324
|
-
const vectorJson = serializeEmbeddingForVector(payload.embedding ?? []);
|
|
325
|
-
await executor.execute({
|
|
326
|
-
sql: `
|
|
327
|
-
UPDATE episodes
|
|
328
|
-
SET source_ref = ?,
|
|
329
|
-
transcript_hash = ?,
|
|
330
|
-
summary_hash = ?,
|
|
331
|
-
agent_id = ?,
|
|
332
|
-
surface = ?,
|
|
333
|
-
started_at = ?,
|
|
334
|
-
ended_at = ?,
|
|
335
|
-
summary = ?,
|
|
336
|
-
tags = ?,
|
|
337
|
-
activity_level = ?,
|
|
338
|
-
user_id = ?,
|
|
339
|
-
project = ?,
|
|
340
|
-
gen_model = ?,
|
|
341
|
-
gen_version = ?,
|
|
342
|
-
message_count = ?,
|
|
343
|
-
embedding = CASE WHEN ? IS NULL THEN NULL ELSE vector32(?) END,
|
|
344
|
-
updated_at = ?
|
|
345
|
-
WHERE id = ?
|
|
346
|
-
`,
|
|
347
|
-
args: [
|
|
348
|
-
toNullableString(payload.sourceRef),
|
|
349
|
-
toNullableString(payload.transcriptHash),
|
|
350
|
-
summaryHash,
|
|
351
|
-
toNullableString(payload.agentId),
|
|
352
|
-
toNullableString(payload.surface),
|
|
353
|
-
payload.startedAt,
|
|
354
|
-
toNullableString(payload.endedAt),
|
|
355
|
-
payload.summary,
|
|
356
|
-
serializeTags(payload.tags),
|
|
357
|
-
toNullableString(payload.activityLevel),
|
|
358
|
-
toNullableString(payload.userId),
|
|
359
|
-
toNullableString(payload.project),
|
|
360
|
-
toNullableString(payload.genModel),
|
|
361
|
-
toNullableString(payload.genVersion),
|
|
362
|
-
toNullableInteger(payload.messageCount),
|
|
363
|
-
vectorJson,
|
|
364
|
-
vectorJson,
|
|
365
|
-
now,
|
|
366
|
-
existing.id
|
|
367
|
-
]
|
|
368
|
-
});
|
|
369
|
-
return {
|
|
370
|
-
episode: await getEpisodeById(executor, existing.id),
|
|
371
|
-
action: "updated"
|
|
372
|
-
};
|
|
373
|
-
}
|
|
374
|
-
async function listEpisodesByTimeWindow(executor, window, limit) {
|
|
375
|
-
const bounds = resolveWindowBounds(window);
|
|
376
|
-
if (!bounds) {
|
|
377
|
-
return [];
|
|
378
|
-
}
|
|
379
|
-
const whereClauses = [buildActiveEpisodeClause()];
|
|
380
|
-
const args = [];
|
|
381
|
-
if (bounds.start && bounds.end) {
|
|
382
|
-
whereClauses.push("started_at <= ?");
|
|
383
|
-
whereClauses.push("COALESCE(ended_at, started_at) >= ?");
|
|
384
|
-
args.push(bounds.end, bounds.start);
|
|
385
|
-
} else if (bounds.start) {
|
|
386
|
-
whereClauses.push("COALESCE(ended_at, started_at) >= ?");
|
|
387
|
-
args.push(bounds.start);
|
|
388
|
-
} else if (bounds.end) {
|
|
389
|
-
whereClauses.push("started_at <= ?");
|
|
390
|
-
args.push(bounds.end);
|
|
391
|
-
} else {
|
|
392
|
-
return [];
|
|
393
|
-
}
|
|
394
|
-
const normalizedLimit = normalizePositiveInteger(limit);
|
|
395
|
-
const result = await executor.execute({
|
|
396
|
-
sql: `
|
|
397
|
-
SELECT ${EPISODE_SELECT_COLUMNS}
|
|
398
|
-
FROM episodes
|
|
399
|
-
WHERE ${whereClauses.join(" AND ")}
|
|
400
|
-
ORDER BY started_at DESC, id ASC
|
|
401
|
-
${normalizedLimit ? "LIMIT ?" : ""}
|
|
402
|
-
`,
|
|
403
|
-
args: normalizedLimit ? [...args, normalizedLimit] : args
|
|
404
|
-
});
|
|
405
|
-
return result.rows.map((row) => mapEpisodeRow(row));
|
|
406
|
-
}
|
|
407
|
-
async function episodeVectorSearch(executor, params) {
|
|
408
|
-
if (params.limit <= 0 || params.embedding.length === 0) {
|
|
409
|
-
return [];
|
|
410
|
-
}
|
|
411
|
-
const serializedEmbedding = serializeEmbeddingForVector(params.embedding);
|
|
412
|
-
if (!serializedEmbedding) {
|
|
413
|
-
return [];
|
|
414
|
-
}
|
|
415
|
-
let result;
|
|
416
|
-
try {
|
|
417
|
-
result = await executor.execute({
|
|
418
|
-
sql: `
|
|
419
|
-
SELECT ${prefixColumns(EPISODE_SELECT_COLUMNS, "e")}
|
|
420
|
-
FROM vector_top_k('idx_episodes_embedding', vector32(?), ?) AS v
|
|
421
|
-
JOIN episodes AS e ON e.rowid = v.id
|
|
422
|
-
WHERE ${buildActiveEpisodeClause("e")}
|
|
423
|
-
LIMIT ?
|
|
424
|
-
`,
|
|
425
|
-
args: [serializedEmbedding, params.limit, params.limit]
|
|
426
|
-
});
|
|
427
|
-
} catch (error) {
|
|
428
|
-
throw wrapEpisodeVectorError(error);
|
|
429
|
-
}
|
|
430
|
-
return result.rows.map((row) => {
|
|
431
|
-
const episode = mapEpisodeRow(row);
|
|
432
|
-
return {
|
|
433
|
-
episode,
|
|
434
|
-
vectorSim: cosineSimilarity(params.embedding, episode.embedding ?? [])
|
|
435
|
-
};
|
|
436
|
-
}).filter((candidate) => candidate.vectorSim > 0).sort((left, right) => right.vectorSim - left.vectorSim).slice(0, params.limit);
|
|
437
|
-
}
|
|
438
|
-
async function listEpisodesWithoutEmbeddings(executor, limit) {
|
|
439
|
-
const normalizedLimit = normalizePositiveInteger(limit);
|
|
440
|
-
const result = await executor.execute({
|
|
441
|
-
sql: `
|
|
442
|
-
SELECT ${EPISODE_SELECT_COLUMNS}
|
|
443
|
-
FROM episodes
|
|
444
|
-
WHERE ${buildActiveEpisodeClause()}
|
|
445
|
-
AND embedding IS NULL
|
|
446
|
-
ORDER BY started_at DESC, id ASC
|
|
447
|
-
${normalizedLimit ? "LIMIT ?" : ""}
|
|
448
|
-
`,
|
|
449
|
-
args: normalizedLimit ? [normalizedLimit] : []
|
|
450
|
-
});
|
|
451
|
-
return result.rows.map((row) => mapEpisodeRow(row));
|
|
452
|
-
}
|
|
453
|
-
async function updateEpisodeEmbedding(executor, id, embedding) {
|
|
454
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
455
|
-
const vectorJson = serializeEmbeddingForVector(embedding);
|
|
456
|
-
await executor.execute({
|
|
457
|
-
sql: `
|
|
458
|
-
UPDATE episodes
|
|
459
|
-
SET embedding = CASE WHEN ? IS NULL THEN NULL ELSE vector32(?) END,
|
|
460
|
-
updated_at = ?
|
|
461
|
-
WHERE id = ?
|
|
462
|
-
`,
|
|
463
|
-
args: [vectorJson, vectorJson, now, id]
|
|
464
|
-
});
|
|
465
|
-
}
|
|
466
|
-
function createEpisodePayloadHash(payload) {
|
|
467
|
-
return createHash("sha256").update(JSON.stringify(payload)).digest("hex");
|
|
468
|
-
}
|
|
469
|
-
function fromStoredEpisode(episode) {
|
|
470
|
-
return {
|
|
471
|
-
source: episode.source,
|
|
472
|
-
sourceId: episode.sourceId,
|
|
473
|
-
sourceRef: episode.sourceRef,
|
|
474
|
-
transcriptHash: episode.transcriptHash,
|
|
475
|
-
agentId: episode.agentId,
|
|
476
|
-
surface: episode.surface,
|
|
477
|
-
startedAt: episode.startedAt,
|
|
478
|
-
endedAt: episode.endedAt,
|
|
479
|
-
summary: episode.summary,
|
|
480
|
-
tags: episode.tags,
|
|
481
|
-
activityLevel: episode.activityLevel,
|
|
482
|
-
userId: episode.userId,
|
|
483
|
-
project: episode.project,
|
|
484
|
-
genModel: episode.genModel,
|
|
485
|
-
genVersion: episode.genVersion,
|
|
486
|
-
messageCount: episode.messageCount,
|
|
487
|
-
embedding: episode.embedding
|
|
488
|
-
};
|
|
489
|
-
}
|
|
490
|
-
async function getEpisodeById(executor, id) {
|
|
491
|
-
const result = await executor.execute({
|
|
492
|
-
sql: `
|
|
493
|
-
SELECT ${EPISODE_SELECT_COLUMNS}
|
|
494
|
-
FROM episodes
|
|
495
|
-
WHERE id = ?
|
|
496
|
-
LIMIT 1
|
|
497
|
-
`,
|
|
498
|
-
args: [id]
|
|
499
|
-
});
|
|
500
|
-
const row = result.rows[0];
|
|
501
|
-
if (!row) {
|
|
502
|
-
throw new Error(`Episode ${id} was not found after persistence.`);
|
|
503
|
-
}
|
|
504
|
-
return mapEpisodeRow(row);
|
|
505
|
-
}
|
|
506
|
-
function normalizeEpisodePayload(input) {
|
|
507
|
-
const sourceId = normalizeOptionalString(input.sourceId);
|
|
508
|
-
const transcriptHash = normalizeOptionalString(input.transcriptHash);
|
|
509
|
-
if (!sourceId && !transcriptHash) {
|
|
510
|
-
throw new Error("Episode writes require either sourceId or transcriptHash.");
|
|
511
|
-
}
|
|
512
|
-
const startedAt = normalizeRequiredString(input.startedAt, "startedAt");
|
|
513
|
-
const endedAt = normalizeOptionalString(input.endedAt);
|
|
514
|
-
const summary = normalizeSummary(input.summary);
|
|
515
|
-
return {
|
|
516
|
-
source: input.source,
|
|
517
|
-
...sourceId ? { sourceId } : {},
|
|
518
|
-
...normalizeOptionalString(input.sourceRef) ? { sourceRef: normalizeOptionalString(input.sourceRef) ?? void 0 } : {},
|
|
519
|
-
...transcriptHash ? { transcriptHash } : {},
|
|
520
|
-
...normalizeOptionalString(input.agentId) ? { agentId: normalizeOptionalString(input.agentId) ?? void 0 } : {},
|
|
521
|
-
...normalizeOptionalString(input.surface) ? { surface: normalizeOptionalString(input.surface) ?? void 0 } : {},
|
|
522
|
-
startedAt,
|
|
523
|
-
...endedAt ? { endedAt } : {},
|
|
524
|
-
summary,
|
|
525
|
-
tags: normalizeTags(input.tags),
|
|
526
|
-
...normalizeActivityLevel(input.activityLevel) ? { activityLevel: normalizeActivityLevel(input.activityLevel) } : {},
|
|
527
|
-
...normalizeOptionalString(input.userId) ? { userId: normalizeOptionalString(input.userId) ?? void 0 } : {},
|
|
528
|
-
...normalizeOptionalText(input.project) ? { project: normalizeOptionalText(input.project) ?? void 0 } : {},
|
|
529
|
-
...normalizeOptionalString(input.genModel) ? { genModel: normalizeOptionalString(input.genModel) ?? void 0 } : {},
|
|
530
|
-
...normalizeOptionalString(input.genVersion) ? { genVersion: normalizeOptionalString(input.genVersion) ?? void 0 } : {},
|
|
531
|
-
...normalizeOptionalInteger(input.messageCount) !== void 0 ? { messageCount: normalizeOptionalInteger(input.messageCount) } : {},
|
|
532
|
-
...normalizeEmbedding(input.embedding) ? { embedding: normalizeEmbedding(input.embedding) } : {}
|
|
533
|
-
};
|
|
534
|
-
}
|
|
535
|
-
function resolveWindowBounds(window) {
|
|
536
|
-
switch (window.kind) {
|
|
537
|
-
case "interval": {
|
|
538
|
-
if (!window.start || !window.end) {
|
|
539
|
-
return null;
|
|
540
|
-
}
|
|
541
|
-
return { start: window.start.toISOString(), end: window.end.toISOString() };
|
|
542
|
-
}
|
|
543
|
-
case "anchor": {
|
|
544
|
-
if (!window.anchor || window.radiusDays === void 0 || window.radiusDays < 0) {
|
|
545
|
-
return null;
|
|
546
|
-
}
|
|
547
|
-
const radiusMs = Math.trunc(window.radiusDays) * 24 * 60 * 60 * 1e3;
|
|
548
|
-
return {
|
|
549
|
-
start: new Date(window.anchor.getTime() - radiusMs).toISOString(),
|
|
550
|
-
end: new Date(window.anchor.getTime() + radiusMs).toISOString()
|
|
551
|
-
};
|
|
552
|
-
}
|
|
553
|
-
case "open_end":
|
|
554
|
-
return window.start ? { start: window.start.toISOString() } : null;
|
|
555
|
-
case "open_start":
|
|
556
|
-
return window.end ? { end: window.end.toISOString() } : null;
|
|
557
|
-
default:
|
|
558
|
-
return null;
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
function readRequiredIdentityHash(payload) {
|
|
562
|
-
if (!payload.transcriptHash) {
|
|
563
|
-
throw new Error("Episode writes without sourceId require transcriptHash.");
|
|
564
|
-
}
|
|
565
|
-
return payload.transcriptHash;
|
|
566
|
-
}
|
|
567
|
-
function normalizeActivityLevel(value) {
|
|
568
|
-
if (!value) {
|
|
569
|
-
return void 0;
|
|
570
|
-
}
|
|
571
|
-
return value;
|
|
572
|
-
}
|
|
573
|
-
function normalizeOptionalString(value) {
|
|
574
|
-
const trimmed = value?.trim();
|
|
575
|
-
return trimmed ? trimmed : void 0;
|
|
576
|
-
}
|
|
577
|
-
function normalizeOptionalText(value) {
|
|
578
|
-
const normalized = value?.replace(/\s+/gu, " ").trim();
|
|
579
|
-
return normalized ? normalized : void 0;
|
|
580
|
-
}
|
|
581
|
-
function normalizeSummary(value) {
|
|
582
|
-
const normalized = value.replace(/\s+/gu, " ").trim();
|
|
583
|
-
if (!normalized) {
|
|
584
|
-
throw new Error("Episode summary must not be empty.");
|
|
585
|
-
}
|
|
586
|
-
return normalized;
|
|
587
|
-
}
|
|
588
|
-
function normalizeTags(tags) {
|
|
589
|
-
if (!tags || tags.length === 0) {
|
|
590
|
-
return [];
|
|
591
|
-
}
|
|
592
|
-
return Array.from(new Set(tags.map((tag) => tag.trim().toLowerCase()).filter((tag) => tag.length > 0))).sort((left, right) => left.localeCompare(right)).slice(0, 8);
|
|
593
|
-
}
|
|
594
|
-
function normalizeOptionalInteger(value) {
|
|
595
|
-
if (value === void 0 || !Number.isFinite(value)) {
|
|
596
|
-
return void 0;
|
|
597
|
-
}
|
|
598
|
-
return Math.trunc(value);
|
|
599
|
-
}
|
|
600
|
-
function normalizeEmbedding(embedding) {
|
|
601
|
-
if (!embedding || embedding.length === 0) {
|
|
602
|
-
return void 0;
|
|
603
|
-
}
|
|
604
|
-
return embedding.map((value) => Number.isFinite(value) ? value : 0);
|
|
605
|
-
}
|
|
606
|
-
function normalizeRequiredString(value, fieldName) {
|
|
607
|
-
const normalized = value.trim();
|
|
608
|
-
if (!normalized) {
|
|
609
|
-
throw new Error(`Episode field "${fieldName}" must not be empty.`);
|
|
610
|
-
}
|
|
611
|
-
return normalized;
|
|
612
|
-
}
|
|
613
|
-
function normalizePositiveInteger(value) {
|
|
614
|
-
const normalized = normalizeOptionalInteger(value);
|
|
615
|
-
if (normalized === void 0 || normalized <= 0) {
|
|
616
|
-
return void 0;
|
|
617
|
-
}
|
|
618
|
-
return normalized;
|
|
619
|
-
}
|
|
620
|
-
function prefixColumns(columns, alias) {
|
|
621
|
-
return columns.split(",").map((col) => {
|
|
622
|
-
const trimmed = col.trim();
|
|
623
|
-
return trimmed ? `${alias}.${trimmed}` : "";
|
|
624
|
-
}).filter(Boolean).join(", ");
|
|
625
|
-
}
|
|
626
|
-
function wrapEpisodeVectorError(error) {
|
|
627
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
628
|
-
return new Error(`Episode vector search is unavailable: ${message}`);
|
|
629
|
-
}
|
|
630
|
-
function toNullableString(value) {
|
|
631
|
-
return value ?? null;
|
|
632
|
-
}
|
|
633
|
-
function toNullableInteger(value) {
|
|
634
|
-
return value ?? null;
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
// src/adapters/db/queries.ts
|
|
638
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
639
|
-
var LOOKUP_CHUNK_SIZE = 100;
|
|
640
|
-
var DEFAULT_QUALITY_SCORE2 = 0.5;
|
|
641
|
-
async function insertEntry(executor, entry, embedding, contentHash) {
|
|
642
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
643
|
-
const id = entry.id.trim().length > 0 ? entry.id.trim() : randomUUID2();
|
|
644
|
-
const createdAt = normalizeTimestamp(entry.created_at) ?? now;
|
|
645
|
-
const updatedAt = normalizeTimestamp(entry.updated_at) ?? now;
|
|
646
|
-
const vectorJson = serializeEmbeddingForVector(embedding);
|
|
647
|
-
await executor.execute({
|
|
648
|
-
sql: `
|
|
649
|
-
INSERT INTO entries (
|
|
650
|
-
id,
|
|
651
|
-
type,
|
|
652
|
-
subject,
|
|
653
|
-
content,
|
|
654
|
-
importance,
|
|
655
|
-
expiry,
|
|
656
|
-
tags,
|
|
657
|
-
source_file,
|
|
658
|
-
source_context,
|
|
659
|
-
embedding,
|
|
660
|
-
content_hash,
|
|
661
|
-
norm_content_hash,
|
|
662
|
-
minhash_sig,
|
|
663
|
-
quality_score,
|
|
664
|
-
recall_count,
|
|
665
|
-
last_recalled_at,
|
|
666
|
-
superseded_by,
|
|
667
|
-
valid_from,
|
|
668
|
-
valid_to,
|
|
669
|
-
claim_key,
|
|
670
|
-
supersession_kind,
|
|
671
|
-
supersession_reason,
|
|
672
|
-
cluster_id,
|
|
673
|
-
user_id,
|
|
674
|
-
project,
|
|
675
|
-
retired,
|
|
676
|
-
retired_at,
|
|
677
|
-
retired_reason,
|
|
678
|
-
created_at,
|
|
679
|
-
updated_at
|
|
680
|
-
)
|
|
681
|
-
VALUES (
|
|
682
|
-
?, ?, ?, ?, ?, ?, ?, ?, ?,
|
|
683
|
-
CASE WHEN ? IS NULL THEN NULL ELSE vector32(?) END,
|
|
684
|
-
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
|
685
|
-
)
|
|
686
|
-
`,
|
|
687
|
-
args: [
|
|
688
|
-
id,
|
|
689
|
-
entry.type,
|
|
690
|
-
entry.subject,
|
|
691
|
-
entry.content,
|
|
692
|
-
normalizeInteger(entry.importance, 0),
|
|
693
|
-
entry.expiry,
|
|
694
|
-
serializeTags(entry.tags),
|
|
695
|
-
normalizeOptionalString2(entry.source_file),
|
|
696
|
-
normalizeOptionalString2(entry.source_context),
|
|
697
|
-
vectorJson,
|
|
698
|
-
vectorJson,
|
|
699
|
-
contentHash.trim(),
|
|
700
|
-
normalizeOptionalString2(entry.norm_content_hash),
|
|
701
|
-
null,
|
|
702
|
-
normalizeNumber(entry.quality_score, DEFAULT_QUALITY_SCORE2),
|
|
703
|
-
normalizeInteger(entry.recall_count, 0),
|
|
704
|
-
normalizeOptionalString2(entry.last_recalled_at),
|
|
705
|
-
normalizeOptionalString2(entry.superseded_by),
|
|
706
|
-
normalizeOptionalString2(entry.valid_from),
|
|
707
|
-
normalizeOptionalString2(entry.valid_to),
|
|
708
|
-
normalizeOptionalString2(entry.claim_key),
|
|
709
|
-
normalizeOptionalString2(entry.supersession_kind),
|
|
710
|
-
normalizeOptionalString2(entry.supersession_reason),
|
|
711
|
-
normalizeOptionalString2(entry.cluster_id),
|
|
712
|
-
normalizeOptionalString2(entry.user_id),
|
|
713
|
-
normalizeOptionalString2(entry.project),
|
|
714
|
-
entry.retired ? 1 : 0,
|
|
715
|
-
normalizeOptionalString2(entry.retired_at),
|
|
716
|
-
normalizeOptionalString2(entry.retired_reason),
|
|
717
|
-
createdAt,
|
|
718
|
-
updatedAt
|
|
719
|
-
]
|
|
720
|
-
});
|
|
721
|
-
return id;
|
|
722
|
-
}
|
|
723
|
-
async function getEntries(executor, ids) {
|
|
724
|
-
const normalizedIds = dedupeStrings(ids);
|
|
725
|
-
if (normalizedIds.length === 0) {
|
|
726
|
-
return [];
|
|
727
|
-
}
|
|
728
|
-
const byId = /* @__PURE__ */ new Map();
|
|
729
|
-
for (const chunk of chunkValues(normalizedIds, LOOKUP_CHUNK_SIZE)) {
|
|
730
|
-
const placeholders = chunk.map(() => "?").join(", ");
|
|
731
|
-
const result = await executor.execute({
|
|
732
|
-
sql: `
|
|
733
|
-
SELECT
|
|
734
|
-
id,
|
|
735
|
-
type,
|
|
736
|
-
subject,
|
|
737
|
-
content,
|
|
738
|
-
importance,
|
|
739
|
-
expiry,
|
|
740
|
-
tags,
|
|
741
|
-
source_file,
|
|
742
|
-
source_context,
|
|
743
|
-
embedding,
|
|
744
|
-
content_hash,
|
|
745
|
-
norm_content_hash,
|
|
746
|
-
quality_score,
|
|
747
|
-
recall_count,
|
|
748
|
-
last_recalled_at,
|
|
749
|
-
superseded_by,
|
|
750
|
-
valid_from,
|
|
751
|
-
valid_to,
|
|
752
|
-
claim_key,
|
|
753
|
-
supersession_kind,
|
|
754
|
-
supersession_reason,
|
|
755
|
-
cluster_id,
|
|
756
|
-
user_id,
|
|
757
|
-
project,
|
|
758
|
-
retired,
|
|
759
|
-
retired_at,
|
|
760
|
-
retired_reason,
|
|
761
|
-
created_at,
|
|
762
|
-
updated_at
|
|
763
|
-
FROM entries
|
|
764
|
-
WHERE id IN (${placeholders})
|
|
765
|
-
AND ${ACTIVE_ENTRY_CLAUSE}
|
|
766
|
-
`,
|
|
767
|
-
args: chunk
|
|
768
|
-
});
|
|
769
|
-
for (const row of result.rows) {
|
|
770
|
-
const entry = mapEntryRow(row);
|
|
771
|
-
byId.set(entry.id, entry);
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
return ids.map((id) => byId.get(id.trim())).filter((entry) => entry !== void 0);
|
|
775
|
-
}
|
|
776
|
-
async function getEntry(executor, id) {
|
|
777
|
-
const [entry] = await getEntries(executor, [id]);
|
|
778
|
-
return entry ?? null;
|
|
779
|
-
}
|
|
780
|
-
async function findExistingHashes(executor, hashes) {
|
|
781
|
-
const normalizedHashes = dedupeStrings(hashes);
|
|
782
|
-
if (normalizedHashes.length === 0) {
|
|
783
|
-
return /* @__PURE__ */ new Set();
|
|
784
|
-
}
|
|
785
|
-
const matches = /* @__PURE__ */ new Set();
|
|
786
|
-
for (const chunk of chunkValues(normalizedHashes, LOOKUP_CHUNK_SIZE)) {
|
|
787
|
-
const placeholders = chunk.map(() => "?").join(", ");
|
|
788
|
-
const result = await executor.execute({
|
|
789
|
-
sql: `
|
|
790
|
-
SELECT DISTINCT content_hash
|
|
791
|
-
FROM entries
|
|
792
|
-
WHERE content_hash IN (${placeholders})
|
|
793
|
-
AND ${ACTIVE_ENTRY_CLAUSE}
|
|
794
|
-
`,
|
|
795
|
-
args: chunk
|
|
796
|
-
});
|
|
797
|
-
for (const row of result.rows) {
|
|
798
|
-
matches.add(readRequiredString(row, "content_hash"));
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
return matches;
|
|
802
|
-
}
|
|
803
|
-
async function findExistingNormHashes(executor, hashes) {
|
|
804
|
-
const normalizedHashes = dedupeStrings(hashes);
|
|
805
|
-
if (normalizedHashes.length === 0) {
|
|
806
|
-
return /* @__PURE__ */ new Set();
|
|
807
|
-
}
|
|
808
|
-
const matches = /* @__PURE__ */ new Set();
|
|
809
|
-
for (const chunk of chunkValues(normalizedHashes, LOOKUP_CHUNK_SIZE)) {
|
|
810
|
-
const placeholders = chunk.map(() => "?").join(", ");
|
|
811
|
-
const result = await executor.execute({
|
|
812
|
-
sql: `
|
|
813
|
-
SELECT DISTINCT norm_content_hash
|
|
814
|
-
FROM entries
|
|
815
|
-
WHERE norm_content_hash IN (${placeholders})
|
|
816
|
-
AND ${ACTIVE_ENTRY_CLAUSE}
|
|
817
|
-
`,
|
|
818
|
-
args: chunk
|
|
819
|
-
});
|
|
820
|
-
for (const row of result.rows) {
|
|
821
|
-
matches.add(readRequiredString(row, "norm_content_hash"));
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
return matches;
|
|
825
|
-
}
|
|
826
|
-
async function retireEntry(executor, id, reason) {
|
|
827
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
828
|
-
const result = await executor.execute({
|
|
829
|
-
sql: `
|
|
830
|
-
UPDATE entries
|
|
831
|
-
SET retired = 1,
|
|
832
|
-
retired_at = ?,
|
|
833
|
-
retired_reason = ?,
|
|
834
|
-
updated_at = ?
|
|
835
|
-
WHERE id = ?
|
|
836
|
-
AND ${ACTIVE_ENTRY_CLAUSE}
|
|
837
|
-
`,
|
|
838
|
-
args: [now, normalizeOptionalString2(reason), now, id]
|
|
839
|
-
});
|
|
840
|
-
return result.rowsAffected > 0;
|
|
841
|
-
}
|
|
842
|
-
async function supersedeEntry(executor, oldId, newId, kind, reason) {
|
|
843
|
-
const normalizedOldId = oldId.trim();
|
|
844
|
-
const normalizedNewId = newId.trim();
|
|
845
|
-
if (normalizedOldId.length === 0 || normalizedNewId.length === 0 || normalizedOldId === normalizedNewId) {
|
|
846
|
-
return false;
|
|
847
|
-
}
|
|
848
|
-
const existing = await executor.execute({
|
|
849
|
-
sql: `
|
|
850
|
-
SELECT id
|
|
851
|
-
FROM entries
|
|
852
|
-
WHERE id = ?
|
|
853
|
-
AND ${ACTIVE_ENTRY_CLAUSE}
|
|
854
|
-
LIMIT 1
|
|
855
|
-
`,
|
|
856
|
-
args: [normalizedOldId]
|
|
857
|
-
});
|
|
858
|
-
if (existing.rows.length === 0) {
|
|
859
|
-
return false;
|
|
860
|
-
}
|
|
861
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
862
|
-
const result = await executor.execute({
|
|
863
|
-
sql: `
|
|
864
|
-
UPDATE entries
|
|
865
|
-
SET superseded_by = ?,
|
|
866
|
-
supersession_kind = ?,
|
|
867
|
-
supersession_reason = ?,
|
|
868
|
-
updated_at = ?
|
|
869
|
-
WHERE id = ?
|
|
870
|
-
AND ${ACTIVE_ENTRY_CLAUSE}
|
|
871
|
-
`,
|
|
872
|
-
args: [normalizedNewId, normalizeOptionalString2(kind) ?? "update", normalizeOptionalString2(reason), now, normalizedOldId]
|
|
873
|
-
});
|
|
874
|
-
return result.rowsAffected > 0;
|
|
875
|
-
}
|
|
876
|
-
async function findActiveEntriesByClaimKey(executor, claimKey) {
|
|
877
|
-
const normalizedClaimKey = claimKey.trim();
|
|
878
|
-
if (normalizedClaimKey.length === 0) {
|
|
879
|
-
return [];
|
|
880
|
-
}
|
|
881
|
-
const result = await executor.execute({
|
|
882
|
-
sql: `
|
|
883
|
-
SELECT *
|
|
884
|
-
FROM entries
|
|
885
|
-
WHERE claim_key = ?
|
|
886
|
-
AND ${ACTIVE_ENTRY_CLAUSE}
|
|
887
|
-
`,
|
|
888
|
-
args: [normalizedClaimKey]
|
|
889
|
-
});
|
|
890
|
-
return result.rows.map((row) => mapEntryRow(row));
|
|
891
|
-
}
|
|
892
|
-
async function getDistinctClaimKeyPrefixes(executor) {
|
|
893
|
-
const result = await executor.execute({
|
|
894
|
-
sql: `
|
|
895
|
-
SELECT DISTINCT lower(trim(substr(claim_key, 1, instr(claim_key, '/') - 1))) AS claim_key_prefix
|
|
896
|
-
FROM entries
|
|
897
|
-
WHERE claim_key IS NOT NULL
|
|
898
|
-
AND instr(claim_key, '/') > 1
|
|
899
|
-
AND ${ACTIVE_ENTRY_CLAUSE}
|
|
900
|
-
ORDER BY claim_key_prefix ASC
|
|
901
|
-
`
|
|
902
|
-
});
|
|
903
|
-
return result.rows.flatMap((row) => {
|
|
904
|
-
const prefix = row.claim_key_prefix;
|
|
905
|
-
return typeof prefix === "string" && prefix.length > 0 ? [prefix] : [];
|
|
906
|
-
});
|
|
907
|
-
}
|
|
908
|
-
async function getClaimKeyExamples(executor, limit = 8) {
|
|
909
|
-
const normalizedLimit = Number.isFinite(limit) ? Math.max(1, Math.floor(limit)) : 8;
|
|
910
|
-
const result = await executor.execute({
|
|
911
|
-
sql: `
|
|
912
|
-
SELECT claim_key
|
|
913
|
-
FROM entries
|
|
914
|
-
WHERE claim_key IS NOT NULL
|
|
915
|
-
AND ${ACTIVE_ENTRY_CLAUSE}
|
|
916
|
-
GROUP BY claim_key
|
|
917
|
-
ORDER BY COUNT(*) DESC, MAX(importance) DESC, MAX(created_at) DESC, claim_key ASC
|
|
918
|
-
LIMIT ?
|
|
919
|
-
`,
|
|
920
|
-
args: [normalizedLimit]
|
|
921
|
-
});
|
|
922
|
-
return result.rows.flatMap((row) => {
|
|
923
|
-
const claimKey = row.claim_key;
|
|
924
|
-
return typeof claimKey === "string" && claimKey.length > 0 ? [claimKey] : [];
|
|
925
|
-
});
|
|
926
|
-
}
|
|
927
|
-
async function updateEntry(executor, id, fields, options) {
|
|
928
|
-
const assignments = [];
|
|
929
|
-
const args = [];
|
|
930
|
-
if (fields.importance !== void 0) {
|
|
931
|
-
assignments.push("importance = ?");
|
|
932
|
-
args.push(normalizeInteger(fields.importance, 0));
|
|
933
|
-
}
|
|
934
|
-
if (fields.expiry !== void 0) {
|
|
935
|
-
assignments.push("expiry = ?");
|
|
936
|
-
args.push(fields.expiry);
|
|
937
|
-
}
|
|
938
|
-
if (fields.claim_key !== void 0) {
|
|
939
|
-
assignments.push("claim_key = ?");
|
|
940
|
-
args.push(normalizeOptionalString2(fields.claim_key));
|
|
941
|
-
}
|
|
942
|
-
if (fields.valid_from !== void 0) {
|
|
943
|
-
assignments.push("valid_from = ?");
|
|
944
|
-
args.push(normalizeOptionalString2(fields.valid_from));
|
|
945
|
-
}
|
|
946
|
-
if (fields.valid_to !== void 0) {
|
|
947
|
-
assignments.push("valid_to = ?");
|
|
948
|
-
args.push(normalizeOptionalString2(fields.valid_to));
|
|
949
|
-
}
|
|
950
|
-
if (assignments.length === 0) {
|
|
951
|
-
return false;
|
|
952
|
-
}
|
|
953
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
954
|
-
assignments.push("updated_at = ?");
|
|
955
|
-
args.push(now, id);
|
|
956
|
-
const result = await executor.execute({
|
|
957
|
-
sql: `
|
|
958
|
-
UPDATE entries
|
|
959
|
-
SET ${assignments.join(", ")}
|
|
960
|
-
WHERE id = ?
|
|
961
|
-
AND ${options?.includeInactive === true ? "1 = 1" : ACTIVE_ENTRY_CLAUSE}
|
|
962
|
-
`,
|
|
963
|
-
args
|
|
964
|
-
});
|
|
965
|
-
return result.rowsAffected > 0;
|
|
966
|
-
}
|
|
967
|
-
async function recordRecallEvent(executor, entryId, query, sessionKey) {
|
|
968
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
969
|
-
const updateResult = await executor.execute({
|
|
970
|
-
sql: `
|
|
971
|
-
UPDATE entries
|
|
972
|
-
SET recall_count = COALESCE(recall_count, 0) + 1,
|
|
973
|
-
last_recalled_at = ?,
|
|
974
|
-
updated_at = ?
|
|
975
|
-
WHERE id = ?
|
|
976
|
-
AND ${ACTIVE_ENTRY_CLAUSE}
|
|
977
|
-
`,
|
|
978
|
-
args: [now, now, entryId]
|
|
979
|
-
});
|
|
980
|
-
if (updateResult.rowsAffected === 0) {
|
|
981
|
-
return;
|
|
982
|
-
}
|
|
983
|
-
await executor.execute({
|
|
984
|
-
sql: `
|
|
985
|
-
INSERT INTO recall_events (
|
|
986
|
-
id,
|
|
987
|
-
entry_id,
|
|
988
|
-
query,
|
|
989
|
-
session_key,
|
|
990
|
-
recalled_at
|
|
991
|
-
)
|
|
992
|
-
VALUES (?, ?, ?, ?, ?)
|
|
993
|
-
`,
|
|
994
|
-
args: [randomUUID2(), entryId, query, normalizeOptionalString2(sessionKey), now]
|
|
995
|
-
});
|
|
996
|
-
}
|
|
997
|
-
async function getIngestLogEntry(executor, filePath) {
|
|
998
|
-
const result = await executor.execute({
|
|
999
|
-
sql: `
|
|
1000
|
-
SELECT file_hash, ingested_at
|
|
1001
|
-
FROM ingest_log
|
|
1002
|
-
WHERE file_path = ?
|
|
1003
|
-
LIMIT 1
|
|
1004
|
-
`,
|
|
1005
|
-
args: [filePath]
|
|
1006
|
-
});
|
|
1007
|
-
const row = result.rows[0];
|
|
1008
|
-
if (!row) {
|
|
1009
|
-
return null;
|
|
1010
|
-
}
|
|
1011
|
-
return {
|
|
1012
|
-
fileHash: readRequiredString(row, "file_hash"),
|
|
1013
|
-
ingestedAt: readRequiredString(row, "ingested_at")
|
|
1014
|
-
};
|
|
1015
|
-
}
|
|
1016
|
-
async function insertIngestLogEntry(executor, filePath, fileHash, entryCount) {
|
|
1017
|
-
await executor.execute({
|
|
1018
|
-
sql: `
|
|
1019
|
-
INSERT INTO ingest_log (
|
|
1020
|
-
file_path,
|
|
1021
|
-
file_hash,
|
|
1022
|
-
ingested_at,
|
|
1023
|
-
entry_count
|
|
1024
|
-
)
|
|
1025
|
-
VALUES (?, ?, ?, ?)
|
|
1026
|
-
ON CONFLICT(file_path) DO UPDATE SET
|
|
1027
|
-
file_hash = excluded.file_hash,
|
|
1028
|
-
ingested_at = excluded.ingested_at,
|
|
1029
|
-
entry_count = excluded.entry_count
|
|
1030
|
-
`,
|
|
1031
|
-
args: [filePath, fileHash, (/* @__PURE__ */ new Date()).toISOString(), normalizeInteger(entryCount, 0)]
|
|
1032
|
-
});
|
|
1033
|
-
}
|
|
1034
|
-
function dedupeStrings(values) {
|
|
1035
|
-
return Array.from(new Set(values.map((value) => value.trim()).filter((value) => value.length > 0)));
|
|
1036
|
-
}
|
|
1037
|
-
function chunkValues(values, size) {
|
|
1038
|
-
const chunks = [];
|
|
1039
|
-
for (let index = 0; index < values.length; index += size) {
|
|
1040
|
-
chunks.push(values.slice(index, index + size));
|
|
1041
|
-
}
|
|
1042
|
-
return chunks;
|
|
1043
|
-
}
|
|
1044
|
-
function normalizeOptionalString2(value) {
|
|
1045
|
-
if (value === void 0) {
|
|
1046
|
-
return null;
|
|
1047
|
-
}
|
|
1048
|
-
const trimmed = value.trim();
|
|
1049
|
-
return trimmed.length > 0 ? trimmed : null;
|
|
1050
|
-
}
|
|
1051
|
-
function normalizeTimestamp(value) {
|
|
1052
|
-
return normalizeOptionalString2(value);
|
|
1053
|
-
}
|
|
1054
|
-
function normalizeNumber(value, fallback) {
|
|
1055
|
-
return Number.isFinite(value) ? value : fallback;
|
|
1056
|
-
}
|
|
1057
|
-
function normalizeInteger(value, fallback) {
|
|
1058
|
-
const normalized = Number.isFinite(value) ? Math.trunc(value) : fallback;
|
|
1059
|
-
return Number.isFinite(normalized) ? normalized : fallback;
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
// src/adapters/db/schema.ts
|
|
1063
|
-
var SCHEMA_VERSION = "7";
|
|
1064
|
-
var VECTOR_INDEX_NAME = "idx_entries_embedding";
|
|
1065
|
-
var EPISODE_VECTOR_INDEX_NAME = "idx_episodes_embedding";
|
|
1066
|
-
var BULK_WRITE_STATE_META_KEY = "bulk_write_state";
|
|
1067
|
-
var LAST_BULK_INGEST_META_KEY = "last_bulk_ingest_at";
|
|
1068
|
-
var CREATE_ENTRIES_TABLE_SQL = `
|
|
1069
|
-
CREATE TABLE IF NOT EXISTS entries (
|
|
1070
|
-
id TEXT PRIMARY KEY,
|
|
1071
|
-
type TEXT NOT NULL,
|
|
1072
|
-
subject TEXT NOT NULL,
|
|
1073
|
-
content TEXT NOT NULL,
|
|
1074
|
-
importance INTEGER NOT NULL,
|
|
1075
|
-
expiry TEXT NOT NULL,
|
|
1076
|
-
tags TEXT,
|
|
1077
|
-
source_file TEXT,
|
|
1078
|
-
source_context TEXT,
|
|
1079
|
-
embedding F32_BLOB(1024),
|
|
1080
|
-
content_hash TEXT,
|
|
1081
|
-
norm_content_hash TEXT,
|
|
1082
|
-
minhash_sig BLOB,
|
|
1083
|
-
quality_score REAL NOT NULL DEFAULT 0.5,
|
|
1084
|
-
recall_count INTEGER DEFAULT 0,
|
|
1085
|
-
last_recalled_at TEXT,
|
|
1086
|
-
superseded_by TEXT REFERENCES entries(id),
|
|
1087
|
-
valid_from TEXT,
|
|
1088
|
-
valid_to TEXT,
|
|
1089
|
-
claim_key TEXT,
|
|
1090
|
-
supersession_kind TEXT,
|
|
1091
|
-
supersession_reason TEXT,
|
|
1092
|
-
cluster_id TEXT,
|
|
1093
|
-
user_id TEXT,
|
|
1094
|
-
project TEXT,
|
|
1095
|
-
retired INTEGER NOT NULL DEFAULT 0,
|
|
1096
|
-
retired_at TEXT,
|
|
1097
|
-
retired_reason TEXT,
|
|
1098
|
-
created_at TEXT NOT NULL,
|
|
1099
|
-
updated_at TEXT NOT NULL
|
|
1100
|
-
)
|
|
1101
|
-
`;
|
|
1102
|
-
var CREATE_ENTRIES_FTS_TABLE_SQL = `
|
|
1103
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS entries_fts USING fts5(
|
|
1104
|
-
content,
|
|
1105
|
-
subject,
|
|
1106
|
-
content=entries,
|
|
1107
|
-
content_rowid=rowid
|
|
1108
|
-
)
|
|
1109
|
-
`;
|
|
1110
|
-
var CREATE_ENTRIES_FTS_INSERT_TRIGGER_SQL = `
|
|
1111
|
-
CREATE TRIGGER IF NOT EXISTS entries_ai AFTER INSERT ON entries
|
|
1112
|
-
WHEN new.retired = 0 AND new.superseded_by IS NULL BEGIN
|
|
1113
|
-
INSERT INTO entries_fts(rowid, content, subject)
|
|
1114
|
-
VALUES (new.rowid, new.content, new.subject);
|
|
1115
|
-
END
|
|
1116
|
-
`;
|
|
1117
|
-
var CREATE_ENTRIES_FTS_DELETE_TRIGGER_SQL = `
|
|
1118
|
-
CREATE TRIGGER IF NOT EXISTS entries_ad AFTER DELETE ON entries
|
|
1119
|
-
WHEN old.retired = 0 AND old.superseded_by IS NULL BEGIN
|
|
1120
|
-
INSERT INTO entries_fts(entries_fts, rowid, content, subject)
|
|
1121
|
-
VALUES ('delete', old.rowid, old.content, old.subject);
|
|
1122
|
-
END
|
|
1123
|
-
`;
|
|
1124
|
-
var CREATE_ENTRIES_FTS_UPDATE_TRIGGER_SQL = `
|
|
1125
|
-
CREATE TRIGGER IF NOT EXISTS entries_au AFTER UPDATE ON entries BEGIN
|
|
1126
|
-
INSERT INTO entries_fts(entries_fts, rowid, content, subject)
|
|
1127
|
-
SELECT 'delete', old.rowid, old.content, old.subject
|
|
1128
|
-
WHERE old.retired = 0 AND old.superseded_by IS NULL;
|
|
1129
|
-
|
|
1130
|
-
INSERT INTO entries_fts(rowid, content, subject)
|
|
1131
|
-
SELECT new.rowid, new.content, new.subject
|
|
1132
|
-
WHERE new.retired = 0 AND new.superseded_by IS NULL;
|
|
1133
|
-
END
|
|
1134
|
-
`;
|
|
1135
|
-
var CREATE_INGEST_LOG_TABLE_SQL = `
|
|
1136
|
-
CREATE TABLE IF NOT EXISTS ingest_log (
|
|
1137
|
-
file_path TEXT PRIMARY KEY,
|
|
1138
|
-
file_hash TEXT NOT NULL,
|
|
1139
|
-
ingested_at TEXT NOT NULL,
|
|
1140
|
-
entry_count INTEGER DEFAULT 0
|
|
1141
|
-
)
|
|
1142
|
-
`;
|
|
1143
|
-
var CREATE_EPISODES_TABLE_SQL = `
|
|
1144
|
-
CREATE TABLE IF NOT EXISTS episodes (
|
|
1145
|
-
id TEXT PRIMARY KEY,
|
|
1146
|
-
source TEXT NOT NULL,
|
|
1147
|
-
source_id TEXT,
|
|
1148
|
-
source_ref TEXT,
|
|
1149
|
-
transcript_hash TEXT,
|
|
1150
|
-
summary_hash TEXT,
|
|
1151
|
-
agent_id TEXT,
|
|
1152
|
-
surface TEXT,
|
|
1153
|
-
started_at TEXT NOT NULL,
|
|
1154
|
-
ended_at TEXT,
|
|
1155
|
-
summary TEXT NOT NULL,
|
|
1156
|
-
tags TEXT,
|
|
1157
|
-
activity_level TEXT,
|
|
1158
|
-
user_id TEXT,
|
|
1159
|
-
project TEXT,
|
|
1160
|
-
gen_model TEXT,
|
|
1161
|
-
gen_version TEXT,
|
|
1162
|
-
message_count INTEGER,
|
|
1163
|
-
embedding F32_BLOB(1024),
|
|
1164
|
-
retired INTEGER NOT NULL DEFAULT 0,
|
|
1165
|
-
retired_at TEXT,
|
|
1166
|
-
retired_reason TEXT,
|
|
1167
|
-
superseded_by TEXT REFERENCES episodes(id),
|
|
1168
|
-
created_at TEXT NOT NULL,
|
|
1169
|
-
updated_at TEXT NOT NULL
|
|
1170
|
-
)
|
|
1171
|
-
`;
|
|
1172
|
-
var CREATE_RECALL_EVENTS_TABLE_SQL = `
|
|
1173
|
-
CREATE TABLE IF NOT EXISTS recall_events (
|
|
1174
|
-
id TEXT PRIMARY KEY,
|
|
1175
|
-
entry_id TEXT NOT NULL REFERENCES entries(id),
|
|
1176
|
-
query TEXT,
|
|
1177
|
-
session_key TEXT,
|
|
1178
|
-
recalled_at TEXT NOT NULL
|
|
1179
|
-
)
|
|
1180
|
-
`;
|
|
1181
|
-
var CREATE_SURGEON_RUNS_TABLE_SQL = `
|
|
1182
|
-
CREATE TABLE IF NOT EXISTS surgeon_runs (
|
|
1183
|
-
id TEXT PRIMARY KEY,
|
|
1184
|
-
pass_type TEXT NOT NULL DEFAULT 'retirement',
|
|
1185
|
-
project TEXT,
|
|
1186
|
-
started_at TEXT NOT NULL,
|
|
1187
|
-
completed_at TEXT,
|
|
1188
|
-
status TEXT NOT NULL DEFAULT 'running',
|
|
1189
|
-
input_tokens INTEGER DEFAULT 0,
|
|
1190
|
-
output_tokens INTEGER DEFAULT 0,
|
|
1191
|
-
estimated_cost_usd REAL DEFAULT 0,
|
|
1192
|
-
model TEXT,
|
|
1193
|
-
actions_taken INTEGER DEFAULT 0,
|
|
1194
|
-
actions_skipped INTEGER DEFAULT 0,
|
|
1195
|
-
entries_retired INTEGER DEFAULT 0,
|
|
1196
|
-
summary TEXT,
|
|
1197
|
-
summary_json TEXT,
|
|
1198
|
-
error TEXT,
|
|
1199
|
-
dry_run INTEGER NOT NULL DEFAULT 1,
|
|
1200
|
-
config_json TEXT
|
|
1201
|
-
)
|
|
1202
|
-
`;
|
|
1203
|
-
var CREATE_SURGEON_RUN_ACTIONS_TABLE_SQL = `
|
|
1204
|
-
CREATE TABLE IF NOT EXISTS surgeon_run_actions (
|
|
1205
|
-
id TEXT PRIMARY KEY,
|
|
1206
|
-
run_id TEXT NOT NULL REFERENCES surgeon_runs(id),
|
|
1207
|
-
action_type TEXT NOT NULL,
|
|
1208
|
-
entry_id TEXT,
|
|
1209
|
-
entry_ids TEXT NOT NULL DEFAULT '[]',
|
|
1210
|
-
reasoning TEXT NOT NULL DEFAULT '',
|
|
1211
|
-
recall_delta TEXT,
|
|
1212
|
-
details_json TEXT,
|
|
1213
|
-
created_at TEXT NOT NULL
|
|
1214
|
-
)
|
|
1215
|
-
`;
|
|
1216
|
-
var CREATE_SURGEON_RUN_ACTIONS_RUN_ID_INDEX_SQL = `
|
|
1217
|
-
CREATE INDEX IF NOT EXISTS idx_surgeon_run_actions_run_id
|
|
1218
|
-
ON surgeon_run_actions(run_id)
|
|
1219
|
-
`;
|
|
1220
|
-
var CREATE_SURGEON_RUN_ACTIONS_ENTRY_ID_INDEX_SQL = `
|
|
1221
|
-
CREATE INDEX IF NOT EXISTS idx_surgeon_run_actions_entry_id
|
|
1222
|
-
ON surgeon_run_actions(entry_id)
|
|
1223
|
-
`;
|
|
1224
|
-
var CREATE_SURGEON_RUN_ACTIONS_CREATED_AT_INDEX_SQL = `
|
|
1225
|
-
CREATE INDEX IF NOT EXISTS idx_surgeon_run_actions_created_at
|
|
1226
|
-
ON surgeon_run_actions(created_at)
|
|
1227
|
-
`;
|
|
1228
|
-
var CREATE_SURGEON_RUN_PROPOSALS_TABLE_SQL = `
|
|
1229
|
-
CREATE TABLE IF NOT EXISTS surgeon_run_proposals (
|
|
1230
|
-
id TEXT PRIMARY KEY,
|
|
1231
|
-
run_id TEXT NOT NULL REFERENCES surgeon_runs(id),
|
|
1232
|
-
group_id TEXT NOT NULL,
|
|
1233
|
-
issue_kind TEXT NOT NULL,
|
|
1234
|
-
scope TEXT NOT NULL,
|
|
1235
|
-
entry_ids TEXT NOT NULL DEFAULT '[]',
|
|
1236
|
-
current_claim_keys TEXT NOT NULL DEFAULT '[]',
|
|
1237
|
-
proposed_claim_keys TEXT NOT NULL DEFAULT '[]',
|
|
1238
|
-
rationale TEXT NOT NULL DEFAULT '',
|
|
1239
|
-
confidence REAL NOT NULL DEFAULT 0,
|
|
1240
|
-
source TEXT NOT NULL DEFAULT '',
|
|
1241
|
-
eligible_for_apply INTEGER NOT NULL DEFAULT 0,
|
|
1242
|
-
created_at TEXT NOT NULL
|
|
1243
|
-
)
|
|
1244
|
-
`;
|
|
1245
|
-
var CREATE_SURGEON_RUN_PROPOSALS_RUN_ID_INDEX_SQL = `
|
|
1246
|
-
CREATE INDEX IF NOT EXISTS idx_surgeon_run_proposals_run_id
|
|
1247
|
-
ON surgeon_run_proposals(run_id)
|
|
1248
|
-
`;
|
|
1249
|
-
var CREATE_SURGEON_RUN_PROPOSALS_GROUP_ID_INDEX_SQL = `
|
|
1250
|
-
CREATE INDEX IF NOT EXISTS idx_surgeon_run_proposals_group_id
|
|
1251
|
-
ON surgeon_run_proposals(group_id)
|
|
1252
|
-
`;
|
|
1253
|
-
var CREATE_SURGEON_RUN_PROPOSALS_CREATED_AT_INDEX_SQL = `
|
|
1254
|
-
CREATE INDEX IF NOT EXISTS idx_surgeon_run_proposals_created_at
|
|
1255
|
-
ON surgeon_run_proposals(created_at)
|
|
1256
|
-
`;
|
|
1257
|
-
var CREATE_META_TABLE_SQL = `
|
|
1258
|
-
CREATE TABLE IF NOT EXISTS _meta (
|
|
1259
|
-
key TEXT PRIMARY KEY,
|
|
1260
|
-
value TEXT
|
|
1261
|
-
)
|
|
1262
|
-
`;
|
|
1263
|
-
var CREATE_ENTRIES_CONTENT_HASH_INDEX_SQL = `
|
|
1264
|
-
CREATE INDEX IF NOT EXISTS idx_entries_content_hash
|
|
1265
|
-
ON entries(content_hash)
|
|
1266
|
-
`;
|
|
1267
|
-
var CREATE_ENTRIES_NORM_CONTENT_HASH_INDEX_SQL = `
|
|
1268
|
-
CREATE INDEX IF NOT EXISTS idx_entries_norm_content_hash
|
|
1269
|
-
ON entries(norm_content_hash)
|
|
1270
|
-
`;
|
|
1271
|
-
var CREATE_ENTRIES_TYPE_INDEX_SQL = `
|
|
1272
|
-
CREATE INDEX IF NOT EXISTS idx_entries_type
|
|
1273
|
-
ON entries(type)
|
|
1274
|
-
`;
|
|
1275
|
-
var CREATE_ENTRIES_EXPIRY_INDEX_SQL = `
|
|
1276
|
-
CREATE INDEX IF NOT EXISTS idx_entries_expiry
|
|
1277
|
-
ON entries(expiry)
|
|
1278
|
-
`;
|
|
1279
|
-
var CREATE_ENTRIES_RETIRED_INDEX_SQL = `
|
|
1280
|
-
CREATE INDEX IF NOT EXISTS idx_entries_retired
|
|
1281
|
-
ON entries(retired)
|
|
1282
|
-
`;
|
|
1283
|
-
var CREATE_ENTRIES_CREATED_AT_INDEX_SQL = `
|
|
1284
|
-
CREATE INDEX IF NOT EXISTS idx_entries_created_at
|
|
1285
|
-
ON entries(created_at)
|
|
1286
|
-
`;
|
|
1287
|
-
var CREATE_ENTRIES_CLAIM_KEY_INDEX_SQL = `
|
|
1288
|
-
CREATE INDEX IF NOT EXISTS idx_entries_claim_key
|
|
1289
|
-
ON entries(claim_key)
|
|
1290
|
-
WHERE claim_key IS NOT NULL
|
|
1291
|
-
`;
|
|
1292
|
-
var CREATE_ENTRIES_VALID_FROM_INDEX_SQL = `
|
|
1293
|
-
CREATE INDEX IF NOT EXISTS idx_entries_valid_from
|
|
1294
|
-
ON entries(valid_from)
|
|
1295
|
-
WHERE valid_from IS NOT NULL
|
|
1296
|
-
`;
|
|
1297
|
-
var CREATE_ENTRIES_VALID_TO_INDEX_SQL = `
|
|
1298
|
-
CREATE INDEX IF NOT EXISTS idx_entries_valid_to
|
|
1299
|
-
ON entries(valid_to)
|
|
1300
|
-
WHERE valid_to IS NOT NULL
|
|
1301
|
-
`;
|
|
1302
|
-
var CREATE_EPISODES_STARTED_AT_INDEX_SQL = `
|
|
1303
|
-
CREATE INDEX IF NOT EXISTS idx_episodes_started_at
|
|
1304
|
-
ON episodes(started_at)
|
|
1305
|
-
`;
|
|
1306
|
-
var CREATE_EPISODES_ENDED_AT_INDEX_SQL = `
|
|
1307
|
-
CREATE INDEX IF NOT EXISTS idx_episodes_ended_at
|
|
1308
|
-
ON episodes(ended_at)
|
|
1309
|
-
`;
|
|
1310
|
-
var CREATE_EPISODES_SOURCE_INDEX_SQL = `
|
|
1311
|
-
CREATE INDEX IF NOT EXISTS idx_episodes_source
|
|
1312
|
-
ON episodes(source)
|
|
1313
|
-
`;
|
|
1314
|
-
var CREATE_EPISODES_SOURCE_ID_INDEX_SQL = `
|
|
1315
|
-
CREATE INDEX IF NOT EXISTS idx_episodes_source_id
|
|
1316
|
-
ON episodes(source_id)
|
|
1317
|
-
`;
|
|
1318
|
-
var CREATE_EPISODES_RETIRED_INDEX_SQL = `
|
|
1319
|
-
CREATE INDEX IF NOT EXISTS idx_episodes_retired
|
|
1320
|
-
ON episodes(retired)
|
|
1321
|
-
`;
|
|
1322
|
-
var CREATE_EPISODES_SOURCE_SOURCE_ID_UNIQUE_INDEX_SQL = `
|
|
1323
|
-
CREATE UNIQUE INDEX IF NOT EXISTS idx_episodes_source_source_id
|
|
1324
|
-
ON episodes(source, source_id)
|
|
1325
|
-
WHERE source_id IS NOT NULL
|
|
1326
|
-
`;
|
|
1327
|
-
var CREATE_RECALL_EVENTS_ENTRY_ID_INDEX_SQL = `
|
|
1328
|
-
CREATE INDEX IF NOT EXISTS idx_recall_events_entry_id
|
|
1329
|
-
ON recall_events(entry_id)
|
|
1330
|
-
`;
|
|
1331
|
-
var CREATE_RECALL_EVENTS_RECALLED_AT_INDEX_SQL = `
|
|
1332
|
-
CREATE INDEX IF NOT EXISTS idx_recall_events_recalled_at
|
|
1333
|
-
ON recall_events(recalled_at)
|
|
1334
|
-
`;
|
|
1335
|
-
var CREATE_ENTRIES_EMBEDDING_INDEX_SQL = `
|
|
1336
|
-
CREATE INDEX IF NOT EXISTS idx_entries_embedding ON entries (
|
|
1337
|
-
libsql_vector_idx(
|
|
1338
|
-
embedding,
|
|
1339
|
-
'metric=cosine',
|
|
1340
|
-
'compress_neighbors=float8',
|
|
1341
|
-
'max_neighbors=50'
|
|
1342
|
-
)
|
|
1343
|
-
)
|
|
1344
|
-
WHERE embedding IS NOT NULL
|
|
1345
|
-
AND retired = 0
|
|
1346
|
-
AND superseded_by IS NULL
|
|
1347
|
-
`;
|
|
1348
|
-
var CREATE_EPISODES_EMBEDDING_INDEX_SQL = `
|
|
1349
|
-
CREATE INDEX IF NOT EXISTS idx_episodes_embedding ON episodes (
|
|
1350
|
-
libsql_vector_idx(
|
|
1351
|
-
embedding,
|
|
1352
|
-
'metric=cosine',
|
|
1353
|
-
'compress_neighbors=float8',
|
|
1354
|
-
'max_neighbors=50'
|
|
1355
|
-
)
|
|
1356
|
-
)
|
|
1357
|
-
WHERE embedding IS NOT NULL
|
|
1358
|
-
AND retired = 0
|
|
1359
|
-
AND superseded_by IS NULL
|
|
1360
|
-
`;
|
|
1361
|
-
var SCHEMA_STATEMENTS = [
|
|
1362
|
-
CREATE_ENTRIES_TABLE_SQL,
|
|
1363
|
-
CREATE_ENTRIES_FTS_TABLE_SQL,
|
|
1364
|
-
CREATE_ENTRIES_FTS_INSERT_TRIGGER_SQL,
|
|
1365
|
-
CREATE_ENTRIES_FTS_DELETE_TRIGGER_SQL,
|
|
1366
|
-
CREATE_ENTRIES_FTS_UPDATE_TRIGGER_SQL,
|
|
1367
|
-
CREATE_INGEST_LOG_TABLE_SQL,
|
|
1368
|
-
CREATE_EPISODES_TABLE_SQL,
|
|
1369
|
-
CREATE_RECALL_EVENTS_TABLE_SQL,
|
|
1370
|
-
CREATE_SURGEON_RUNS_TABLE_SQL,
|
|
1371
|
-
CREATE_SURGEON_RUN_ACTIONS_TABLE_SQL,
|
|
1372
|
-
CREATE_SURGEON_RUN_ACTIONS_RUN_ID_INDEX_SQL,
|
|
1373
|
-
CREATE_SURGEON_RUN_ACTIONS_ENTRY_ID_INDEX_SQL,
|
|
1374
|
-
CREATE_SURGEON_RUN_ACTIONS_CREATED_AT_INDEX_SQL,
|
|
1375
|
-
CREATE_SURGEON_RUN_PROPOSALS_TABLE_SQL,
|
|
1376
|
-
CREATE_SURGEON_RUN_PROPOSALS_RUN_ID_INDEX_SQL,
|
|
1377
|
-
CREATE_SURGEON_RUN_PROPOSALS_GROUP_ID_INDEX_SQL,
|
|
1378
|
-
CREATE_SURGEON_RUN_PROPOSALS_CREATED_AT_INDEX_SQL,
|
|
1379
|
-
CREATE_META_TABLE_SQL,
|
|
1380
|
-
CREATE_ENTRIES_CONTENT_HASH_INDEX_SQL,
|
|
1381
|
-
CREATE_ENTRIES_NORM_CONTENT_HASH_INDEX_SQL,
|
|
1382
|
-
CREATE_ENTRIES_TYPE_INDEX_SQL,
|
|
1383
|
-
CREATE_ENTRIES_EXPIRY_INDEX_SQL,
|
|
1384
|
-
CREATE_ENTRIES_RETIRED_INDEX_SQL,
|
|
1385
|
-
CREATE_ENTRIES_CREATED_AT_INDEX_SQL,
|
|
1386
|
-
CREATE_ENTRIES_CLAIM_KEY_INDEX_SQL,
|
|
1387
|
-
CREATE_ENTRIES_VALID_FROM_INDEX_SQL,
|
|
1388
|
-
CREATE_ENTRIES_VALID_TO_INDEX_SQL,
|
|
1389
|
-
CREATE_EPISODES_STARTED_AT_INDEX_SQL,
|
|
1390
|
-
CREATE_EPISODES_ENDED_AT_INDEX_SQL,
|
|
1391
|
-
CREATE_EPISODES_SOURCE_INDEX_SQL,
|
|
1392
|
-
CREATE_EPISODES_SOURCE_ID_INDEX_SQL,
|
|
1393
|
-
CREATE_EPISODES_RETIRED_INDEX_SQL,
|
|
1394
|
-
CREATE_EPISODES_SOURCE_SOURCE_ID_UNIQUE_INDEX_SQL,
|
|
1395
|
-
CREATE_RECALL_EVENTS_ENTRY_ID_INDEX_SQL,
|
|
1396
|
-
CREATE_RECALL_EVENTS_RECALLED_AT_INDEX_SQL
|
|
1397
|
-
];
|
|
1398
|
-
async function initSchema(db) {
|
|
1399
|
-
await db.execute("PRAGMA foreign_keys = ON");
|
|
1400
|
-
let currentVersion = await getSchemaVersion(db);
|
|
1401
|
-
await assertSupportedSchemaState(db, currentVersion);
|
|
1402
|
-
if (currentVersion === "5") {
|
|
1403
|
-
await migrateV5ToV6(db);
|
|
1404
|
-
currentVersion = "6";
|
|
1405
|
-
}
|
|
1406
|
-
if (currentVersion === "6") {
|
|
1407
|
-
await migrateV6ToV7(db);
|
|
1408
|
-
currentVersion = "7";
|
|
1409
|
-
}
|
|
1410
|
-
const hadEntriesFts = await tableExists(db, "entries_fts");
|
|
1411
|
-
for (const statement of SCHEMA_STATEMENTS) {
|
|
1412
|
-
await db.execute(statement);
|
|
1413
|
-
}
|
|
1414
|
-
await db.execute({
|
|
1415
|
-
sql: `
|
|
1416
|
-
INSERT INTO _meta (key, value)
|
|
1417
|
-
VALUES ('schema_version', ?)
|
|
1418
|
-
ON CONFLICT(key) DO UPDATE SET value = excluded.value
|
|
1419
|
-
`,
|
|
1420
|
-
args: [SCHEMA_VERSION]
|
|
1421
|
-
});
|
|
1422
|
-
if (await hasActiveBulkWriteState(db)) {
|
|
1423
|
-
await finalizeBulkWrites(db);
|
|
1424
|
-
return;
|
|
1425
|
-
}
|
|
1426
|
-
if (currentVersion !== SCHEMA_VERSION || !hadEntriesFts) {
|
|
1427
|
-
await rebuildFts(db);
|
|
1428
|
-
}
|
|
1429
|
-
await ensureVectorIndexes(db);
|
|
1430
|
-
}
|
|
1431
|
-
async function assertSupportedSchemaState(db, currentVersion) {
|
|
1432
|
-
if (currentVersion && currentVersion !== "5" && currentVersion !== "6" && currentVersion !== SCHEMA_VERSION) {
|
|
1433
|
-
throw new Error(
|
|
1434
|
-
`Unsupported agenr database schema version "${currentVersion}". This build only supports schema version ${SCHEMA_VERSION}. Create a fresh database with \`agenr db reset\` or manually migrate the data into a new database.`
|
|
1435
|
-
);
|
|
1436
|
-
}
|
|
1437
|
-
if (currentVersion !== null) {
|
|
1438
|
-
return;
|
|
1439
|
-
}
|
|
1440
|
-
const existingTables = await listUserTables(db);
|
|
1441
|
-
if (existingTables.length === 0) {
|
|
1442
|
-
return;
|
|
1443
|
-
}
|
|
1444
|
-
throw new Error(
|
|
1445
|
-
`Unsupported agenr database without schema metadata. This build only supports a fresh database or one already initialized at schema version ${SCHEMA_VERSION}. Create a fresh database with \`agenr db reset\` or manually migrate the data into a new database.`
|
|
1446
|
-
);
|
|
1447
|
-
}
|
|
1448
|
-
async function migrateV5ToV6(db) {
|
|
1449
|
-
const columns = ["valid_from TEXT", "valid_to TEXT", "claim_key TEXT", "supersession_kind TEXT", "supersession_reason TEXT"];
|
|
1450
|
-
for (const column of columns) {
|
|
1451
|
-
try {
|
|
1452
|
-
await db.execute(`ALTER TABLE entries ADD COLUMN ${column}`);
|
|
1453
|
-
} catch (error) {
|
|
1454
|
-
if (!(error instanceof Error && /duplicate column/i.test(error.message))) {
|
|
1455
|
-
throw error;
|
|
1456
|
-
}
|
|
1457
|
-
}
|
|
1458
|
-
}
|
|
1459
|
-
}
|
|
1460
|
-
async function migrateV6ToV7(db) {
|
|
1461
|
-
if (await tableExists(db, "surgeon_run_actions")) {
|
|
1462
|
-
try {
|
|
1463
|
-
await db.execute("ALTER TABLE surgeon_run_actions ADD COLUMN details_json TEXT");
|
|
1464
|
-
} catch (error) {
|
|
1465
|
-
if (!(error instanceof Error && /duplicate column/i.test(error.message))) {
|
|
1466
|
-
throw error;
|
|
1467
|
-
}
|
|
1468
|
-
}
|
|
1469
|
-
}
|
|
1470
|
-
await db.execute(CREATE_SURGEON_RUN_PROPOSALS_TABLE_SQL);
|
|
1471
|
-
await db.execute(CREATE_SURGEON_RUN_PROPOSALS_RUN_ID_INDEX_SQL);
|
|
1472
|
-
await db.execute(CREATE_SURGEON_RUN_PROPOSALS_GROUP_ID_INDEX_SQL);
|
|
1473
|
-
await db.execute(CREATE_SURGEON_RUN_PROPOSALS_CREATED_AT_INDEX_SQL);
|
|
1474
|
-
}
|
|
1475
|
-
async function rebuildFts(db) {
|
|
1476
|
-
await db.execute("INSERT INTO entries_fts(entries_fts) VALUES ('rebuild')");
|
|
1477
|
-
}
|
|
1478
|
-
async function prepareBulkWrites(db) {
|
|
1479
|
-
await runImmediateTransaction(db, async () => {
|
|
1480
|
-
await db.execute({
|
|
1481
|
-
sql: `
|
|
1482
|
-
INSERT INTO _meta (key, value)
|
|
1483
|
-
VALUES (?, 'active')
|
|
1484
|
-
ON CONFLICT(key) DO UPDATE SET value = excluded.value
|
|
1485
|
-
`,
|
|
1486
|
-
args: [BULK_WRITE_STATE_META_KEY]
|
|
1487
|
-
});
|
|
1488
|
-
await db.execute("DROP TRIGGER IF EXISTS entries_ai");
|
|
1489
|
-
await db.execute("DROP TRIGGER IF EXISTS entries_ad");
|
|
1490
|
-
await db.execute("DROP TRIGGER IF EXISTS entries_au");
|
|
1491
|
-
await dropVectorIndexes(db);
|
|
1492
|
-
});
|
|
1493
|
-
}
|
|
1494
|
-
async function finalizeBulkWrites(db) {
|
|
1495
|
-
await runImmediateTransaction(db, async () => {
|
|
1496
|
-
await db.execute(CREATE_ENTRIES_FTS_INSERT_TRIGGER_SQL);
|
|
1497
|
-
await db.execute(CREATE_ENTRIES_FTS_DELETE_TRIGGER_SQL);
|
|
1498
|
-
await db.execute(CREATE_ENTRIES_FTS_UPDATE_TRIGGER_SQL);
|
|
1499
|
-
await rebuildFts(db);
|
|
1500
|
-
await ensureVectorIndexes(db);
|
|
1501
|
-
await db.execute({
|
|
1502
|
-
sql: "DELETE FROM _meta WHERE key = ?",
|
|
1503
|
-
args: [BULK_WRITE_STATE_META_KEY]
|
|
1504
|
-
});
|
|
1505
|
-
await db.execute({
|
|
1506
|
-
sql: `
|
|
1507
|
-
INSERT INTO _meta (key, value)
|
|
1508
|
-
VALUES (?, ?)
|
|
1509
|
-
ON CONFLICT(key) DO UPDATE SET value = excluded.value
|
|
1510
|
-
`,
|
|
1511
|
-
args: [LAST_BULK_INGEST_META_KEY, (/* @__PURE__ */ new Date()).toISOString()]
|
|
1512
|
-
});
|
|
1513
|
-
});
|
|
1514
|
-
}
|
|
1515
|
-
async function getLastBulkIngestAt(db) {
|
|
1516
|
-
try {
|
|
1517
|
-
const result = await db.execute({
|
|
1518
|
-
sql: "SELECT value FROM _meta WHERE key = ? LIMIT 1",
|
|
1519
|
-
args: [LAST_BULK_INGEST_META_KEY]
|
|
1520
|
-
});
|
|
1521
|
-
const row = result.rows[0];
|
|
1522
|
-
return row?.value ? String(row.value) : null;
|
|
1523
|
-
} catch {
|
|
1524
|
-
return null;
|
|
1525
|
-
}
|
|
1526
|
-
}
|
|
1527
|
-
async function getSchemaVersion(db) {
|
|
1528
|
-
try {
|
|
1529
|
-
const result = await db.execute("SELECT value FROM _meta WHERE key = 'schema_version' LIMIT 1");
|
|
1530
|
-
const row = result.rows[0];
|
|
1531
|
-
if (!row) {
|
|
1532
|
-
return null;
|
|
1533
|
-
}
|
|
1534
|
-
const value = row.value;
|
|
1535
|
-
return typeof value === "string" ? value : null;
|
|
1536
|
-
} catch {
|
|
1537
|
-
return null;
|
|
1538
|
-
}
|
|
1539
|
-
}
|
|
1540
|
-
async function tableExists(db, tableName) {
|
|
1541
|
-
const result = await db.execute({
|
|
1542
|
-
sql: `
|
|
1543
|
-
SELECT 1
|
|
1544
|
-
FROM sqlite_master
|
|
1545
|
-
WHERE type = 'table'
|
|
1546
|
-
AND name = ?
|
|
1547
|
-
LIMIT 1
|
|
1548
|
-
`,
|
|
1549
|
-
args: [tableName]
|
|
1550
|
-
});
|
|
1551
|
-
return result.rows.length > 0;
|
|
1552
|
-
}
|
|
1553
|
-
async function listUserTables(db) {
|
|
1554
|
-
const result = await db.execute({
|
|
1555
|
-
sql: `
|
|
1556
|
-
SELECT name
|
|
1557
|
-
FROM sqlite_master
|
|
1558
|
-
WHERE type = 'table'
|
|
1559
|
-
AND name NOT LIKE 'sqlite_%'
|
|
1560
|
-
ORDER BY name
|
|
1561
|
-
`
|
|
1562
|
-
});
|
|
1563
|
-
return result.rows.flatMap((row) => {
|
|
1564
|
-
const name = row.name;
|
|
1565
|
-
return typeof name === "string" ? [name] : [];
|
|
1566
|
-
});
|
|
1567
|
-
}
|
|
1568
|
-
async function hasActiveBulkWriteState(db) {
|
|
1569
|
-
try {
|
|
1570
|
-
const result = await db.execute({
|
|
1571
|
-
sql: "SELECT value FROM _meta WHERE key = ? LIMIT 1",
|
|
1572
|
-
args: [BULK_WRITE_STATE_META_KEY]
|
|
1573
|
-
});
|
|
1574
|
-
const row = result.rows[0];
|
|
1575
|
-
return row?.value === "active";
|
|
1576
|
-
} catch {
|
|
1577
|
-
return false;
|
|
1578
|
-
}
|
|
1579
|
-
}
|
|
1580
|
-
async function ensureVectorIndexes(db) {
|
|
1581
|
-
try {
|
|
1582
|
-
await db.execute(CREATE_ENTRIES_EMBEDDING_INDEX_SQL);
|
|
1583
|
-
await db.execute(CREATE_EPISODES_EMBEDDING_INDEX_SQL);
|
|
1584
|
-
} catch (error) {
|
|
1585
|
-
if (!isVectorUnavailableError(error)) {
|
|
1586
|
-
throw error;
|
|
1587
|
-
}
|
|
1588
|
-
}
|
|
1589
|
-
}
|
|
1590
|
-
async function dropVectorIndexes(db) {
|
|
1591
|
-
try {
|
|
1592
|
-
await db.execute(`DROP INDEX IF EXISTS ${VECTOR_INDEX_NAME}`);
|
|
1593
|
-
await db.execute(`DROP INDEX IF EXISTS ${EPISODE_VECTOR_INDEX_NAME}`);
|
|
1594
|
-
} catch (error) {
|
|
1595
|
-
if (!isVectorUnavailableError(error)) {
|
|
1596
|
-
throw error;
|
|
1597
|
-
}
|
|
1598
|
-
}
|
|
1599
|
-
}
|
|
1600
|
-
async function runImmediateTransaction(db, fn) {
|
|
1601
|
-
await db.execute("BEGIN IMMEDIATE");
|
|
1602
|
-
try {
|
|
1603
|
-
await fn();
|
|
1604
|
-
await db.execute("COMMIT");
|
|
1605
|
-
} catch (error) {
|
|
1606
|
-
try {
|
|
1607
|
-
await db.execute("ROLLBACK");
|
|
1608
|
-
} catch {
|
|
1609
|
-
}
|
|
1610
|
-
throw error;
|
|
1611
|
-
}
|
|
1612
|
-
}
|
|
1613
|
-
function isVectorUnavailableError(error) {
|
|
1614
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1615
|
-
return /libsql_vector_idx|vector32|vector_top_k|vector|no such function|unsupported/i.test(message);
|
|
1616
|
-
}
|
|
1617
|
-
|
|
1618
|
-
// src/adapters/db/client.ts
|
|
1619
|
-
var DEFAULT_BUSY_TIMEOUT_MS = 3e3;
|
|
1620
|
-
async function createDatabase(dbPath) {
|
|
1621
|
-
const client = await openClient(dbPath);
|
|
1622
|
-
const database = new LibsqlDatabase(client, client);
|
|
1623
|
-
await database.init();
|
|
1624
|
-
return database;
|
|
1625
|
-
}
|
|
1626
|
-
var LibsqlDatabase = class _LibsqlDatabase {
|
|
1627
|
-
/** Creates a database adapter over a shared client and SQL executor. */
|
|
1628
|
-
constructor(client, executor) {
|
|
1629
|
-
this.client = client;
|
|
1630
|
-
this.executor = executor;
|
|
1631
|
-
}
|
|
1632
|
-
/** Inserts a new entry row and its derived storage fields. */
|
|
1633
|
-
async insertEntry(entry, embedding, contentHash) {
|
|
1634
|
-
return insertEntry(this.executor, entry, embedding, contentHash);
|
|
1635
|
-
}
|
|
1636
|
-
/** Drops indexes and triggers that slow down bulk ingest writes. */
|
|
1637
|
-
async prepareForBulkWrites() {
|
|
1638
|
-
await prepareBulkWrites(this.client);
|
|
1639
|
-
}
|
|
1640
|
-
/** Restores indexes and triggers after bulk ingest writes complete. */
|
|
1641
|
-
async finalizeBulkWrites() {
|
|
1642
|
-
await finalizeBulkWrites(this.client);
|
|
1643
|
-
}
|
|
1644
|
-
/** Loads entries by identifier while preserving caller order when possible. */
|
|
1645
|
-
async getEntries(ids) {
|
|
1646
|
-
return getEntries(this.executor, ids);
|
|
1647
|
-
}
|
|
1648
|
-
/** Loads a single entry by identifier. */
|
|
1649
|
-
async getEntry(id) {
|
|
1650
|
-
return getEntry(this.executor, id);
|
|
1651
|
-
}
|
|
1652
|
-
/** Finds which exact content hashes already exist in storage. */
|
|
1653
|
-
async findExistingHashes(hashes) {
|
|
1654
|
-
return findExistingHashes(this.executor, hashes);
|
|
1655
|
-
}
|
|
1656
|
-
/** Loads one episode by stable `(source, sourceId)` identity. */
|
|
1657
|
-
async getEpisodeBySourceId(source, sourceId) {
|
|
1658
|
-
return getEpisodeBySourceId(this.executor, source, sourceId);
|
|
1659
|
-
}
|
|
1660
|
-
/** Loads one episode by fallback `(source, transcriptHash)` identity. */
|
|
1661
|
-
async getEpisodeByTranscriptHash(source, transcriptHash) {
|
|
1662
|
-
return getEpisodeByTranscriptHash(this.executor, source, transcriptHash);
|
|
1663
|
-
}
|
|
1664
|
-
/** Inserts or updates an episodic-memory row using normalized change detection. */
|
|
1665
|
-
async upsertEpisode(input) {
|
|
1666
|
-
return upsertEpisode(this.executor, input);
|
|
1667
|
-
}
|
|
1668
|
-
/** Lists active episodes whose time range overlaps the requested window. */
|
|
1669
|
-
async listEpisodesByTimeWindow(window, limit) {
|
|
1670
|
-
return listEpisodesByTimeWindow(this.executor, window, limit);
|
|
1671
|
-
}
|
|
1672
|
-
/** Finds active episodes by vector similarity. */
|
|
1673
|
-
async episodeVectorSearch(params) {
|
|
1674
|
-
return episodeVectorSearch(this.executor, params);
|
|
1675
|
-
}
|
|
1676
|
-
/** Lists active episodes that are still missing embeddings. */
|
|
1677
|
-
async listEpisodesWithoutEmbeddings(limit) {
|
|
1678
|
-
return listEpisodesWithoutEmbeddings(this.executor, limit);
|
|
1679
|
-
}
|
|
1680
|
-
/** Updates only the embedding payload for one episode row. */
|
|
1681
|
-
async updateEpisodeEmbedding(id, embedding) {
|
|
1682
|
-
await updateEpisodeEmbedding(this.executor, id, embedding);
|
|
1683
|
-
}
|
|
1684
|
-
/** Finds which normalized content hashes already exist in storage. */
|
|
1685
|
-
async findExistingNormHashes(hashes) {
|
|
1686
|
-
return findExistingNormHashes(this.executor, hashes);
|
|
1687
|
-
}
|
|
1688
|
-
/** Marks an entry as retired with an optional reason. */
|
|
1689
|
-
async retireEntry(id, reason) {
|
|
1690
|
-
return retireEntry(this.executor, id, reason);
|
|
1691
|
-
}
|
|
1692
|
-
/** Marks one active entry as superseded by a newer entry. */
|
|
1693
|
-
async supersedeEntry(oldId, newId, kind, reason) {
|
|
1694
|
-
return supersedeEntry(this.executor, oldId, newId, kind, reason);
|
|
1695
|
-
}
|
|
1696
|
-
/** Finds active entries by exact claim key. */
|
|
1697
|
-
async findActiveEntriesByClaimKey(claimKey) {
|
|
1698
|
-
return findActiveEntriesByClaimKey(this.executor, claimKey);
|
|
1699
|
-
}
|
|
1700
|
-
/** Lists distinct entity prefixes derived from active claim keys. */
|
|
1701
|
-
async getDistinctClaimKeyPrefixes() {
|
|
1702
|
-
return getDistinctClaimKeyPrefixes(this.executor);
|
|
1703
|
-
}
|
|
1704
|
-
/** Lists bounded full claim-key examples ordered for extraction hinting. */
|
|
1705
|
-
async getClaimKeyExamples(limit) {
|
|
1706
|
-
return getClaimKeyExamples(this.executor, limit);
|
|
1707
|
-
}
|
|
1708
|
-
/** Updates mutable entry fields such as importance, expiry, and temporal metadata. */
|
|
1709
|
-
async updateEntry(id, fields) {
|
|
1710
|
-
return updateEntry(this.executor, id, fields);
|
|
1711
|
-
}
|
|
1712
|
-
/** Looks up the ingest log row for a previously processed file. */
|
|
1713
|
-
async getIngestLogEntry(filePath) {
|
|
1714
|
-
return getIngestLogEntry(this.executor, filePath);
|
|
1715
|
-
}
|
|
1716
|
-
/** Upserts ingest metadata for a processed transcript file. */
|
|
1717
|
-
async insertIngestLogEntry(filePath, fileHash, entryCount) {
|
|
1718
|
-
return insertIngestLogEntry(this.executor, filePath, fileHash, entryCount);
|
|
1719
|
-
}
|
|
1720
|
-
/** Ensures the schema exists before the adapter is used. */
|
|
1721
|
-
async init() {
|
|
1722
|
-
await initSchema(this.client);
|
|
1723
|
-
}
|
|
1724
|
-
/** Closes the underlying libSQL client. */
|
|
1725
|
-
async close() {
|
|
1726
|
-
this.client.close();
|
|
1727
|
-
}
|
|
1728
|
-
/** Executes a callback inside a write transaction when supported. */
|
|
1729
|
-
async withTransaction(fn) {
|
|
1730
|
-
if (this.executor !== this.client) {
|
|
1731
|
-
return fn(this);
|
|
1732
|
-
}
|
|
1733
|
-
const transaction = await this.client.transaction("write");
|
|
1734
|
-
const transactionDb = new _LibsqlDatabase(this.client, transaction);
|
|
1735
|
-
try {
|
|
1736
|
-
const result = await fn(transactionDb);
|
|
1737
|
-
await transaction.commit();
|
|
1738
|
-
return result;
|
|
1739
|
-
} catch (error) {
|
|
1740
|
-
await rollbackTransaction(transaction);
|
|
1741
|
-
throw error;
|
|
1742
|
-
} finally {
|
|
1743
|
-
transaction.close();
|
|
1744
|
-
}
|
|
1745
|
-
}
|
|
1746
|
-
/** Executes one statement through the shared libSQL executor. */
|
|
1747
|
-
async execute(statementOrSql, args) {
|
|
1748
|
-
if (typeof statementOrSql === "string") {
|
|
1749
|
-
return this.executor.execute(statementOrSql, args);
|
|
1750
|
-
}
|
|
1751
|
-
return this.executor.execute(statementOrSql);
|
|
1752
|
-
}
|
|
1753
|
-
};
|
|
1754
|
-
async function openClient(dbPath) {
|
|
1755
|
-
const trimmedPath = dbPath.trim();
|
|
1756
|
-
if (trimmedPath.length === 0) {
|
|
1757
|
-
throw new Error("Database path must not be empty.");
|
|
1758
|
-
}
|
|
1759
|
-
if (trimmedPath !== ":memory:" && !trimmedPath.startsWith("file:")) {
|
|
1760
|
-
const resolvedPath = path.resolve(trimmedPath);
|
|
1761
|
-
await fs.mkdir(path.dirname(resolvedPath), { recursive: true });
|
|
1762
|
-
}
|
|
1763
|
-
const client = createClient({ url: resolveClientUrl(trimmedPath) });
|
|
1764
|
-
await client.execute("PRAGMA foreign_keys = ON");
|
|
1765
|
-
await client.execute(`PRAGMA busy_timeout=${DEFAULT_BUSY_TIMEOUT_MS}`);
|
|
1766
|
-
if (trimmedPath !== ":memory:") {
|
|
1767
|
-
await client.execute("PRAGMA journal_mode=WAL");
|
|
1768
|
-
}
|
|
1769
|
-
return client;
|
|
1770
|
-
}
|
|
1771
|
-
function resolveClientUrl(dbPath) {
|
|
1772
|
-
if (dbPath === ":memory:") {
|
|
1773
|
-
return dbPath;
|
|
1774
|
-
}
|
|
1775
|
-
if (dbPath.startsWith("file:")) {
|
|
1776
|
-
return dbPath;
|
|
1777
|
-
}
|
|
1778
|
-
return `file:${path.resolve(dbPath)}`;
|
|
1779
|
-
}
|
|
1780
|
-
async function rollbackTransaction(transaction) {
|
|
1781
|
-
if (transaction.closed) {
|
|
1782
|
-
return;
|
|
1783
|
-
}
|
|
1784
|
-
await transaction.rollback();
|
|
1785
|
-
}
|
|
1786
|
-
|
|
1787
|
-
// src/config.ts
|
|
1788
|
-
import fs2 from "fs";
|
|
1789
|
-
import os from "os";
|
|
1790
|
-
import path2 from "path";
|
|
1791
|
-
import { fileURLToPath } from "url";
|
|
1792
|
-
|
|
1793
|
-
// src/core/types.ts
|
|
1794
|
-
var ENTRY_TYPES = ["fact", "decision", "preference", "lesson", "relationship", "milestone"];
|
|
1795
|
-
var EXPIRY_LEVELS = ["core", "permanent", "temporary"];
|
|
1796
|
-
var EPISODE_ACTIVITY_LEVELS = ["substantial", "minimal", "none"];
|
|
1797
|
-
|
|
1798
|
-
// src/config.ts
|
|
1799
|
-
var AUTH_METHOD_DEFINITIONS = [
|
|
1800
|
-
{
|
|
1801
|
-
id: "openai-api-key",
|
|
1802
|
-
provider: "openai",
|
|
1803
|
-
title: "OpenAI API key",
|
|
1804
|
-
setupDescription: "Standard OpenAI API key. Pay per token.",
|
|
1805
|
-
preferredModels: ["gpt-5.4-mini", "gpt-5.4", "gpt-5.4-nano"]
|
|
1806
|
-
},
|
|
1807
|
-
{
|
|
1808
|
-
id: "openai-subscription",
|
|
1809
|
-
provider: "openai-codex",
|
|
1810
|
-
title: "OpenAI - Subscription (via Codex CLI)",
|
|
1811
|
-
setupDescription: "Uses your Codex CLI login. No per-token cost. Requires `codex auth`.",
|
|
1812
|
-
preferredModels: ["gpt-5.4-mini", "gpt-5.4"]
|
|
1813
|
-
},
|
|
1814
|
-
{
|
|
1815
|
-
id: "anthropic-api-key",
|
|
1816
|
-
provider: "anthropic",
|
|
1817
|
-
title: "Anthropic API key",
|
|
1818
|
-
setupDescription: "Standard Anthropic API key. Pay per token.",
|
|
1819
|
-
preferredModels: ["claude-sonnet-4-6", "claude-opus-4-6", "claude-haiku-4-5"]
|
|
1820
|
-
},
|
|
1821
|
-
{
|
|
1822
|
-
id: "anthropic-oauth",
|
|
1823
|
-
provider: "anthropic",
|
|
1824
|
-
title: "Anthropic - Claude subscription (OAuth)",
|
|
1825
|
-
setupDescription: "Uses your Claude Code CLI login. No per-token cost.",
|
|
1826
|
-
preferredModels: ["claude-sonnet-4-6", "claude-opus-4-6", "claude-haiku-4-5"]
|
|
1827
|
-
},
|
|
1828
|
-
{
|
|
1829
|
-
id: "anthropic-token",
|
|
1830
|
-
provider: "anthropic",
|
|
1831
|
-
title: "Anthropic - Claude subscription (long-lived token)",
|
|
1832
|
-
setupDescription: "Uses a Claude long-lived token instead of an API key.",
|
|
1833
|
-
preferredModels: ["claude-sonnet-4-6", "claude-opus-4-6", "claude-haiku-4-5"]
|
|
1834
|
-
}
|
|
1835
|
-
];
|
|
1836
|
-
var AUTH_METHOD_SET = new Set(AUTH_METHOD_DEFINITIONS.map((definition) => definition.id));
|
|
1837
|
-
var DEFAULT_CONFIG_DIR = path2.join(os.homedir(), ".agenr");
|
|
1838
|
-
var DEFAULT_DB_NAME = "knowledge.db";
|
|
1839
|
-
var CONFIG_DIR_MODE = 448;
|
|
1840
|
-
var CONFIG_FILE_MODE = 384;
|
|
1841
|
-
var DEFAULT_SURGEON_COST_CAP = 15;
|
|
1842
|
-
var DEFAULT_SURGEON_DAILY_COST_CAP = 75;
|
|
1843
|
-
var DEFAULT_SURGEON_CONTEXT_LIMIT = 0;
|
|
1844
|
-
var DEFAULT_SURGEON_RETIREMENT_PROTECT_RECALLED_DAYS = 14;
|
|
1845
|
-
var DEFAULT_SURGEON_RETIREMENT_PROTECT_MIN_IMPORTANCE = 9;
|
|
1846
|
-
var DEFAULT_SURGEON_SKIP_RECENTLY_EVALUATED_DAYS = 7;
|
|
1847
|
-
var DEFAULT_CLAIM_EXTRACTION_CONCURRENCY = 10;
|
|
1848
|
-
var DEFAULT_CLAIM_EXTRACTION_CONFIDENCE_THRESHOLD = 0.8;
|
|
1849
|
-
var DEFAULT_CLAIM_EXTRACTION_ELIGIBLE_TYPES = ["fact", "preference", "decision", "lesson"];
|
|
1850
|
-
function isAgenrAuthMethod(value) {
|
|
1851
|
-
return AUTH_METHOD_SET.has(value);
|
|
1852
|
-
}
|
|
1853
|
-
function authMethodToProvider(auth) {
|
|
1854
|
-
const definition = AUTH_METHOD_DEFINITIONS.find((candidate) => candidate.id === auth);
|
|
1855
|
-
if (!definition) {
|
|
1856
|
-
throw new Error(`Unsupported auth method "${auth}".`);
|
|
1857
|
-
}
|
|
1858
|
-
return definition.provider;
|
|
1859
|
-
}
|
|
1860
|
-
function getAuthMethodDefinition(auth) {
|
|
1861
|
-
const definition = AUTH_METHOD_DEFINITIONS.find((candidate) => candidate.id === auth);
|
|
1862
|
-
if (!definition) {
|
|
1863
|
-
throw new Error(`Unsupported auth method "${auth}".`);
|
|
1864
|
-
}
|
|
1865
|
-
return definition;
|
|
1866
|
-
}
|
|
1867
|
-
function resolveConfigDir() {
|
|
1868
|
-
return process.env.AGENR_CONFIG_DIR ?? DEFAULT_CONFIG_DIR;
|
|
1869
|
-
}
|
|
1870
|
-
function resolveConfigPath(options = {}) {
|
|
1871
|
-
const envConfigPath = normalizeOptionalString3(process.env.AGENR_CONFIG_PATH);
|
|
1872
|
-
if (envConfigPath) {
|
|
1873
|
-
return envConfigPath;
|
|
1874
|
-
}
|
|
1875
|
-
const explicitConfigPath = normalizeOptionalString3(options.configPath);
|
|
1876
|
-
if (explicitConfigPath) {
|
|
1877
|
-
return explicitConfigPath;
|
|
1878
|
-
}
|
|
1879
|
-
const adjacentConfigPath = resolveAdjacentConfigPath(options.dbPath);
|
|
1880
|
-
if (adjacentConfigPath) {
|
|
1881
|
-
return adjacentConfigPath;
|
|
1882
|
-
}
|
|
1883
|
-
return path2.join(resolveConfigDir(), "config.json");
|
|
1884
|
-
}
|
|
1885
|
-
function resolveDbPath(config) {
|
|
1886
|
-
return process.env.AGENR_DB_PATH ?? config?.dbPath ?? path2.join(resolveConfigDir(), DEFAULT_DB_NAME);
|
|
1887
|
-
}
|
|
1888
|
-
function resolveClaimExtractionConfig(config) {
|
|
1889
|
-
return {
|
|
1890
|
-
enabled: config?.claimExtraction?.enabled ?? true,
|
|
1891
|
-
confidenceThreshold: normalizeClaimExtractionConfidence(config?.claimExtraction?.confidenceThreshold),
|
|
1892
|
-
eligibleTypes: normalizeClaimExtractionEligibleTypes(config?.claimExtraction?.eligibleTypes),
|
|
1893
|
-
concurrency: normalizeClaimExtractionConcurrency(config?.claimExtraction?.concurrency)
|
|
1894
|
-
};
|
|
1895
|
-
}
|
|
1896
|
-
function readConfig(options = {}) {
|
|
1897
|
-
const configPath = resolveFilesystemPath(resolveConfigPath(options));
|
|
1898
|
-
if (!fs2.existsSync(configPath)) {
|
|
1899
|
-
return {};
|
|
1900
|
-
}
|
|
1901
|
-
let parsed;
|
|
1902
|
-
try {
|
|
1903
|
-
const raw = fs2.readFileSync(configPath, "utf-8");
|
|
1904
|
-
parsed = JSON.parse(raw);
|
|
1905
|
-
} catch {
|
|
1906
|
-
return {};
|
|
1907
|
-
}
|
|
1908
|
-
if (!isRecord(parsed)) {
|
|
1909
|
-
return {};
|
|
1910
|
-
}
|
|
1911
|
-
assertSupportedConfig(parsed, configPath);
|
|
1912
|
-
return parsed;
|
|
1913
|
-
}
|
|
1914
|
-
function configFileExists(options = {}) {
|
|
1915
|
-
return fs2.existsSync(resolveFilesystemPath(resolveConfigPath(options)));
|
|
1916
|
-
}
|
|
1917
|
-
function writeConfig(config, options = {}) {
|
|
1918
|
-
const configPath = resolveFilesystemPath(resolveConfigPath(options));
|
|
1919
|
-
const configDir = path2.dirname(configPath);
|
|
1920
|
-
assertSupportedConfig(config, configPath);
|
|
1921
|
-
fs2.mkdirSync(configDir, { recursive: true, mode: CONFIG_DIR_MODE });
|
|
1922
|
-
try {
|
|
1923
|
-
fs2.chmodSync(configDir, CONFIG_DIR_MODE);
|
|
1924
|
-
} catch {
|
|
1925
|
-
}
|
|
1926
|
-
fs2.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
|
|
1927
|
-
`, {
|
|
1928
|
-
encoding: "utf-8",
|
|
1929
|
-
mode: CONFIG_FILE_MODE
|
|
1930
|
-
});
|
|
1931
|
-
try {
|
|
1932
|
-
fs2.chmodSync(configPath, CONFIG_FILE_MODE);
|
|
1933
|
-
} catch {
|
|
1934
|
-
}
|
|
1935
|
-
}
|
|
1936
|
-
function resolveAdjacentConfigPath(dbPath) {
|
|
1937
|
-
const normalizedDbPath = normalizeOptionalString3(dbPath);
|
|
1938
|
-
if (!normalizedDbPath || normalizedDbPath === ":memory:") {
|
|
1939
|
-
return void 0;
|
|
1940
|
-
}
|
|
1941
|
-
if (normalizedDbPath.startsWith("file:")) {
|
|
1942
|
-
try {
|
|
1943
|
-
return path2.join(path2.dirname(fileURLToPath(normalizedDbPath)), "config.json");
|
|
1944
|
-
} catch {
|
|
1945
|
-
return void 0;
|
|
1946
|
-
}
|
|
1947
|
-
}
|
|
1948
|
-
return path2.join(path2.dirname(normalizedDbPath), "config.json");
|
|
1949
|
-
}
|
|
1950
|
-
function normalizeOptionalString3(value) {
|
|
1951
|
-
const normalized = value?.trim();
|
|
1952
|
-
return normalized && normalized.length > 0 ? normalized : void 0;
|
|
1953
|
-
}
|
|
1954
|
-
function assertSupportedConfig(config, configPath) {
|
|
1955
|
-
const unsupportedFields = ["apiKey", "embeddingApiKey"].filter((field) => field in config);
|
|
1956
|
-
if (unsupportedFields.length === 0) {
|
|
1957
|
-
return;
|
|
1958
|
-
}
|
|
1959
|
-
throw new Error(
|
|
1960
|
-
`Unsupported agenr config field(s) in ${configPath}: ${unsupportedFields.join(", ")}. Move \`apiKey\` to \`credentials.openaiApiKey\` or \`credentials.anthropicApiKey\` depending on your configured auth, move \`embeddingApiKey\` to \`credentials.openaiApiKey\`, then remove the legacy fields.`
|
|
1961
|
-
);
|
|
1962
|
-
}
|
|
1963
|
-
function resolveFilesystemPath(targetPath) {
|
|
1964
|
-
if (!targetPath.startsWith("file:")) {
|
|
1965
|
-
return targetPath;
|
|
1966
|
-
}
|
|
1967
|
-
try {
|
|
1968
|
-
return fileURLToPath(targetPath);
|
|
1969
|
-
} catch {
|
|
1970
|
-
return targetPath;
|
|
1971
|
-
}
|
|
1972
|
-
}
|
|
1973
|
-
function isRecord(value) {
|
|
1974
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1975
|
-
}
|
|
1976
|
-
function normalizeClaimExtractionConfidence(value) {
|
|
1977
|
-
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
1978
|
-
return DEFAULT_CLAIM_EXTRACTION_CONFIDENCE_THRESHOLD;
|
|
1979
|
-
}
|
|
1980
|
-
return Math.min(1, Math.max(0, value));
|
|
1981
|
-
}
|
|
1982
|
-
function normalizeClaimExtractionEligibleTypes(value) {
|
|
1983
|
-
if (!Array.isArray(value) || value.length === 0) {
|
|
1984
|
-
return [...DEFAULT_CLAIM_EXTRACTION_ELIGIBLE_TYPES];
|
|
1985
|
-
}
|
|
1986
|
-
const normalized = Array.from(new Set(value.filter((candidate) => ENTRY_TYPES.includes(candidate))));
|
|
1987
|
-
return normalized.length > 0 ? normalized : [...DEFAULT_CLAIM_EXTRACTION_ELIGIBLE_TYPES];
|
|
1988
|
-
}
|
|
1989
|
-
function normalizeClaimExtractionConcurrency(value) {
|
|
1990
|
-
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
1991
|
-
return DEFAULT_CLAIM_EXTRACTION_CONCURRENCY;
|
|
1992
|
-
}
|
|
1993
|
-
const normalized = Math.trunc(value);
|
|
1994
|
-
return normalized > 0 ? normalized : DEFAULT_CLAIM_EXTRACTION_CONCURRENCY;
|
|
1995
|
-
}
|
|
1996
|
-
|
|
1997
|
-
// src/core/store/embedding-text.ts
|
|
1998
|
-
function composeEmbeddingText(entry) {
|
|
1999
|
-
return `${entry.type}: ${entry.subject} - ${entry.content}`;
|
|
2000
|
-
}
|
|
2001
|
-
|
|
2002
|
-
// src/adapters/embeddings.ts
|
|
2003
|
-
var OPENAI_EMBEDDINGS_URL = "https://api.openai.com/v1/embeddings";
|
|
2004
|
-
var EMBEDDING_MODEL = "text-embedding-3-small";
|
|
2005
|
-
var EMBEDDING_DIMENSIONS = 1024;
|
|
2006
|
-
var EMBEDDING_BATCH_SIZE = 200;
|
|
2007
|
-
var EMBEDDING_MAX_CONCURRENCY = 3;
|
|
2008
|
-
var EMBEDDING_MAX_RETRIES = 5;
|
|
2009
|
-
function createEmbeddingClient(apiKey, model = EMBEDDING_MODEL) {
|
|
2010
|
-
const normalizedApiKey = apiKey.trim();
|
|
2011
|
-
if (normalizedApiKey.length === 0) {
|
|
2012
|
-
throw new Error("Embedding API key must not be empty.");
|
|
2013
|
-
}
|
|
2014
|
-
const normalizedModel = model.trim().length > 0 ? model.trim() : EMBEDDING_MODEL;
|
|
2015
|
-
return {
|
|
2016
|
-
embed: async (texts) => embedTexts(texts, normalizedApiKey, normalizedModel)
|
|
2017
|
-
};
|
|
2018
|
-
}
|
|
2019
|
-
function resolveEmbeddingApiKey(config) {
|
|
2020
|
-
const candidates = [config?.credentials?.openaiApiKey, process.env.OPENAI_API_KEY];
|
|
2021
|
-
for (const candidate of candidates) {
|
|
2022
|
-
const normalized = candidate?.trim();
|
|
2023
|
-
if (normalized && normalized.length > 0) {
|
|
2024
|
-
return normalized;
|
|
2025
|
-
}
|
|
2026
|
-
}
|
|
2027
|
-
throw new Error("Embedding API key is required. Set config.credentials.openaiApiKey or OPENAI_API_KEY.");
|
|
2028
|
-
}
|
|
2029
|
-
function resolveEmbeddingModel(config) {
|
|
2030
|
-
const configuredModel = config?.embeddingModel?.trim();
|
|
2031
|
-
return configuredModel && configuredModel.length > 0 ? configuredModel : EMBEDDING_MODEL;
|
|
2032
|
-
}
|
|
2033
|
-
async function embedTexts(texts, apiKey, model) {
|
|
2034
|
-
if (texts.length === 0) {
|
|
2035
|
-
return [];
|
|
2036
|
-
}
|
|
2037
|
-
const chunks = chunkValues2(texts, EMBEDDING_BATCH_SIZE);
|
|
2038
|
-
const chunkResults = new Array(chunks.length);
|
|
2039
|
-
let nextChunkIndex = 0;
|
|
2040
|
-
await Promise.all(
|
|
2041
|
-
Array.from({ length: Math.min(EMBEDDING_MAX_CONCURRENCY, chunks.length) }, async () => {
|
|
2042
|
-
while (true) {
|
|
2043
|
-
const chunkIndex = nextChunkIndex;
|
|
2044
|
-
nextChunkIndex += 1;
|
|
2045
|
-
if (chunkIndex >= chunks.length) {
|
|
2046
|
-
return;
|
|
2047
|
-
}
|
|
2048
|
-
chunkResults[chunkIndex] = await requestEmbeddingBatch(chunks[chunkIndex], apiKey, model);
|
|
2049
|
-
}
|
|
2050
|
-
})
|
|
2051
|
-
);
|
|
2052
|
-
return chunkResults.flat();
|
|
2053
|
-
}
|
|
2054
|
-
async function requestEmbeddingBatch(texts, apiKey, model) {
|
|
2055
|
-
let lastError = null;
|
|
2056
|
-
for (let attempt = 1; attempt <= EMBEDDING_MAX_RETRIES; attempt += 1) {
|
|
2057
|
-
try {
|
|
2058
|
-
const response = await fetch(OPENAI_EMBEDDINGS_URL, {
|
|
2059
|
-
method: "POST",
|
|
2060
|
-
headers: {
|
|
2061
|
-
Authorization: `Bearer ${apiKey}`,
|
|
2062
|
-
"Content-Type": "application/json"
|
|
2063
|
-
},
|
|
2064
|
-
body: JSON.stringify({
|
|
2065
|
-
model,
|
|
2066
|
-
dimensions: EMBEDDING_DIMENSIONS,
|
|
2067
|
-
input: texts
|
|
2068
|
-
})
|
|
2069
|
-
});
|
|
2070
|
-
const rawBody = await response.text();
|
|
2071
|
-
if (!response.ok) {
|
|
2072
|
-
lastError = buildHttpError(response.status, rawBody);
|
|
2073
|
-
if (attempt < EMBEDDING_MAX_RETRIES && isRetryableStatus(response.status)) {
|
|
2074
|
-
await sleep(backoffMs(attempt));
|
|
2075
|
-
continue;
|
|
2076
|
-
}
|
|
2077
|
-
throw lastError;
|
|
2078
|
-
}
|
|
2079
|
-
return parseEmbeddingResponse(rawBody, texts.length);
|
|
2080
|
-
} catch (error) {
|
|
2081
|
-
lastError = normalizeFetchError(error);
|
|
2082
|
-
if (attempt < EMBEDDING_MAX_RETRIES && isRetryableError(lastError)) {
|
|
2083
|
-
await sleep(backoffMs(attempt));
|
|
2084
|
-
continue;
|
|
2085
|
-
}
|
|
2086
|
-
throw lastError;
|
|
2087
|
-
}
|
|
2088
|
-
}
|
|
2089
|
-
throw lastError ?? new Error("OpenAI embeddings request failed.");
|
|
2090
|
-
}
|
|
2091
|
-
function parseEmbeddingResponse(rawBody, expectedLength) {
|
|
2092
|
-
let parsed;
|
|
2093
|
-
try {
|
|
2094
|
-
parsed = JSON.parse(rawBody);
|
|
2095
|
-
} catch (error) {
|
|
2096
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2097
|
-
throw new Error(`OpenAI embeddings response was not valid JSON: ${message}`, {
|
|
2098
|
-
cause: error
|
|
2099
|
-
});
|
|
2100
|
-
}
|
|
2101
|
-
if (!Array.isArray(parsed.data)) {
|
|
2102
|
-
throw new Error("OpenAI embeddings response missing data array.");
|
|
2103
|
-
}
|
|
2104
|
-
const sorted = [...parsed.data].sort((left, right) => left.index - right.index);
|
|
2105
|
-
if (sorted.length !== expectedLength) {
|
|
2106
|
-
throw new Error(`OpenAI embeddings response length mismatch: expected ${expectedLength}, received ${sorted.length}.`);
|
|
2107
|
-
}
|
|
2108
|
-
return sorted.map((item) => {
|
|
2109
|
-
if (!Array.isArray(item.embedding)) {
|
|
2110
|
-
throw new Error("OpenAI embeddings response contained an item with no embedding array.");
|
|
2111
|
-
}
|
|
2112
|
-
if (!item.embedding.every((value) => typeof value === "number" && Number.isFinite(value))) {
|
|
2113
|
-
throw new Error("OpenAI embeddings response contained a non-numeric embedding value.");
|
|
2114
|
-
}
|
|
2115
|
-
return [...item.embedding];
|
|
2116
|
-
});
|
|
2117
|
-
}
|
|
2118
|
-
function normalizeFetchError(error) {
|
|
2119
|
-
if (error instanceof Error) {
|
|
2120
|
-
return error;
|
|
2121
|
-
}
|
|
2122
|
-
return new Error(String(error));
|
|
2123
|
-
}
|
|
2124
|
-
function isRetryableStatus(status) {
|
|
2125
|
-
return status === 429 || status === 500 || status === 502 || status === 503 || status === 504;
|
|
2126
|
-
}
|
|
2127
|
-
function isRetryableError(error) {
|
|
2128
|
-
const message = error.message.toLowerCase();
|
|
2129
|
-
return message.includes("429") || message.includes("500") || message.includes("502") || message.includes("503") || message.includes("504") || message.includes("timeout") || message.includes("network") || message.includes("connection") || message.includes("fetch failed");
|
|
2130
|
-
}
|
|
2131
|
-
function buildHttpError(status, rawBody) {
|
|
2132
|
-
const detail = getErrorSnippet(rawBody);
|
|
2133
|
-
if (status === 401) {
|
|
2134
|
-
return new Error(`OpenAI embeddings request failed (401): invalid API key. ${detail}`);
|
|
2135
|
-
}
|
|
2136
|
-
if (status === 429) {
|
|
2137
|
-
return new Error(`OpenAI embeddings request failed (429): rate limited. ${detail}`);
|
|
2138
|
-
}
|
|
2139
|
-
return new Error(`OpenAI embeddings request failed (${status}): ${detail}`);
|
|
2140
|
-
}
|
|
2141
|
-
function getErrorSnippet(rawBody) {
|
|
2142
|
-
const trimmed = rawBody.trim();
|
|
2143
|
-
if (trimmed.length === 0) {
|
|
2144
|
-
return "unknown error";
|
|
2145
|
-
}
|
|
2146
|
-
try {
|
|
2147
|
-
const parsed = JSON.parse(trimmed);
|
|
2148
|
-
const message = parsed.error?.message;
|
|
2149
|
-
if (typeof message === "string" && message.trim().length > 0) {
|
|
2150
|
-
return message.trim();
|
|
2151
|
-
}
|
|
2152
|
-
} catch {
|
|
2153
|
-
}
|
|
2154
|
-
const maxLength = 200;
|
|
2155
|
-
return trimmed.length <= maxLength ? trimmed : `${trimmed.slice(0, maxLength)}...`;
|
|
2156
|
-
}
|
|
2157
|
-
function chunkValues2(values, chunkSize) {
|
|
2158
|
-
const chunks = [];
|
|
2159
|
-
for (let index = 0; index < values.length; index += chunkSize) {
|
|
2160
|
-
chunks.push(values.slice(index, index + chunkSize));
|
|
2161
|
-
}
|
|
2162
|
-
return chunks;
|
|
2163
|
-
}
|
|
2164
|
-
function backoffMs(attempt) {
|
|
2165
|
-
return Math.min(2e3 * 2 ** (attempt - 1), 6e4);
|
|
2166
|
-
}
|
|
2167
|
-
async function sleep(durationMs) {
|
|
2168
|
-
await new Promise((resolve) => {
|
|
2169
|
-
setTimeout(resolve, durationMs);
|
|
2170
|
-
});
|
|
2171
|
-
}
|
|
2172
|
-
|
|
2173
|
-
// src/adapters/db/recall-adapter.ts
|
|
2174
|
-
var ENTRY_SELECT_COLUMNS = `
|
|
2175
|
-
e.id,
|
|
2176
|
-
e.type,
|
|
2177
|
-
e.subject,
|
|
2178
|
-
e.content,
|
|
2179
|
-
e.importance,
|
|
2180
|
-
e.expiry,
|
|
2181
|
-
e.tags,
|
|
2182
|
-
e.source_file,
|
|
2183
|
-
e.source_context,
|
|
2184
|
-
e.embedding,
|
|
2185
|
-
e.content_hash,
|
|
2186
|
-
e.norm_content_hash,
|
|
2187
|
-
e.quality_score,
|
|
2188
|
-
e.recall_count,
|
|
2189
|
-
e.last_recalled_at,
|
|
2190
|
-
e.superseded_by,
|
|
2191
|
-
e.valid_from,
|
|
2192
|
-
e.valid_to,
|
|
2193
|
-
e.claim_key,
|
|
2194
|
-
e.supersession_kind,
|
|
2195
|
-
e.supersession_reason,
|
|
2196
|
-
e.cluster_id,
|
|
2197
|
-
e.user_id,
|
|
2198
|
-
e.project,
|
|
2199
|
-
e.retired,
|
|
2200
|
-
e.retired_at,
|
|
2201
|
-
e.retired_reason,
|
|
2202
|
-
e.created_at,
|
|
2203
|
-
e.updated_at
|
|
2204
|
-
`;
|
|
2205
|
-
var RECALL_CANDIDATE_SELECT_COLUMNS = `
|
|
2206
|
-
e.id,
|
|
2207
|
-
e.subject,
|
|
2208
|
-
e.content,
|
|
2209
|
-
e.importance,
|
|
2210
|
-
e.expiry,
|
|
2211
|
-
e.embedding,
|
|
2212
|
-
e.superseded_by,
|
|
2213
|
-
e.claim_key,
|
|
2214
|
-
e.retired,
|
|
2215
|
-
e.created_at
|
|
2216
|
-
`;
|
|
2217
|
-
var FTS_TIERS = ["exact", "all_tokens", "any_tokens"];
|
|
2218
|
-
var PREDECESSOR_EXPANSION_LIMIT_PER_SEED = 8;
|
|
2219
|
-
var PREDECESSOR_EXPANSION_MAX_RESULTS = 40;
|
|
2220
|
-
function createRecallAdapter(executor, embeddingPort) {
|
|
2221
|
-
return new LibsqlRecallAdapter(executor, embeddingPort);
|
|
2222
|
-
}
|
|
2223
|
-
var LibsqlRecallAdapter = class {
|
|
2224
|
-
/**
|
|
2225
|
-
* Creates a recall adapter over a SQL executor and embedding provider.
|
|
2226
|
-
*
|
|
2227
|
-
* @param executor - SQL executor used for reads and telemetry writes.
|
|
2228
|
-
* @param embeddingPort - Embedding provider used for query embeddings.
|
|
2229
|
-
*/
|
|
2230
|
-
constructor(executor, embeddingPort) {
|
|
2231
|
-
this.executor = executor;
|
|
2232
|
-
this.embeddingPort = embeddingPort;
|
|
2233
|
-
}
|
|
2234
|
-
pendingWrites = Promise.resolve();
|
|
2235
|
-
/** Computes the query embedding by reusing the shared embedding port. */
|
|
2236
|
-
async embed(text) {
|
|
2237
|
-
const results = await this.embeddingPort.embed([text]);
|
|
2238
|
-
return results[0] ?? [];
|
|
2239
|
-
}
|
|
2240
|
-
/** Finds ranking-time vector candidates with SQL-level filters applied. */
|
|
2241
|
-
async vectorSearch(params) {
|
|
2242
|
-
if (params.limit <= 0 || params.embedding.length === 0) {
|
|
2243
|
-
return [];
|
|
2244
|
-
}
|
|
2245
|
-
const serializedEmbedding = serializeEmbeddingForVector(params.embedding);
|
|
2246
|
-
if (!serializedEmbedding) {
|
|
2247
|
-
return [];
|
|
2248
|
-
}
|
|
2249
|
-
const filters = buildEntryFilterClause(params.filters, "e");
|
|
2250
|
-
let result;
|
|
2251
|
-
try {
|
|
2252
|
-
result = await this.executor.execute({
|
|
2253
|
-
sql: `
|
|
2254
|
-
SELECT
|
|
2255
|
-
${RECALL_CANDIDATE_SELECT_COLUMNS}
|
|
2256
|
-
FROM vector_top_k('idx_entries_embedding', vector32(?), ?) AS v
|
|
2257
|
-
JOIN entries AS e ON e.rowid = v.id
|
|
2258
|
-
WHERE ${buildActiveEntryClause("e")}
|
|
2259
|
-
${filters.sql}
|
|
2260
|
-
LIMIT ?
|
|
2261
|
-
`,
|
|
2262
|
-
args: [serializedEmbedding, params.limit, ...filters.args, params.limit]
|
|
2263
|
-
});
|
|
2264
|
-
} catch (error) {
|
|
2265
|
-
throw wrapVectorError(error);
|
|
2266
|
-
}
|
|
2267
|
-
return result.rows.map((row) => ({
|
|
2268
|
-
entry: mapRecallCandidateRow(row),
|
|
2269
|
-
vectorSim: cosineSimilarity(params.embedding, readEmbedding(row, "embedding"))
|
|
2270
|
-
})).filter((candidate) => candidate.vectorSim > 0).sort((left, right) => right.vectorSim - left.vectorSim).slice(0, params.limit);
|
|
2271
|
-
}
|
|
2272
|
-
/** Finds ranking-time FTS candidates using the exact -> AND -> OR cascade. */
|
|
2273
|
-
async ftsSearch(params) {
|
|
2274
|
-
if (params.limit <= 0) {
|
|
2275
|
-
return [];
|
|
2276
|
-
}
|
|
2277
|
-
const plan = buildLexicalPlan(params.text);
|
|
2278
|
-
if (plan.length === 0) {
|
|
2279
|
-
return [];
|
|
2280
|
-
}
|
|
2281
|
-
const filters = buildEntryFilterClause(params.filters, "e");
|
|
2282
|
-
const matches = /* @__PURE__ */ new Map();
|
|
2283
|
-
for (const tier of plan) {
|
|
2284
|
-
const query = compileLexicalTier(tier);
|
|
2285
|
-
let result;
|
|
2286
|
-
try {
|
|
2287
|
-
result = await this.executor.execute({
|
|
2288
|
-
sql: `
|
|
2289
|
-
SELECT
|
|
2290
|
-
${RECALL_CANDIDATE_SELECT_COLUMNS},
|
|
2291
|
-
bm25(entries_fts, 1.0, 2.0) AS rank
|
|
2292
|
-
FROM entries_fts
|
|
2293
|
-
JOIN entries AS e ON e.rowid = entries_fts.rowid
|
|
2294
|
-
WHERE entries_fts MATCH ?
|
|
2295
|
-
AND ${buildActiveEntryClause("e")}
|
|
2296
|
-
${filters.sql}
|
|
2297
|
-
ORDER BY bm25(entries_fts, 1.0, 2.0)
|
|
2298
|
-
LIMIT ?
|
|
2299
|
-
`,
|
|
2300
|
-
args: [query, ...filters.args, params.limit]
|
|
2301
|
-
});
|
|
2302
|
-
} catch {
|
|
2303
|
-
continue;
|
|
2304
|
-
}
|
|
2305
|
-
for (const row of result.rows) {
|
|
2306
|
-
const entryId = readRequiredString(row, "id");
|
|
2307
|
-
if (matches.has(entryId)) {
|
|
2308
|
-
continue;
|
|
2309
|
-
}
|
|
2310
|
-
matches.set(entryId, {
|
|
2311
|
-
entry: mapRecallCandidateRow(row),
|
|
2312
|
-
rank: readNumber(row, "rank", Number.POSITIVE_INFINITY),
|
|
2313
|
-
tier: tier.tier
|
|
2314
|
-
});
|
|
2315
|
-
}
|
|
2316
|
-
}
|
|
2317
|
-
return Array.from(matches.values()).sort((left, right) => compareFtsCandidates(left, right)).slice(0, params.limit);
|
|
2318
|
-
}
|
|
2319
|
-
/**
|
|
2320
|
-
* Finds historical predecessors scoped to a seed set of active candidate IDs.
|
|
2321
|
-
*
|
|
2322
|
-
* Direct supersession links are preferred. Same-claim-key siblings are used as
|
|
2323
|
-
* the structural lineage path, with retired same-subject entries preserved as
|
|
2324
|
-
* a weaker fallback when explicit slot identity is unavailable.
|
|
2325
|
-
*/
|
|
2326
|
-
async fetchPredecessors(params) {
|
|
2327
|
-
const normalizedIds = normalizeStrings(params.activeEntryIds);
|
|
2328
|
-
if (normalizedIds.length === 0) {
|
|
2329
|
-
return [];
|
|
2330
|
-
}
|
|
2331
|
-
const placeholders = normalizedIds.map(() => "?").join(", ");
|
|
2332
|
-
const expansionLimit = normalizePredecessorExpansionLimit(normalizedIds.length);
|
|
2333
|
-
const result = await this.executor.execute({
|
|
2334
|
-
sql: `
|
|
2335
|
-
WITH seed AS (
|
|
2336
|
-
SELECT id, subject, claim_key
|
|
2337
|
-
FROM entries
|
|
2338
|
-
WHERE id IN (${placeholders})
|
|
2339
|
-
),
|
|
2340
|
-
seed_subjects AS (
|
|
2341
|
-
SELECT DISTINCT subject
|
|
2342
|
-
FROM seed
|
|
2343
|
-
WHERE TRIM(subject) <> ''
|
|
2344
|
-
),
|
|
2345
|
-
seed_claim_keys AS (
|
|
2346
|
-
SELECT DISTINCT claim_key
|
|
2347
|
-
FROM seed
|
|
2348
|
-
WHERE claim_key IS NOT NULL
|
|
2349
|
-
),
|
|
2350
|
-
lineage AS (
|
|
2351
|
-
SELECT
|
|
2352
|
-
${RECALL_CANDIDATE_SELECT_COLUMNS},
|
|
2353
|
-
CASE
|
|
2354
|
-
WHEN e.superseded_by IN (SELECT id FROM seed) THEN 0
|
|
2355
|
-
WHEN e.claim_key IS NOT NULL
|
|
2356
|
-
AND e.claim_key IN (SELECT claim_key FROM seed_claim_keys)
|
|
2357
|
-
AND (e.retired = 1 OR e.superseded_by IS NOT NULL) THEN 1
|
|
2358
|
-
WHEN e.claim_key IS NOT NULL
|
|
2359
|
-
AND e.claim_key IN (SELECT claim_key FROM seed_claim_keys) THEN 2
|
|
2360
|
-
WHEN e.retired = 1
|
|
2361
|
-
AND e.subject IN (SELECT subject FROM seed_subjects) THEN 3
|
|
2362
|
-
ELSE 4
|
|
2363
|
-
END AS lineage_priority
|
|
2364
|
-
FROM entries AS e
|
|
2365
|
-
WHERE e.id NOT IN (SELECT id FROM seed)
|
|
2366
|
-
AND (
|
|
2367
|
-
e.superseded_by IN (SELECT id FROM seed)
|
|
2368
|
-
OR (
|
|
2369
|
-
e.claim_key IS NOT NULL
|
|
2370
|
-
AND e.claim_key IN (SELECT claim_key FROM seed_claim_keys)
|
|
2371
|
-
)
|
|
2372
|
-
OR (
|
|
2373
|
-
e.retired = 1
|
|
2374
|
-
AND e.subject IN (SELECT subject FROM seed_subjects)
|
|
2375
|
-
)
|
|
2376
|
-
)
|
|
2377
|
-
)
|
|
2378
|
-
SELECT *
|
|
2379
|
-
FROM lineage
|
|
2380
|
-
ORDER BY lineage_priority ASC, created_at ASC, id ASC
|
|
2381
|
-
LIMIT ?
|
|
2382
|
-
`,
|
|
2383
|
-
args: [...normalizedIds, expansionLimit]
|
|
2384
|
-
});
|
|
2385
|
-
return result.rows.map((row) => mapRecallCandidateRow(row));
|
|
2386
|
-
}
|
|
2387
|
-
/** Hydrates full entries for the final ranked result set. */
|
|
2388
|
-
async hydrateEntries(ids) {
|
|
2389
|
-
const normalizedIds = normalizeStrings(ids);
|
|
2390
|
-
if (normalizedIds.length === 0) {
|
|
2391
|
-
return [];
|
|
2392
|
-
}
|
|
2393
|
-
const placeholders = normalizedIds.map(() => "?").join(", ");
|
|
2394
|
-
const result = await this.executor.execute({
|
|
2395
|
-
sql: `
|
|
2396
|
-
SELECT
|
|
2397
|
-
${ENTRY_SELECT_COLUMNS}
|
|
2398
|
-
FROM entries AS e
|
|
2399
|
-
WHERE e.id IN (${placeholders})
|
|
2400
|
-
`,
|
|
2401
|
-
args: normalizedIds
|
|
2402
|
-
});
|
|
2403
|
-
return result.rows.map((row) => mapEntryRow(row));
|
|
2404
|
-
}
|
|
2405
|
-
/**
|
|
2406
|
-
* Queues telemetry writes internally while keeping recall callers synchronous.
|
|
2407
|
-
*
|
|
2408
|
-
* Errors are swallowed per entry because telemetry must never fail recall.
|
|
2409
|
-
*/
|
|
2410
|
-
async recordRecallEvents(params) {
|
|
2411
|
-
const task = this.pendingWrites.then(async () => {
|
|
2412
|
-
for (const entryId of params.entryIds) {
|
|
2413
|
-
try {
|
|
2414
|
-
await recordRecallEvent(this.executor, entryId, params.query, params.sessionKey);
|
|
2415
|
-
} catch {
|
|
2416
|
-
}
|
|
2417
|
-
}
|
|
2418
|
-
});
|
|
2419
|
-
this.pendingWrites = task.catch(() => void 0);
|
|
2420
|
-
await this.pendingWrites;
|
|
2421
|
-
}
|
|
2422
|
-
};
|
|
2423
|
-
function buildEntryFilterClause(filters, alias) {
|
|
2424
|
-
if (!filters) {
|
|
2425
|
-
return { sql: "", args: [] };
|
|
2426
|
-
}
|
|
2427
|
-
const clauses = [];
|
|
2428
|
-
const args = [];
|
|
2429
|
-
const types = normalizeStrings(filters.types);
|
|
2430
|
-
if (types.length > 0) {
|
|
2431
|
-
clauses.push(`${alias}.type IN (${types.map(() => "?").join(", ")})`);
|
|
2432
|
-
args.push(...types);
|
|
2433
|
-
}
|
|
2434
|
-
const tags = normalizeStrings(filters.tags);
|
|
2435
|
-
for (const tag of tags) {
|
|
2436
|
-
clauses.push(`EXISTS (
|
|
2437
|
-
SELECT 1
|
|
2438
|
-
FROM json_each(CASE WHEN json_valid(${alias}.tags) THEN ${alias}.tags ELSE '[]' END)
|
|
2439
|
-
WHERE json_each.value = ?
|
|
2440
|
-
)`);
|
|
2441
|
-
args.push(tag);
|
|
2442
|
-
}
|
|
2443
|
-
if (filters.since) {
|
|
2444
|
-
clauses.push(`${alias}.created_at >= ?`);
|
|
2445
|
-
args.push(filters.since.toISOString());
|
|
2446
|
-
}
|
|
2447
|
-
if (filters.until) {
|
|
2448
|
-
clauses.push(`${alias}.created_at <= ?`);
|
|
2449
|
-
args.push(filters.until.toISOString());
|
|
2450
|
-
}
|
|
2451
|
-
if (clauses.length === 0) {
|
|
2452
|
-
return { sql: "", args };
|
|
2453
|
-
}
|
|
2454
|
-
return {
|
|
2455
|
-
sql: `AND ${clauses.join("\n AND ")}`,
|
|
2456
|
-
args
|
|
2457
|
-
};
|
|
2458
|
-
}
|
|
2459
|
-
function normalizeStrings(values) {
|
|
2460
|
-
if (!values) {
|
|
2461
|
-
return [];
|
|
2462
|
-
}
|
|
2463
|
-
return Array.from(new Set(values.map((value) => value.trim()).filter((value) => value.length > 0)));
|
|
2464
|
-
}
|
|
2465
|
-
function compareFtsCandidates(left, right) {
|
|
2466
|
-
const tierDelta = ftsTierPriority(left.tier) - ftsTierPriority(right.tier);
|
|
2467
|
-
if (tierDelta !== 0) {
|
|
2468
|
-
return tierDelta;
|
|
2469
|
-
}
|
|
2470
|
-
return left.rank - right.rank;
|
|
2471
|
-
}
|
|
2472
|
-
function ftsTierPriority(tier) {
|
|
2473
|
-
return FTS_TIERS.indexOf(tier);
|
|
2474
|
-
}
|
|
2475
|
-
function compileLexicalTier(tier) {
|
|
2476
|
-
if (tier.tier === "exact") {
|
|
2477
|
-
return `"${tier.text.replaceAll('"', '""')}"`;
|
|
2478
|
-
}
|
|
2479
|
-
return tier.tier === "all_tokens" ? tier.tokens.join(" ") : tier.tokens.join(" OR ");
|
|
2480
|
-
}
|
|
2481
|
-
function mapRecallCandidateRow(row) {
|
|
2482
|
-
const expiry = readRequiredString(row, "expiry");
|
|
2483
|
-
return {
|
|
2484
|
-
id: readRequiredString(row, "id"),
|
|
2485
|
-
subject: readRequiredString(row, "subject"),
|
|
2486
|
-
content: readRequiredString(row, "content"),
|
|
2487
|
-
importance: readNumber(row, "importance", 0),
|
|
2488
|
-
expiry,
|
|
2489
|
-
embedding: readEmbedding(row, "embedding"),
|
|
2490
|
-
superseded_by: readOptionalString(row, "superseded_by"),
|
|
2491
|
-
claim_key: readOptionalString(row, "claim_key"),
|
|
2492
|
-
retired: readBoolean(row, "retired"),
|
|
2493
|
-
created_at: readRequiredString(row, "created_at")
|
|
2494
|
-
};
|
|
2495
|
-
}
|
|
2496
|
-
function normalizePredecessorExpansionLimit(seedCount) {
|
|
2497
|
-
return Math.min(PREDECESSOR_EXPANSION_MAX_RESULTS, seedCount * PREDECESSOR_EXPANSION_LIMIT_PER_SEED);
|
|
2498
|
-
}
|
|
2499
|
-
function wrapVectorError(error) {
|
|
2500
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2501
|
-
return new Error(`Vector search is unavailable: ${message}`);
|
|
2502
|
-
}
|
|
2503
|
-
|
|
2504
|
-
export {
|
|
2505
|
-
buildActiveEntryClause,
|
|
2506
|
-
deserializeTags,
|
|
2507
|
-
readRequiredString,
|
|
2508
|
-
readOptionalString,
|
|
2509
|
-
readNumber,
|
|
2510
|
-
readBoolean,
|
|
2511
|
-
mapEntryRow,
|
|
2512
|
-
getEntry,
|
|
2513
|
-
retireEntry,
|
|
2514
|
-
supersedeEntry,
|
|
2515
|
-
updateEntry,
|
|
2516
|
-
VECTOR_INDEX_NAME,
|
|
2517
|
-
getLastBulkIngestAt,
|
|
2518
|
-
createDatabase,
|
|
2519
|
-
ENTRY_TYPES,
|
|
2520
|
-
EXPIRY_LEVELS,
|
|
2521
|
-
EPISODE_ACTIVITY_LEVELS,
|
|
2522
|
-
DEFAULT_SURGEON_COST_CAP,
|
|
2523
|
-
DEFAULT_SURGEON_DAILY_COST_CAP,
|
|
2524
|
-
DEFAULT_SURGEON_CONTEXT_LIMIT,
|
|
2525
|
-
DEFAULT_SURGEON_RETIREMENT_PROTECT_RECALLED_DAYS,
|
|
2526
|
-
DEFAULT_SURGEON_RETIREMENT_PROTECT_MIN_IMPORTANCE,
|
|
2527
|
-
DEFAULT_SURGEON_SKIP_RECENTLY_EVALUATED_DAYS,
|
|
2528
|
-
DEFAULT_CLAIM_EXTRACTION_CONCURRENCY,
|
|
2529
|
-
isAgenrAuthMethod,
|
|
2530
|
-
authMethodToProvider,
|
|
2531
|
-
getAuthMethodDefinition,
|
|
2532
|
-
resolveConfigPath,
|
|
2533
|
-
resolveDbPath,
|
|
2534
|
-
resolveClaimExtractionConfig,
|
|
2535
|
-
readConfig,
|
|
2536
|
-
configFileExists,
|
|
2537
|
-
writeConfig,
|
|
2538
|
-
composeEmbeddingText,
|
|
2539
|
-
EMBEDDING_MODEL,
|
|
2540
|
-
EMBEDDING_DIMENSIONS,
|
|
2541
|
-
createEmbeddingClient,
|
|
2542
|
-
resolveEmbeddingApiKey,
|
|
2543
|
-
resolveEmbeddingModel,
|
|
2544
|
-
createRecallAdapter
|
|
2545
|
-
};
|