agenr 0.9.21 → 0.9.23
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/CHANGELOG.md +20 -0
- package/dist/chunk-56GW7EIS.js +2821 -0
- package/dist/chunk-DVNOWSKK.js +2820 -0
- package/dist/chunk-RM5SLLEM.js +241 -0
- package/dist/cli-main.js +52 -4
- package/dist/co-recall-2UNFBE7S.js +16 -0
- package/dist/openclaw-plugin/index.js +42 -359
- package/package.json +1 -1
|
@@ -0,0 +1,2821 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getCoRecallEdgeCounts,
|
|
3
|
+
parseDaysBetween,
|
|
4
|
+
toNumber,
|
|
5
|
+
toRowsAffected,
|
|
6
|
+
toStringValue
|
|
7
|
+
} from "./chunk-RM5SLLEM.js";
|
|
8
|
+
|
|
9
|
+
// src/config.ts
|
|
10
|
+
import fs from "fs";
|
|
11
|
+
import os from "os";
|
|
12
|
+
import path from "path";
|
|
13
|
+
|
|
14
|
+
// src/llm/models.ts
|
|
15
|
+
import { getModel } from "@mariozechner/pi-ai";
|
|
16
|
+
var PROVIDERS = /* @__PURE__ */ new Set(["anthropic", "openai", "openai-codex"]);
|
|
17
|
+
var MODEL_ALIASES = {
|
|
18
|
+
anthropic: {
|
|
19
|
+
opus: "claude-opus-4-6",
|
|
20
|
+
sonnet: "claude-sonnet-4-20250514",
|
|
21
|
+
"claude-opus": "claude-opus-4-6"
|
|
22
|
+
},
|
|
23
|
+
openai: {
|
|
24
|
+
gpt: "gpt-5.2-codex",
|
|
25
|
+
"gpt-codex": "gpt-5.2-codex",
|
|
26
|
+
"gpt-4.1-nano": "openai/gpt-4.1-nano",
|
|
27
|
+
"gpt-4.1-mini": "openai/gpt-4.1-mini",
|
|
28
|
+
"gpt-4.1": "openai/gpt-4.1",
|
|
29
|
+
"gpt-5-nano": "openai/gpt-5-nano"
|
|
30
|
+
},
|
|
31
|
+
"openai-codex": {
|
|
32
|
+
codex: "gpt-5.3-codex",
|
|
33
|
+
"gpt-codex": "gpt-5.3-codex"
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
function isAgenrProvider(value) {
|
|
37
|
+
return PROVIDERS.has(value);
|
|
38
|
+
}
|
|
39
|
+
function normalizeProvider(value) {
|
|
40
|
+
const normalized = value.trim().toLowerCase();
|
|
41
|
+
if (!isAgenrProvider(normalized)) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`Unsupported provider "${value}". Expected one of: anthropic, openai, openai-codex.`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
return normalized;
|
|
47
|
+
}
|
|
48
|
+
function normalizeModelId(provider, model) {
|
|
49
|
+
const trimmed = model.trim();
|
|
50
|
+
if (!trimmed) {
|
|
51
|
+
throw new Error("Model cannot be empty.");
|
|
52
|
+
}
|
|
53
|
+
const aliasKey = trimmed.toLowerCase();
|
|
54
|
+
return MODEL_ALIASES[provider][aliasKey] ?? trimmed;
|
|
55
|
+
}
|
|
56
|
+
function resolveModel(providerRaw, modelRaw) {
|
|
57
|
+
const provider = normalizeProvider(providerRaw);
|
|
58
|
+
const modelId = normalizeModelId(provider, modelRaw);
|
|
59
|
+
const fallbackModelId = modelId.startsWith(`${provider}/`) ? modelId.slice(provider.length + 1) : void 0;
|
|
60
|
+
const candidateModelIds = [modelId, fallbackModelId].filter((value) => Boolean(value));
|
|
61
|
+
let model;
|
|
62
|
+
for (const candidateId of candidateModelIds) {
|
|
63
|
+
try {
|
|
64
|
+
const candidate = getModel(provider, candidateId);
|
|
65
|
+
if (candidate && typeof candidate.id === "string" && candidate.id.trim()) {
|
|
66
|
+
model = candidate;
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
} catch {
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (!model) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
`Model "${modelId}" is not available for provider "${provider}" in @mariozechner/pi-ai.`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
provider,
|
|
79
|
+
modelId,
|
|
80
|
+
model
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// src/utils/string.ts
|
|
85
|
+
function normalizeLabel(value) {
|
|
86
|
+
return value.trim().toLowerCase().replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// src/config.ts
|
|
90
|
+
var AUTH_METHOD_DEFINITIONS = [
|
|
91
|
+
{
|
|
92
|
+
id: "anthropic-oauth",
|
|
93
|
+
provider: "anthropic",
|
|
94
|
+
title: "Anthropic -- Claude subscription (OAuth)",
|
|
95
|
+
setupDescription: "Uses your Claude Pro/Team subscription via Claude CLI credentials. No per-token cost. Requires Claude Code CLI.",
|
|
96
|
+
preferredModels: ["claude-opus-4-6", "claude-sonnet-4-20250514", "claude-haiku-3-5-20241022"]
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: "anthropic-token",
|
|
100
|
+
provider: "anthropic",
|
|
101
|
+
title: "Anthropic -- Claude subscription (long-lived token)",
|
|
102
|
+
setupDescription: "Uses a long-lived token from `claude setup-token`. No per-token cost. Simpler setup.",
|
|
103
|
+
preferredModels: ["claude-opus-4-6", "claude-sonnet-4-20250514", "claude-haiku-3-5-20241022"]
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: "anthropic-api-key",
|
|
107
|
+
provider: "anthropic",
|
|
108
|
+
title: "Anthropic -- API key",
|
|
109
|
+
setupDescription: "Standard API key from console.anthropic.com. Pay per token.",
|
|
110
|
+
preferredModels: ["claude-sonnet-4-20250514", "claude-opus-4-6", "claude-haiku-3-5-20241022"]
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
id: "openai-subscription",
|
|
114
|
+
provider: "openai-codex",
|
|
115
|
+
title: "OpenAI -- Subscription (via Codex CLI)",
|
|
116
|
+
setupDescription: "Uses your ChatGPT Plus subscription via Codex CLI credentials. No per-token cost. Requires Codex CLI.",
|
|
117
|
+
preferredModels: ["gpt-5.3-codex", "o3-codex"]
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
id: "openai-api-key",
|
|
121
|
+
provider: "openai",
|
|
122
|
+
title: "OpenAI -- API key",
|
|
123
|
+
setupDescription: "Standard API key from https://platform.openai.com/api-keys. Pay per token.",
|
|
124
|
+
preferredModels: ["gpt-4.1-nano", "gpt-4.1-mini", "gpt-5-nano"]
|
|
125
|
+
}
|
|
126
|
+
];
|
|
127
|
+
var AUTH_METHOD_SET = new Set(AUTH_METHOD_DEFINITIONS.map((item) => item.id));
|
|
128
|
+
var PROVIDER_SET = /* @__PURE__ */ new Set(["anthropic", "openai", "openai-codex"]);
|
|
129
|
+
var MODEL_TASK_KEYS = ["extraction", "claimExtraction", "contradictionJudge", "handoffSummary"];
|
|
130
|
+
var CONFIG_FILE_MODE = 384;
|
|
131
|
+
var CONFIG_DIR_MODE = 448;
|
|
132
|
+
var DEFAULT_EMBEDDING_PROVIDER = "openai";
|
|
133
|
+
var DEFAULT_EMBEDDING_MODEL = "text-embedding-3-small";
|
|
134
|
+
var DEFAULT_EMBEDDING_DIMENSIONS = 1024;
|
|
135
|
+
var DEFAULT_FORGETTING_SCORE_THRESHOLD = 0.05;
|
|
136
|
+
var DEFAULT_FORGETTING_MAX_AGE_DAYS = 60;
|
|
137
|
+
var DEFAULT_CONTRADICTION_ENABLED = true;
|
|
138
|
+
var DEFAULT_TASK_MODEL = "gpt-4.1-nano";
|
|
139
|
+
var DEFAULT_AUTO_SUPERSEDE_CONFIDENCE = 0.85;
|
|
140
|
+
function isModelTask(value) {
|
|
141
|
+
return MODEL_TASK_KEYS.includes(value);
|
|
142
|
+
}
|
|
143
|
+
function resolveUserPath(inputPath) {
|
|
144
|
+
if (!inputPath.startsWith("~")) {
|
|
145
|
+
return inputPath;
|
|
146
|
+
}
|
|
147
|
+
return path.join(os.homedir(), inputPath.slice(1));
|
|
148
|
+
}
|
|
149
|
+
function resolveDefaultKnowledgeDbPath() {
|
|
150
|
+
return path.join(os.homedir(), ".agenr", "knowledge.db");
|
|
151
|
+
}
|
|
152
|
+
function resolveConfigPath(env = process.env) {
|
|
153
|
+
const explicit = env.AGENR_CONFIG_PATH?.trim();
|
|
154
|
+
if (explicit) {
|
|
155
|
+
return resolveUserPath(explicit);
|
|
156
|
+
}
|
|
157
|
+
return path.join(os.homedir(), ".agenr", "config.json");
|
|
158
|
+
}
|
|
159
|
+
function resolveConfigDir(env = process.env) {
|
|
160
|
+
return path.dirname(resolveConfigPath(env));
|
|
161
|
+
}
|
|
162
|
+
function isAgenrAuthMethod(value) {
|
|
163
|
+
return AUTH_METHOD_SET.has(value);
|
|
164
|
+
}
|
|
165
|
+
function isAgenrProvider2(value) {
|
|
166
|
+
return PROVIDER_SET.has(value);
|
|
167
|
+
}
|
|
168
|
+
function authMethodToProvider(auth) {
|
|
169
|
+
return AUTH_METHOD_DEFINITIONS.find((item) => item.id === auth)?.provider ?? "anthropic";
|
|
170
|
+
}
|
|
171
|
+
function getAuthMethodDefinition(auth) {
|
|
172
|
+
const found = AUTH_METHOD_DEFINITIONS.find((item) => item.id === auth);
|
|
173
|
+
if (!found) {
|
|
174
|
+
throw new Error(`Unsupported auth method "${auth}".`);
|
|
175
|
+
}
|
|
176
|
+
return found;
|
|
177
|
+
}
|
|
178
|
+
function normalizeStoredCredentials(input) {
|
|
179
|
+
if (!input || typeof input !== "object") {
|
|
180
|
+
return void 0;
|
|
181
|
+
}
|
|
182
|
+
const record = input;
|
|
183
|
+
const normalized = {};
|
|
184
|
+
if (typeof record.anthropicApiKey === "string" && record.anthropicApiKey.trim()) {
|
|
185
|
+
normalized.anthropicApiKey = record.anthropicApiKey.trim();
|
|
186
|
+
}
|
|
187
|
+
if (typeof record.anthropicOauthToken === "string" && record.anthropicOauthToken.trim()) {
|
|
188
|
+
normalized.anthropicOauthToken = record.anthropicOauthToken.trim();
|
|
189
|
+
}
|
|
190
|
+
if (typeof record.openaiApiKey === "string" && record.openaiApiKey.trim()) {
|
|
191
|
+
normalized.openaiApiKey = record.openaiApiKey.trim();
|
|
192
|
+
}
|
|
193
|
+
return Object.keys(normalized).length > 0 ? normalized : void 0;
|
|
194
|
+
}
|
|
195
|
+
function normalizeEmbeddingConfig(input) {
|
|
196
|
+
const normalized = {
|
|
197
|
+
provider: DEFAULT_EMBEDDING_PROVIDER,
|
|
198
|
+
model: DEFAULT_EMBEDDING_MODEL,
|
|
199
|
+
dimensions: DEFAULT_EMBEDDING_DIMENSIONS
|
|
200
|
+
};
|
|
201
|
+
if (!input || typeof input !== "object") {
|
|
202
|
+
return normalized;
|
|
203
|
+
}
|
|
204
|
+
const record = input;
|
|
205
|
+
if (record.provider === "openai") {
|
|
206
|
+
normalized.provider = record.provider;
|
|
207
|
+
}
|
|
208
|
+
if (typeof record.model === "string" && record.model.trim()) {
|
|
209
|
+
normalized.model = record.model.trim();
|
|
210
|
+
}
|
|
211
|
+
if (typeof record.dimensions === "number" && Number.isFinite(record.dimensions) && record.dimensions > 0) {
|
|
212
|
+
normalized.dimensions = Math.floor(record.dimensions);
|
|
213
|
+
}
|
|
214
|
+
if (typeof record.apiKey === "string" && record.apiKey.trim()) {
|
|
215
|
+
normalized.apiKey = record.apiKey.trim();
|
|
216
|
+
}
|
|
217
|
+
return normalized;
|
|
218
|
+
}
|
|
219
|
+
function normalizeDbConfig(input) {
|
|
220
|
+
const normalized = {
|
|
221
|
+
path: resolveDefaultKnowledgeDbPath()
|
|
222
|
+
};
|
|
223
|
+
if (!input || typeof input !== "object") {
|
|
224
|
+
return normalized;
|
|
225
|
+
}
|
|
226
|
+
const record = input;
|
|
227
|
+
if (typeof record.path === "string" && record.path.trim()) {
|
|
228
|
+
normalized.path = record.path.trim();
|
|
229
|
+
}
|
|
230
|
+
return normalized;
|
|
231
|
+
}
|
|
232
|
+
function normalizeForgettingConfig(input) {
|
|
233
|
+
const normalized = {
|
|
234
|
+
protect: [],
|
|
235
|
+
scoreThreshold: DEFAULT_FORGETTING_SCORE_THRESHOLD,
|
|
236
|
+
maxAgeDays: DEFAULT_FORGETTING_MAX_AGE_DAYS,
|
|
237
|
+
enabled: true
|
|
238
|
+
};
|
|
239
|
+
if (!input || typeof input !== "object") {
|
|
240
|
+
return normalized;
|
|
241
|
+
}
|
|
242
|
+
const record = input;
|
|
243
|
+
if (Array.isArray(record.protect)) {
|
|
244
|
+
normalized.protect = record.protect.filter((item) => typeof item === "string").map((item) => item.trim()).filter((item) => item.length > 0);
|
|
245
|
+
}
|
|
246
|
+
if (typeof record.scoreThreshold === "number" && Number.isFinite(record.scoreThreshold) && record.scoreThreshold >= 0 && record.scoreThreshold <= 1) {
|
|
247
|
+
normalized.scoreThreshold = record.scoreThreshold;
|
|
248
|
+
}
|
|
249
|
+
if (typeof record.maxAgeDays === "number" && Number.isFinite(record.maxAgeDays) && record.maxAgeDays >= 0) {
|
|
250
|
+
normalized.maxAgeDays = Math.floor(record.maxAgeDays);
|
|
251
|
+
}
|
|
252
|
+
if (typeof record.enabled === "boolean") {
|
|
253
|
+
normalized.enabled = record.enabled;
|
|
254
|
+
}
|
|
255
|
+
return normalized;
|
|
256
|
+
}
|
|
257
|
+
function normalizeDedupConfig(input) {
|
|
258
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
259
|
+
return void 0;
|
|
260
|
+
}
|
|
261
|
+
const record = input;
|
|
262
|
+
const normalized = {};
|
|
263
|
+
if (typeof record.aggressive === "boolean") {
|
|
264
|
+
normalized.aggressive = record.aggressive;
|
|
265
|
+
}
|
|
266
|
+
if (typeof record.threshold === "number" && Number.isFinite(record.threshold) && record.threshold >= 0 && record.threshold <= 1) {
|
|
267
|
+
normalized.threshold = record.threshold;
|
|
268
|
+
}
|
|
269
|
+
return Object.keys(normalized).length > 0 ? normalized : void 0;
|
|
270
|
+
}
|
|
271
|
+
function normalizeModelsConfig(input) {
|
|
272
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
273
|
+
return void 0;
|
|
274
|
+
}
|
|
275
|
+
const record = input;
|
|
276
|
+
const normalized = {};
|
|
277
|
+
if (typeof record.extraction === "string" && record.extraction.trim()) {
|
|
278
|
+
normalized.extraction = record.extraction.trim();
|
|
279
|
+
}
|
|
280
|
+
if (typeof record.claimExtraction === "string" && record.claimExtraction.trim()) {
|
|
281
|
+
normalized.claimExtraction = record.claimExtraction.trim();
|
|
282
|
+
}
|
|
283
|
+
if (typeof record.contradictionJudge === "string" && record.contradictionJudge.trim()) {
|
|
284
|
+
normalized.contradictionJudge = record.contradictionJudge.trim();
|
|
285
|
+
}
|
|
286
|
+
if (typeof record.handoffSummary === "string" && record.handoffSummary.trim()) {
|
|
287
|
+
normalized.handoffSummary = record.handoffSummary.trim();
|
|
288
|
+
}
|
|
289
|
+
return Object.keys(normalized).length > 0 ? normalized : void 0;
|
|
290
|
+
}
|
|
291
|
+
function normalizeLegacyContradictionModels(input) {
|
|
292
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
293
|
+
return void 0;
|
|
294
|
+
}
|
|
295
|
+
const record = input;
|
|
296
|
+
const normalized = {};
|
|
297
|
+
if (typeof record.claimExtractionModel === "string" && record.claimExtractionModel.trim()) {
|
|
298
|
+
normalized.claimExtraction = record.claimExtractionModel.trim();
|
|
299
|
+
}
|
|
300
|
+
if (typeof record.judgeModel === "string" && record.judgeModel.trim()) {
|
|
301
|
+
normalized.contradictionJudge = record.judgeModel.trim();
|
|
302
|
+
}
|
|
303
|
+
return Object.keys(normalized).length > 0 ? normalized : void 0;
|
|
304
|
+
}
|
|
305
|
+
function toLegacyBaseModel(input) {
|
|
306
|
+
if (typeof input !== "string" || !input.trim()) {
|
|
307
|
+
return void 0;
|
|
308
|
+
}
|
|
309
|
+
return input.trim();
|
|
310
|
+
}
|
|
311
|
+
function resolveTaskModels(explicitModels, legacyModels, legacyBaseModel) {
|
|
312
|
+
const mergedModels = {
|
|
313
|
+
...explicitModels ?? {}
|
|
314
|
+
};
|
|
315
|
+
if (legacyModels?.claimExtraction && !mergedModels.claimExtraction) {
|
|
316
|
+
mergedModels.claimExtraction = legacyModels.claimExtraction;
|
|
317
|
+
}
|
|
318
|
+
if (legacyModels?.contradictionJudge && !mergedModels.contradictionJudge) {
|
|
319
|
+
mergedModels.contradictionJudge = legacyModels.contradictionJudge;
|
|
320
|
+
}
|
|
321
|
+
const fallbackModel = legacyBaseModel ?? DEFAULT_TASK_MODEL;
|
|
322
|
+
return {
|
|
323
|
+
extraction: mergedModels.extraction ?? fallbackModel,
|
|
324
|
+
claimExtraction: mergedModels.claimExtraction ?? fallbackModel,
|
|
325
|
+
contradictionJudge: mergedModels.contradictionJudge ?? fallbackModel,
|
|
326
|
+
handoffSummary: mergedModels.handoffSummary ?? fallbackModel
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
function normalizeContradictionConfig(input) {
|
|
330
|
+
const normalized = {
|
|
331
|
+
enabled: DEFAULT_CONTRADICTION_ENABLED,
|
|
332
|
+
autoSupersedeConfidence: DEFAULT_AUTO_SUPERSEDE_CONFIDENCE
|
|
333
|
+
};
|
|
334
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
335
|
+
return normalized;
|
|
336
|
+
}
|
|
337
|
+
const record = input;
|
|
338
|
+
if (typeof record.enabled === "boolean") {
|
|
339
|
+
normalized.enabled = record.enabled;
|
|
340
|
+
}
|
|
341
|
+
if (typeof record.autoSupersedeConfidence === "number" && Number.isFinite(record.autoSupersedeConfidence) && record.autoSupersedeConfidence >= 0 && record.autoSupersedeConfidence <= 1) {
|
|
342
|
+
normalized.autoSupersedeConfidence = record.autoSupersedeConfidence;
|
|
343
|
+
}
|
|
344
|
+
return normalized;
|
|
345
|
+
}
|
|
346
|
+
function normalizeLabelProjectMap(input) {
|
|
347
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
348
|
+
return void 0;
|
|
349
|
+
}
|
|
350
|
+
const out = {};
|
|
351
|
+
for (const [rawLabel, rawProject] of Object.entries(input)) {
|
|
352
|
+
if (typeof rawProject !== "string") {
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
const label = normalizeLabel(rawLabel);
|
|
356
|
+
const project = rawProject.trim();
|
|
357
|
+
if (!label || !project) {
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
out[label] = project;
|
|
361
|
+
}
|
|
362
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
363
|
+
}
|
|
364
|
+
function normalizeProjectsMap(input) {
|
|
365
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
366
|
+
return void 0;
|
|
367
|
+
}
|
|
368
|
+
const out = {};
|
|
369
|
+
for (const [rawDirKey, rawValue] of Object.entries(input)) {
|
|
370
|
+
if (!rawValue || typeof rawValue !== "object" || Array.isArray(rawValue)) {
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
const value = rawValue;
|
|
374
|
+
const normalizeDependencies = () => {
|
|
375
|
+
if (!Array.isArray(value.dependencies)) {
|
|
376
|
+
return void 0;
|
|
377
|
+
}
|
|
378
|
+
const dependencies2 = Array.from(
|
|
379
|
+
new Set(
|
|
380
|
+
value.dependencies.filter((dependency) => typeof dependency === "string").map((dependency) => dependency.trim()).filter((dependency) => dependency.length > 0)
|
|
381
|
+
)
|
|
382
|
+
);
|
|
383
|
+
return dependencies2.length > 0 ? dependencies2 : void 0;
|
|
384
|
+
};
|
|
385
|
+
if (typeof value.project === "string" && value.project.trim() && typeof value.platform === "string" && value.platform.trim()) {
|
|
386
|
+
const dirKey = rawDirKey.trim();
|
|
387
|
+
if (!dirKey) {
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
const entry = {
|
|
391
|
+
project: value.project.trim(),
|
|
392
|
+
platform: value.platform.trim()
|
|
393
|
+
};
|
|
394
|
+
if (typeof value.dbPath === "string" && value.dbPath.trim()) {
|
|
395
|
+
entry.dbPath = value.dbPath.trim();
|
|
396
|
+
}
|
|
397
|
+
const dependencies2 = normalizeDependencies();
|
|
398
|
+
if (dependencies2) {
|
|
399
|
+
entry.dependencies = dependencies2;
|
|
400
|
+
}
|
|
401
|
+
out[path.resolve(resolveUserPath(dirKey))] = entry;
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
if (typeof value.platform !== "string" || !value.platform.trim()) {
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
if (typeof value.projectDir !== "string" || !value.projectDir.trim()) {
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
const legacyProject = rawDirKey.trim();
|
|
411
|
+
if (!legacyProject) {
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
const legacyEntry = {
|
|
415
|
+
project: legacyProject,
|
|
416
|
+
platform: value.platform.trim()
|
|
417
|
+
};
|
|
418
|
+
if (typeof value.dbPath === "string" && value.dbPath.trim()) {
|
|
419
|
+
legacyEntry.dbPath = value.dbPath.trim();
|
|
420
|
+
}
|
|
421
|
+
const dependencies = normalizeDependencies();
|
|
422
|
+
if (dependencies) {
|
|
423
|
+
legacyEntry.dependencies = dependencies;
|
|
424
|
+
}
|
|
425
|
+
out[path.resolve(resolveUserPath(value.projectDir.trim()))] = legacyEntry;
|
|
426
|
+
}
|
|
427
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
428
|
+
}
|
|
429
|
+
function normalizeConfig(input) {
|
|
430
|
+
const record = input && typeof input === "object" ? input : {};
|
|
431
|
+
const explicitModels = normalizeModelsConfig(record.models);
|
|
432
|
+
const legacyModels = normalizeLegacyContradictionModels(record.contradiction);
|
|
433
|
+
const legacyBaseModel = toLegacyBaseModel(record.model);
|
|
434
|
+
const normalized = {
|
|
435
|
+
models: resolveTaskModels(explicitModels, legacyModels, legacyBaseModel),
|
|
436
|
+
embedding: normalizeEmbeddingConfig(record.embedding),
|
|
437
|
+
db: normalizeDbConfig(record.db),
|
|
438
|
+
forgetting: normalizeForgettingConfig(record.forgetting),
|
|
439
|
+
contradiction: normalizeContradictionConfig(record.contradiction)
|
|
440
|
+
};
|
|
441
|
+
if (typeof record.auth === "string") {
|
|
442
|
+
const auth = record.auth.trim();
|
|
443
|
+
if (isAgenrAuthMethod(auth)) {
|
|
444
|
+
normalized.auth = auth;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
if (typeof record.provider === "string") {
|
|
448
|
+
const provider = record.provider.trim().toLowerCase();
|
|
449
|
+
if (isAgenrProvider2(provider)) {
|
|
450
|
+
normalized.provider = provider;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
const credentials = normalizeStoredCredentials(record.credentials);
|
|
454
|
+
if (credentials) {
|
|
455
|
+
normalized.credentials = credentials;
|
|
456
|
+
}
|
|
457
|
+
const labelProjectMap = normalizeLabelProjectMap(record.labelProjectMap);
|
|
458
|
+
if (labelProjectMap) {
|
|
459
|
+
normalized.labelProjectMap = labelProjectMap;
|
|
460
|
+
}
|
|
461
|
+
const projects = normalizeProjectsMap(record.projects);
|
|
462
|
+
if (projects) {
|
|
463
|
+
normalized.projects = projects;
|
|
464
|
+
}
|
|
465
|
+
const dedup = normalizeDedupConfig(record.dedup);
|
|
466
|
+
if (dedup) {
|
|
467
|
+
normalized.dedup = dedup;
|
|
468
|
+
}
|
|
469
|
+
return normalized;
|
|
470
|
+
}
|
|
471
|
+
function ensureConfigDir(env = process.env) {
|
|
472
|
+
const configDir = resolveConfigDir(env);
|
|
473
|
+
fs.mkdirSync(configDir, { recursive: true, mode: CONFIG_DIR_MODE });
|
|
474
|
+
try {
|
|
475
|
+
fs.chmodSync(configDir, CONFIG_DIR_MODE);
|
|
476
|
+
} catch {
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
function readConfig(env = process.env) {
|
|
480
|
+
const configPath = resolveConfigPath(env);
|
|
481
|
+
let raw;
|
|
482
|
+
try {
|
|
483
|
+
raw = fs.readFileSync(configPath, "utf8");
|
|
484
|
+
} catch (error) {
|
|
485
|
+
if (error.code === "ENOENT") {
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
throw error;
|
|
489
|
+
}
|
|
490
|
+
let parsed;
|
|
491
|
+
try {
|
|
492
|
+
parsed = JSON.parse(raw);
|
|
493
|
+
} catch (error) {
|
|
494
|
+
throw new Error(
|
|
495
|
+
`Failed to parse config at ${configPath}: ${error instanceof Error ? error.message : String(error)}`
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
return normalizeConfig(parsed);
|
|
499
|
+
}
|
|
500
|
+
function writeConfig(config, env = process.env) {
|
|
501
|
+
ensureConfigDir(env);
|
|
502
|
+
const configPath = resolveConfigPath(env);
|
|
503
|
+
const normalized = normalizeConfig(config);
|
|
504
|
+
fs.writeFileSync(configPath, `${JSON.stringify(normalized, null, 2)}
|
|
505
|
+
`, {
|
|
506
|
+
encoding: "utf8",
|
|
507
|
+
mode: CONFIG_FILE_MODE
|
|
508
|
+
});
|
|
509
|
+
try {
|
|
510
|
+
fs.chmodSync(configPath, CONFIG_FILE_MODE);
|
|
511
|
+
} catch {
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
function resolveProjectFromGlobalConfig(projectDir, env = process.env) {
|
|
515
|
+
const config = readConfig(env);
|
|
516
|
+
if (!config?.projects) {
|
|
517
|
+
return null;
|
|
518
|
+
}
|
|
519
|
+
const resolvedDir = path.resolve(resolveUserPath(projectDir));
|
|
520
|
+
const entry = config.projects[resolvedDir];
|
|
521
|
+
if (!entry) {
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
524
|
+
return {
|
|
525
|
+
slug: entry.project,
|
|
526
|
+
platform: entry.platform,
|
|
527
|
+
dbPath: entry.dbPath
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
function mergeConfigPatch(current, patch) {
|
|
531
|
+
const merged = {
|
|
532
|
+
...current ?? {},
|
|
533
|
+
...patch
|
|
534
|
+
};
|
|
535
|
+
if (current?.credentials || patch.credentials) {
|
|
536
|
+
merged.credentials = {
|
|
537
|
+
...current?.credentials ?? {},
|
|
538
|
+
...patch.credentials ?? {}
|
|
539
|
+
};
|
|
540
|
+
if (Object.keys(merged.credentials).length === 0) {
|
|
541
|
+
delete merged.credentials;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
if (current?.embedding || patch.embedding) {
|
|
545
|
+
merged.embedding = {
|
|
546
|
+
...current?.embedding ?? {},
|
|
547
|
+
...patch.embedding ?? {}
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
if (current?.db || patch.db) {
|
|
551
|
+
merged.db = {
|
|
552
|
+
...current?.db ?? {},
|
|
553
|
+
...patch.db ?? {}
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
if (current?.forgetting || patch.forgetting) {
|
|
557
|
+
merged.forgetting = {
|
|
558
|
+
...current?.forgetting ?? {},
|
|
559
|
+
...patch.forgetting ?? {}
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
if (current?.dedup || patch.dedup) {
|
|
563
|
+
merged.dedup = {
|
|
564
|
+
...current?.dedup ?? {},
|
|
565
|
+
...patch.dedup ?? {}
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
if (current?.models || patch.models) {
|
|
569
|
+
merged.models = {
|
|
570
|
+
...current?.models ?? {},
|
|
571
|
+
...patch.models ?? {}
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
if (current?.contradiction || patch.contradiction) {
|
|
575
|
+
merged.contradiction = {
|
|
576
|
+
...current?.contradiction ?? {},
|
|
577
|
+
...patch.contradiction ?? {}
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
return normalizeConfig(merged);
|
|
581
|
+
}
|
|
582
|
+
function modelIsValid(provider, model) {
|
|
583
|
+
try {
|
|
584
|
+
resolveModel(provider, model);
|
|
585
|
+
return true;
|
|
586
|
+
} catch {
|
|
587
|
+
return false;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
function hasCompleteTaskModels(models) {
|
|
591
|
+
if (!models || typeof models !== "object") {
|
|
592
|
+
return false;
|
|
593
|
+
}
|
|
594
|
+
const record = models;
|
|
595
|
+
return MODEL_TASK_KEYS.every((task) => typeof record[task] === "string" && record[task].trim().length > 0);
|
|
596
|
+
}
|
|
597
|
+
function appendInvalidTaskModelWarnings(config, warnings) {
|
|
598
|
+
if (!config.provider) {
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
for (const task of MODEL_TASK_KEYS) {
|
|
602
|
+
const model = config.models[task];
|
|
603
|
+
if (!modelIsValid(config.provider, model)) {
|
|
604
|
+
warnings.push(
|
|
605
|
+
`Warning: models.${task} "${model}" is not available for provider "${config.provider}". Update it with: agenr config set models.${task} <model>.`
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
function normalizeValue(value) {
|
|
611
|
+
const trimmed = value.trim();
|
|
612
|
+
if (!trimmed) {
|
|
613
|
+
throw new Error("Value cannot be empty.");
|
|
614
|
+
}
|
|
615
|
+
return trimmed;
|
|
616
|
+
}
|
|
617
|
+
function isCompleteConfig(config) {
|
|
618
|
+
if (!config?.auth || !config.provider || !hasCompleteTaskModels(config.models)) {
|
|
619
|
+
return false;
|
|
620
|
+
}
|
|
621
|
+
return authMethodToProvider(config.auth) === config.provider;
|
|
622
|
+
}
|
|
623
|
+
function setConfigKey(current, key, value) {
|
|
624
|
+
const warnings = [];
|
|
625
|
+
const next = mergeConfigPatch(current, {});
|
|
626
|
+
if (key.startsWith("models.")) {
|
|
627
|
+
const task = key.slice("models.".length);
|
|
628
|
+
if (!isModelTask(task)) {
|
|
629
|
+
throw new Error(`Invalid model task "${task}". Expected one of: ${MODEL_TASK_KEYS.join(", ")}.`);
|
|
630
|
+
}
|
|
631
|
+
const trimmedValue = value.trim();
|
|
632
|
+
const currentModels = {
|
|
633
|
+
...next.models
|
|
634
|
+
};
|
|
635
|
+
currentModels[task] = trimmedValue.toLowerCase() === "default" ? DEFAULT_TASK_MODEL : normalizeValue(value);
|
|
636
|
+
next.models = currentModels;
|
|
637
|
+
if (next.provider && !modelIsValid(next.provider, currentModels[task])) {
|
|
638
|
+
warnings.push(
|
|
639
|
+
`Warning: models.${task} "${currentModels[task]}" is not available for provider "${next.provider}".`
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
return { config: next, warnings };
|
|
643
|
+
}
|
|
644
|
+
const normalizedValue = normalizeValue(value);
|
|
645
|
+
if (key === "auth") {
|
|
646
|
+
if (!isAgenrAuthMethod(normalizedValue)) {
|
|
647
|
+
throw new Error(
|
|
648
|
+
`Invalid auth method "${value}". Expected one of: anthropic-oauth, anthropic-token, anthropic-api-key, openai-subscription, openai-api-key.`
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
next.auth = normalizedValue;
|
|
652
|
+
next.provider = authMethodToProvider(normalizedValue);
|
|
653
|
+
appendInvalidTaskModelWarnings(next, warnings);
|
|
654
|
+
return { config: next, warnings };
|
|
655
|
+
}
|
|
656
|
+
if (key === "provider") {
|
|
657
|
+
const provider = normalizedValue.toLowerCase();
|
|
658
|
+
if (!isAgenrProvider2(provider)) {
|
|
659
|
+
throw new Error(`Invalid provider "${value}". Expected one of: anthropic, openai, openai-codex.`);
|
|
660
|
+
}
|
|
661
|
+
if (next.auth) {
|
|
662
|
+
const expectedProvider = authMethodToProvider(next.auth);
|
|
663
|
+
if (provider !== expectedProvider) {
|
|
664
|
+
throw new Error(
|
|
665
|
+
`Provider "${provider}" is incompatible with auth "${next.auth}". Use \`agenr config set auth <method>\` to switch auth/provider together.`
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
next.provider = provider;
|
|
670
|
+
appendInvalidTaskModelWarnings(next, warnings);
|
|
671
|
+
return { config: next, warnings };
|
|
672
|
+
}
|
|
673
|
+
throw new Error('Invalid key. Expected one of: "provider", "auth", or "models.<task>".');
|
|
674
|
+
}
|
|
675
|
+
function setStoredCredential(current, keyName, secret) {
|
|
676
|
+
const normalizedSecret = normalizeValue(secret);
|
|
677
|
+
const next = mergeConfigPatch(current, {});
|
|
678
|
+
const credentials = {
|
|
679
|
+
...next.credentials ?? {}
|
|
680
|
+
};
|
|
681
|
+
if (keyName === "anthropic") {
|
|
682
|
+
credentials.anthropicApiKey = normalizedSecret;
|
|
683
|
+
} else if (keyName === "anthropic-token") {
|
|
684
|
+
credentials.anthropicOauthToken = normalizedSecret;
|
|
685
|
+
} else if (keyName === "openai") {
|
|
686
|
+
credentials.openaiApiKey = normalizedSecret;
|
|
687
|
+
} else {
|
|
688
|
+
throw new Error(
|
|
689
|
+
`Invalid key name "${keyName}". Expected one of: anthropic, anthropic-token, openai.`
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
next.credentials = credentials;
|
|
693
|
+
return next;
|
|
694
|
+
}
|
|
695
|
+
function maskSecret(secret) {
|
|
696
|
+
if (!secret) {
|
|
697
|
+
return "(not set)";
|
|
698
|
+
}
|
|
699
|
+
const trimmed = secret.trim();
|
|
700
|
+
if (!trimmed) {
|
|
701
|
+
return "(not set)";
|
|
702
|
+
}
|
|
703
|
+
return `****${trimmed.slice(-4)}`;
|
|
704
|
+
}
|
|
705
|
+
function describeAuth(auth) {
|
|
706
|
+
if (auth === "anthropic-oauth") {
|
|
707
|
+
return "Anthropic subscription (OAuth)";
|
|
708
|
+
}
|
|
709
|
+
if (auth === "anthropic-token") {
|
|
710
|
+
return "Anthropic subscription (long-lived token)";
|
|
711
|
+
}
|
|
712
|
+
if (auth === "anthropic-api-key") {
|
|
713
|
+
return "Anthropic API key";
|
|
714
|
+
}
|
|
715
|
+
if (auth === "openai-subscription") {
|
|
716
|
+
return "OpenAI subscription (Codex CLI)";
|
|
717
|
+
}
|
|
718
|
+
return "OpenAI API key";
|
|
719
|
+
}
|
|
720
|
+
function resolveModelForTask(config, task) {
|
|
721
|
+
return config?.models?.[task] ?? DEFAULT_TASK_MODEL;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// src/embeddings/client.ts
|
|
725
|
+
var OPENAI_EMBEDDINGS_URL = "https://api.openai.com/v1/embeddings";
|
|
726
|
+
var EMBEDDING_MODEL = "text-embedding-3-small";
|
|
727
|
+
var EMBEDDING_DIMENSIONS = 1024;
|
|
728
|
+
var EMBEDDING_BATCH_SIZE = 200;
|
|
729
|
+
var EMBEDDING_MAX_CONCURRENCY = 3;
|
|
730
|
+
var EMBEDDING_MAX_ATTEMPTS = 5;
|
|
731
|
+
function chunkArray(values, chunkSize) {
|
|
732
|
+
const out = [];
|
|
733
|
+
for (let i = 0; i < values.length; i += chunkSize) {
|
|
734
|
+
out.push(values.slice(i, i + chunkSize));
|
|
735
|
+
}
|
|
736
|
+
return out;
|
|
737
|
+
}
|
|
738
|
+
function getErrorSnippet(rawBody, fallbackMessage = "unknown error") {
|
|
739
|
+
const trimmed = rawBody.trim();
|
|
740
|
+
if (!trimmed) {
|
|
741
|
+
return fallbackMessage;
|
|
742
|
+
}
|
|
743
|
+
try {
|
|
744
|
+
const parsed = JSON.parse(trimmed);
|
|
745
|
+
const message = parsed.error?.message;
|
|
746
|
+
if (typeof message === "string" && message.trim()) {
|
|
747
|
+
return message.trim();
|
|
748
|
+
}
|
|
749
|
+
} catch {
|
|
750
|
+
}
|
|
751
|
+
const maxLength = 200;
|
|
752
|
+
if (trimmed.length <= maxLength) {
|
|
753
|
+
return trimmed;
|
|
754
|
+
}
|
|
755
|
+
return `${trimmed.slice(0, maxLength)}...`;
|
|
756
|
+
}
|
|
757
|
+
function buildHttpError(status, body) {
|
|
758
|
+
const detail = getErrorSnippet(body);
|
|
759
|
+
if (status === 401) {
|
|
760
|
+
return new Error(`OpenAI embeddings request failed (401): invalid API key. ${detail}`);
|
|
761
|
+
}
|
|
762
|
+
if (status === 429) {
|
|
763
|
+
return new Error(`OpenAI embeddings request failed (429): rate limited. ${detail}`);
|
|
764
|
+
}
|
|
765
|
+
return new Error(`OpenAI embeddings request failed (${status}): ${detail}`);
|
|
766
|
+
}
|
|
767
|
+
function isRetryableStatus(status) {
|
|
768
|
+
return status === 429 || status === 500 || status === 502 || status === 503 || status === 504;
|
|
769
|
+
}
|
|
770
|
+
function isRetryableEmbeddingError(error) {
|
|
771
|
+
if (!(error instanceof Error)) {
|
|
772
|
+
return false;
|
|
773
|
+
}
|
|
774
|
+
const message = error.message.toLowerCase();
|
|
775
|
+
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");
|
|
776
|
+
}
|
|
777
|
+
async function sleepMs(ms) {
|
|
778
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
779
|
+
}
|
|
780
|
+
async function embedBatch(texts, apiKey) {
|
|
781
|
+
let response = null;
|
|
782
|
+
let rawBody = "";
|
|
783
|
+
let lastError = null;
|
|
784
|
+
for (let attempt = 1; attempt <= EMBEDDING_MAX_ATTEMPTS; attempt += 1) {
|
|
785
|
+
try {
|
|
786
|
+
response = await fetch(OPENAI_EMBEDDINGS_URL, {
|
|
787
|
+
method: "POST",
|
|
788
|
+
headers: {
|
|
789
|
+
Authorization: `Bearer ${apiKey}`,
|
|
790
|
+
"Content-Type": "application/json"
|
|
791
|
+
},
|
|
792
|
+
body: JSON.stringify({
|
|
793
|
+
model: EMBEDDING_MODEL,
|
|
794
|
+
dimensions: EMBEDDING_DIMENSIONS,
|
|
795
|
+
input: texts
|
|
796
|
+
})
|
|
797
|
+
});
|
|
798
|
+
rawBody = await response.text();
|
|
799
|
+
} catch (error) {
|
|
800
|
+
lastError = new Error(
|
|
801
|
+
`Failed to call OpenAI embeddings API: ${error instanceof Error ? error.message : String(error)}`
|
|
802
|
+
);
|
|
803
|
+
if (attempt < EMBEDDING_MAX_ATTEMPTS && isRetryableEmbeddingError(lastError)) {
|
|
804
|
+
const backoffMs = Math.min(2e3 * 2 ** (attempt - 1), 6e4);
|
|
805
|
+
await sleepMs(backoffMs);
|
|
806
|
+
continue;
|
|
807
|
+
}
|
|
808
|
+
throw lastError;
|
|
809
|
+
}
|
|
810
|
+
if (!response.ok) {
|
|
811
|
+
const httpError = buildHttpError(response.status, rawBody);
|
|
812
|
+
lastError = httpError;
|
|
813
|
+
if (attempt < EMBEDDING_MAX_ATTEMPTS && isRetryableStatus(response.status)) {
|
|
814
|
+
const backoffMs = Math.min(2e3 * 2 ** (attempt - 1), 6e4);
|
|
815
|
+
await sleepMs(backoffMs);
|
|
816
|
+
continue;
|
|
817
|
+
}
|
|
818
|
+
throw httpError;
|
|
819
|
+
}
|
|
820
|
+
lastError = null;
|
|
821
|
+
break;
|
|
822
|
+
}
|
|
823
|
+
if (!response || !response.ok) {
|
|
824
|
+
throw lastError instanceof Error ? lastError : new Error("OpenAI embeddings request failed.");
|
|
825
|
+
}
|
|
826
|
+
let parsed;
|
|
827
|
+
try {
|
|
828
|
+
parsed = JSON.parse(rawBody);
|
|
829
|
+
} catch (error) {
|
|
830
|
+
throw new Error(
|
|
831
|
+
`OpenAI embeddings response was not valid JSON: ${error instanceof Error ? error.message : String(error)}`
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
if (!Array.isArray(parsed.data)) {
|
|
835
|
+
throw new Error("OpenAI embeddings response missing data array.");
|
|
836
|
+
}
|
|
837
|
+
const sorted = [...parsed.data].sort((a, b) => a.index - b.index);
|
|
838
|
+
if (sorted.length !== texts.length) {
|
|
839
|
+
throw new Error(
|
|
840
|
+
`OpenAI embeddings response length mismatch: expected ${texts.length}, received ${sorted.length}.`
|
|
841
|
+
);
|
|
842
|
+
}
|
|
843
|
+
const embeddings = [];
|
|
844
|
+
for (const item of sorted) {
|
|
845
|
+
if (!Array.isArray(item.embedding)) {
|
|
846
|
+
throw new Error("OpenAI embeddings response contained an item with no embedding array.");
|
|
847
|
+
}
|
|
848
|
+
if (!item.embedding.every((value) => typeof value === "number" && Number.isFinite(value))) {
|
|
849
|
+
throw new Error("OpenAI embeddings response contained a non-numeric embedding value.");
|
|
850
|
+
}
|
|
851
|
+
embeddings.push(item.embedding);
|
|
852
|
+
}
|
|
853
|
+
return embeddings;
|
|
854
|
+
}
|
|
855
|
+
function composeEmbeddingText(entry) {
|
|
856
|
+
return `${entry.type}: ${entry.subject} - ${entry.content}`;
|
|
857
|
+
}
|
|
858
|
+
async function embed(texts, apiKey) {
|
|
859
|
+
if (texts.length === 0) {
|
|
860
|
+
return [];
|
|
861
|
+
}
|
|
862
|
+
const normalizedApiKey = apiKey.trim();
|
|
863
|
+
if (!normalizedApiKey) {
|
|
864
|
+
throw new Error("OpenAI API key is required for embeddings.");
|
|
865
|
+
}
|
|
866
|
+
const batches = chunkArray(texts, EMBEDDING_BATCH_SIZE);
|
|
867
|
+
const out = new Array(texts.length);
|
|
868
|
+
let nextBatchIndex = 0;
|
|
869
|
+
const worker = async () => {
|
|
870
|
+
while (true) {
|
|
871
|
+
const batchIndex = nextBatchIndex;
|
|
872
|
+
nextBatchIndex += 1;
|
|
873
|
+
if (batchIndex >= batches.length) {
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
const batch = batches[batchIndex];
|
|
877
|
+
const batchEmbeddings = await embedBatch(batch, normalizedApiKey);
|
|
878
|
+
const offset = batchIndex * EMBEDDING_BATCH_SIZE;
|
|
879
|
+
for (let i = 0; i < batchEmbeddings.length; i += 1) {
|
|
880
|
+
out[offset + i] = batchEmbeddings[i];
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
};
|
|
884
|
+
const workerCount = Math.min(EMBEDDING_MAX_CONCURRENCY, batches.length);
|
|
885
|
+
await Promise.all(Array.from({ length: workerCount }, () => worker()));
|
|
886
|
+
if (out.some((item) => item === void 0)) {
|
|
887
|
+
throw new Error("Embedding generation failed to return all vectors.");
|
|
888
|
+
}
|
|
889
|
+
return out;
|
|
890
|
+
}
|
|
891
|
+
function resolveEmbeddingApiKey(config, env = process.env) {
|
|
892
|
+
const fromEmbeddingConfig = config?.embedding?.apiKey?.trim();
|
|
893
|
+
if (fromEmbeddingConfig) {
|
|
894
|
+
return fromEmbeddingConfig;
|
|
895
|
+
}
|
|
896
|
+
const fromStoredCredentials = config?.credentials?.openaiApiKey?.trim();
|
|
897
|
+
if (fromStoredCredentials) {
|
|
898
|
+
return fromStoredCredentials;
|
|
899
|
+
}
|
|
900
|
+
const fromEnv = env.OPENAI_API_KEY?.trim();
|
|
901
|
+
if (fromEnv) {
|
|
902
|
+
return fromEnv;
|
|
903
|
+
}
|
|
904
|
+
throw new Error(
|
|
905
|
+
"OpenAI API key is required for embeddings. Set config.embedding.apiKey, config.credentials.openaiApiKey, or OPENAI_API_KEY."
|
|
906
|
+
);
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// src/llm/credentials.ts
|
|
910
|
+
import { execSync } from "child_process";
|
|
911
|
+
import { createHash } from "crypto";
|
|
912
|
+
import fs2 from "fs";
|
|
913
|
+
import os2 from "os";
|
|
914
|
+
import path2 from "path";
|
|
915
|
+
function safeReadJson(filePath) {
|
|
916
|
+
try {
|
|
917
|
+
const raw = fs2.readFileSync(filePath, "utf8");
|
|
918
|
+
return JSON.parse(raw);
|
|
919
|
+
} catch {
|
|
920
|
+
return null;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
function resolveUserPath2(inputPath) {
|
|
924
|
+
if (!inputPath.startsWith("~")) {
|
|
925
|
+
return inputPath;
|
|
926
|
+
}
|
|
927
|
+
return path2.join(os2.homedir(), inputPath.slice(1));
|
|
928
|
+
}
|
|
929
|
+
function resolveHomeDir(env = process.env) {
|
|
930
|
+
const home = env.HOME?.trim();
|
|
931
|
+
if (home) {
|
|
932
|
+
return resolveUserPath2(home);
|
|
933
|
+
}
|
|
934
|
+
return os2.homedir();
|
|
935
|
+
}
|
|
936
|
+
function resolveCodexHome(env = process.env) {
|
|
937
|
+
const codexHome = env.CODEX_HOME ? resolveUserPath2(env.CODEX_HOME) : "~/.codex";
|
|
938
|
+
const resolved = resolveUserPath2(codexHome);
|
|
939
|
+
try {
|
|
940
|
+
return fs2.realpathSync.native(resolved);
|
|
941
|
+
} catch {
|
|
942
|
+
return resolved;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
function parseCodexFromFile(env = process.env) {
|
|
946
|
+
const authPath = path2.join(resolveCodexHome(env), "auth.json");
|
|
947
|
+
const parsed = safeReadJson(authPath);
|
|
948
|
+
if (!parsed || typeof parsed !== "object") {
|
|
949
|
+
return null;
|
|
950
|
+
}
|
|
951
|
+
const record = parsed;
|
|
952
|
+
const tokens = record.tokens;
|
|
953
|
+
if (!tokens || typeof tokens !== "object") {
|
|
954
|
+
return null;
|
|
955
|
+
}
|
|
956
|
+
const accessToken = tokens.access_token;
|
|
957
|
+
if (typeof accessToken !== "string" || !accessToken.trim()) {
|
|
958
|
+
return null;
|
|
959
|
+
}
|
|
960
|
+
return { accessToken, source: `file:${authPath}` };
|
|
961
|
+
}
|
|
962
|
+
function resolveCodexKeychainAccount(env = process.env) {
|
|
963
|
+
const hash = createHash("sha256").update(resolveCodexHome(env)).digest("hex");
|
|
964
|
+
return `cli|${hash.slice(0, 16)}`;
|
|
965
|
+
}
|
|
966
|
+
function parseCodexFromKeychain(env = process.env) {
|
|
967
|
+
if (process.platform !== "darwin") {
|
|
968
|
+
return null;
|
|
969
|
+
}
|
|
970
|
+
try {
|
|
971
|
+
const account = resolveCodexKeychainAccount(env);
|
|
972
|
+
const raw = execSync(`security find-generic-password -s "Codex Auth" -a "${account}" -w`, {
|
|
973
|
+
encoding: "utf8",
|
|
974
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
975
|
+
timeout: 5e3
|
|
976
|
+
}).trim();
|
|
977
|
+
const parsed = JSON.parse(raw);
|
|
978
|
+
const tokens = parsed.tokens;
|
|
979
|
+
const accessToken = tokens?.access_token;
|
|
980
|
+
if (typeof accessToken !== "string" || !accessToken.trim()) {
|
|
981
|
+
return null;
|
|
982
|
+
}
|
|
983
|
+
return { accessToken, source: "keychain:Codex Auth" };
|
|
984
|
+
} catch {
|
|
985
|
+
return null;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
function parseClaudeCredentialRecord(parsed, source) {
|
|
989
|
+
if (!parsed || typeof parsed !== "object") {
|
|
990
|
+
return null;
|
|
991
|
+
}
|
|
992
|
+
const record = parsed;
|
|
993
|
+
const claudeOauth = record.claudeAiOauth;
|
|
994
|
+
if (!claudeOauth || typeof claudeOauth !== "object") {
|
|
995
|
+
return null;
|
|
996
|
+
}
|
|
997
|
+
const accessToken = claudeOauth.accessToken;
|
|
998
|
+
if (typeof accessToken !== "string" || !accessToken.trim()) {
|
|
999
|
+
return null;
|
|
1000
|
+
}
|
|
1001
|
+
return { accessToken, source };
|
|
1002
|
+
}
|
|
1003
|
+
function parseClaudeFromFiles(env = process.env) {
|
|
1004
|
+
const homeDir = resolveHomeDir(env);
|
|
1005
|
+
const candidates = [
|
|
1006
|
+
path2.join(homeDir, ".claude", ".credentials.json"),
|
|
1007
|
+
path2.join(homeDir, ".claude", "credentials.json")
|
|
1008
|
+
];
|
|
1009
|
+
for (const candidate of candidates) {
|
|
1010
|
+
const parsed = safeReadJson(candidate);
|
|
1011
|
+
const result = parseClaudeCredentialRecord(parsed, `file:${candidate}`);
|
|
1012
|
+
if (result) {
|
|
1013
|
+
return result;
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
return null;
|
|
1017
|
+
}
|
|
1018
|
+
function parseClaudeFromKeychain() {
|
|
1019
|
+
if (process.platform !== "darwin") {
|
|
1020
|
+
return null;
|
|
1021
|
+
}
|
|
1022
|
+
try {
|
|
1023
|
+
const raw = execSync('security find-generic-password -s "Claude Code-credentials" -w', {
|
|
1024
|
+
encoding: "utf8",
|
|
1025
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1026
|
+
timeout: 5e3
|
|
1027
|
+
}).trim();
|
|
1028
|
+
return parseClaudeCredentialRecord(JSON.parse(raw), "keychain:Claude Code-credentials");
|
|
1029
|
+
} catch {
|
|
1030
|
+
return null;
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
function candidateFromToken(token, source) {
|
|
1034
|
+
const normalized = token?.trim();
|
|
1035
|
+
if (!normalized) {
|
|
1036
|
+
return null;
|
|
1037
|
+
}
|
|
1038
|
+
return {
|
|
1039
|
+
token: normalized,
|
|
1040
|
+
source
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
function resolveAnthropicOauthCredentials(env) {
|
|
1044
|
+
const file = parseClaudeFromFiles(env);
|
|
1045
|
+
if (file) {
|
|
1046
|
+
return {
|
|
1047
|
+
token: file.accessToken,
|
|
1048
|
+
source: file.source
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
const keychain = parseClaudeFromKeychain();
|
|
1052
|
+
if (keychain) {
|
|
1053
|
+
return {
|
|
1054
|
+
token: keychain.accessToken,
|
|
1055
|
+
source: keychain.source
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
return null;
|
|
1059
|
+
}
|
|
1060
|
+
function resolveAnthropicTokenCredentials(stored, env) {
|
|
1061
|
+
return candidateFromToken(env.ANTHROPIC_OAUTH_TOKEN, "env:ANTHROPIC_OAUTH_TOKEN") ?? candidateFromToken(stored?.anthropicOauthToken, "config:credentials.anthropicOauthToken");
|
|
1062
|
+
}
|
|
1063
|
+
function resolveAnthropicApiKeyCredentials(stored, env) {
|
|
1064
|
+
return candidateFromToken(env.ANTHROPIC_API_KEY, "env:ANTHROPIC_API_KEY") ?? candidateFromToken(stored?.anthropicApiKey, "config:credentials.anthropicApiKey");
|
|
1065
|
+
}
|
|
1066
|
+
function resolveOpenAISubscriptionCredentials(env) {
|
|
1067
|
+
const file = parseCodexFromFile(env);
|
|
1068
|
+
if (file) {
|
|
1069
|
+
return {
|
|
1070
|
+
token: file.accessToken,
|
|
1071
|
+
source: file.source
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
const keychain = parseCodexFromKeychain(env);
|
|
1075
|
+
if (keychain) {
|
|
1076
|
+
return {
|
|
1077
|
+
token: keychain.accessToken,
|
|
1078
|
+
source: keychain.source
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
return null;
|
|
1082
|
+
}
|
|
1083
|
+
function resolveOpenAIApiKeyCredentials(stored, env) {
|
|
1084
|
+
return candidateFromToken(env.OPENAI_API_KEY, "env:OPENAI_API_KEY") ?? candidateFromToken(stored?.openaiApiKey, "config:credentials.openaiApiKey");
|
|
1085
|
+
}
|
|
1086
|
+
function credentialSetupGuidance(auth) {
|
|
1087
|
+
if (auth === "anthropic-oauth") {
|
|
1088
|
+
return [
|
|
1089
|
+
"Claude CLI credentials not found.",
|
|
1090
|
+
"Install Claude Code CLI: npm install -g @anthropic-ai/claude-code",
|
|
1091
|
+
"Then sign in: claude"
|
|
1092
|
+
].join(" ");
|
|
1093
|
+
}
|
|
1094
|
+
if (auth === "anthropic-token") {
|
|
1095
|
+
return [
|
|
1096
|
+
"No long-lived Anthropic token found.",
|
|
1097
|
+
"Generate one with: claude setup-token",
|
|
1098
|
+
"Then store it: agenr config set-key anthropic-token <token>"
|
|
1099
|
+
].join(" ");
|
|
1100
|
+
}
|
|
1101
|
+
if (auth === "anthropic-api-key") {
|
|
1102
|
+
return [
|
|
1103
|
+
"No Anthropic API key found.",
|
|
1104
|
+
"Get a key from console.anthropic.com",
|
|
1105
|
+
"Then store it: agenr config set-key anthropic <key>"
|
|
1106
|
+
].join(" ");
|
|
1107
|
+
}
|
|
1108
|
+
if (auth === "openai-subscription") {
|
|
1109
|
+
return [
|
|
1110
|
+
"Codex CLI credentials not found or expired.",
|
|
1111
|
+
"Run: codex auth"
|
|
1112
|
+
].join(" ");
|
|
1113
|
+
}
|
|
1114
|
+
return [
|
|
1115
|
+
"No OpenAI API key found.",
|
|
1116
|
+
"Get a key from https://platform.openai.com/api-keys",
|
|
1117
|
+
"Then store it: agenr config set-key openai <key>"
|
|
1118
|
+
].join(" ");
|
|
1119
|
+
}
|
|
1120
|
+
function resolveCredentialCandidate(params) {
|
|
1121
|
+
const env = params.env ?? process.env;
|
|
1122
|
+
if (params.auth === "anthropic-oauth") {
|
|
1123
|
+
return resolveAnthropicOauthCredentials(env);
|
|
1124
|
+
}
|
|
1125
|
+
if (params.auth === "anthropic-token") {
|
|
1126
|
+
return resolveAnthropicTokenCredentials(params.storedCredentials, env);
|
|
1127
|
+
}
|
|
1128
|
+
if (params.auth === "anthropic-api-key") {
|
|
1129
|
+
return resolveAnthropicApiKeyCredentials(params.storedCredentials, env);
|
|
1130
|
+
}
|
|
1131
|
+
if (params.auth === "openai-subscription") {
|
|
1132
|
+
return resolveOpenAISubscriptionCredentials(env);
|
|
1133
|
+
}
|
|
1134
|
+
return resolveOpenAIApiKeyCredentials(params.storedCredentials, env);
|
|
1135
|
+
}
|
|
1136
|
+
function probeCredentials(params) {
|
|
1137
|
+
const candidate = resolveCredentialCandidate(params);
|
|
1138
|
+
if (!candidate) {
|
|
1139
|
+
return {
|
|
1140
|
+
available: false,
|
|
1141
|
+
guidance: credentialSetupGuidance(params.auth)
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
return {
|
|
1145
|
+
available: true,
|
|
1146
|
+
source: candidate.source,
|
|
1147
|
+
guidance: "Credentials available.",
|
|
1148
|
+
credentials: {
|
|
1149
|
+
apiKey: candidate.token,
|
|
1150
|
+
source: candidate.source
|
|
1151
|
+
}
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
function resolveCredentials(params) {
|
|
1155
|
+
const probe = probeCredentials(params);
|
|
1156
|
+
if (!probe.available || !probe.credentials) {
|
|
1157
|
+
throw new Error(`${probe.guidance} Run: agenr auth status`);
|
|
1158
|
+
}
|
|
1159
|
+
return probe.credentials;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
// src/llm/stream.ts
|
|
1163
|
+
import { streamSimple } from "@mariozechner/pi-ai/dist/stream.js";
|
|
1164
|
+
function logVerbose(params, line) {
|
|
1165
|
+
if (!params.verbose) {
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
const logger = params.onVerbose ?? ((message) => process.stderr.write(`${message}
|
|
1169
|
+
`));
|
|
1170
|
+
logger(line);
|
|
1171
|
+
}
|
|
1172
|
+
async function runSimpleStream(params) {
|
|
1173
|
+
const streamFn = params.streamSimpleImpl ?? streamSimple;
|
|
1174
|
+
const stream = streamFn(params.model, params.context, params.options);
|
|
1175
|
+
for await (const event of stream) {
|
|
1176
|
+
if (!params.verbose) {
|
|
1177
|
+
continue;
|
|
1178
|
+
}
|
|
1179
|
+
if (event.type === "thinking_start") {
|
|
1180
|
+
logVerbose(params, "[thinking]");
|
|
1181
|
+
} else if (event.type === "thinking_delta") {
|
|
1182
|
+
params.onStreamDelta?.(event.delta, "thinking");
|
|
1183
|
+
} else if (event.type === "thinking_end") {
|
|
1184
|
+
logVerbose(params, "[/thinking]");
|
|
1185
|
+
} else if (event.type === "text_delta") {
|
|
1186
|
+
params.onStreamDelta?.(event.delta, "text");
|
|
1187
|
+
} else if (event.type === "toolcall_delta") {
|
|
1188
|
+
params.onStreamDelta?.(event.delta, "text");
|
|
1189
|
+
} else if (event.type === "error") {
|
|
1190
|
+
logVerbose(params, `[error:${event.reason}] ${event.error.errorMessage ?? "unknown error"}`);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
return stream.result();
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
// src/version.ts
|
|
1197
|
+
import { createRequire } from "module";
|
|
1198
|
+
var require2 = createRequire(import.meta.url);
|
|
1199
|
+
var APP_VERSION = (() => {
|
|
1200
|
+
try {
|
|
1201
|
+
const raw = require2("../package.json");
|
|
1202
|
+
if (typeof raw.version === "string" && raw.version.trim().length > 0) {
|
|
1203
|
+
return raw.version.trim();
|
|
1204
|
+
}
|
|
1205
|
+
} catch {
|
|
1206
|
+
}
|
|
1207
|
+
const fromEnv = process.env.npm_package_version;
|
|
1208
|
+
if (typeof fromEnv === "string" && fromEnv.trim().length > 0) {
|
|
1209
|
+
return fromEnv.trim();
|
|
1210
|
+
}
|
|
1211
|
+
return "0.0.0";
|
|
1212
|
+
})();
|
|
1213
|
+
|
|
1214
|
+
// src/db/client.ts
|
|
1215
|
+
import { createClient } from "@libsql/client";
|
|
1216
|
+
import fs3 from "fs/promises";
|
|
1217
|
+
import os3 from "os";
|
|
1218
|
+
import path3 from "path";
|
|
1219
|
+
|
|
1220
|
+
// src/db/schema.ts
|
|
1221
|
+
var CREATE_IDX_ENTRIES_EMBEDDING_SQL = `
|
|
1222
|
+
CREATE INDEX IF NOT EXISTS idx_entries_embedding ON entries (
|
|
1223
|
+
libsql_vector_idx(embedding, 'metric=cosine', 'compress_neighbors=float8', 'max_neighbors=50')
|
|
1224
|
+
)
|
|
1225
|
+
`;
|
|
1226
|
+
var CREATE_ENTRIES_FTS_TABLE_SQL = `
|
|
1227
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS entries_fts USING fts5(
|
|
1228
|
+
content, subject, content=entries, content_rowid=rowid
|
|
1229
|
+
)
|
|
1230
|
+
`;
|
|
1231
|
+
var CREATE_ENTRIES_FTS_TRIGGER_AI_SQL = `
|
|
1232
|
+
CREATE TRIGGER IF NOT EXISTS entries_ai AFTER INSERT ON entries BEGIN
|
|
1233
|
+
INSERT INTO entries_fts(rowid, content, subject) VALUES (new.rowid, new.content, new.subject);
|
|
1234
|
+
END
|
|
1235
|
+
`;
|
|
1236
|
+
var CREATE_ENTRIES_FTS_TRIGGER_AD_SQL = `
|
|
1237
|
+
CREATE TRIGGER IF NOT EXISTS entries_ad AFTER DELETE ON entries BEGIN
|
|
1238
|
+
INSERT INTO entries_fts(entries_fts, rowid, content, subject) VALUES ('delete', old.rowid, old.content, old.subject);
|
|
1239
|
+
END
|
|
1240
|
+
`;
|
|
1241
|
+
var CREATE_ENTRIES_FTS_TRIGGER_AU_SQL = `
|
|
1242
|
+
CREATE TRIGGER IF NOT EXISTS entries_au AFTER UPDATE ON entries BEGIN
|
|
1243
|
+
INSERT INTO entries_fts(entries_fts, rowid, content, subject) VALUES ('delete', old.rowid, old.content, old.subject);
|
|
1244
|
+
INSERT INTO entries_fts(rowid, content, subject) VALUES (new.rowid, new.content, new.subject);
|
|
1245
|
+
END
|
|
1246
|
+
`;
|
|
1247
|
+
var REBUILD_ENTRIES_FTS_SQL = "INSERT INTO entries_fts(entries_fts) VALUES ('rebuild')";
|
|
1248
|
+
var LEGACY_IMPORTANCE_BACKFILL_META_KEY = "legacy_importance_backfill_from_confidence_v1";
|
|
1249
|
+
var BULK_INGEST_META_KEY = "bulk_ingest_state";
|
|
1250
|
+
var CREATE_TABLE_AND_TRIGGER_STATEMENTS = [
|
|
1251
|
+
`
|
|
1252
|
+
CREATE TABLE IF NOT EXISTS _meta (
|
|
1253
|
+
key TEXT PRIMARY KEY,
|
|
1254
|
+
value TEXT NOT NULL,
|
|
1255
|
+
updated_at TEXT NOT NULL
|
|
1256
|
+
)
|
|
1257
|
+
`,
|
|
1258
|
+
`
|
|
1259
|
+
CREATE TABLE IF NOT EXISTS entries (
|
|
1260
|
+
id TEXT PRIMARY KEY,
|
|
1261
|
+
type TEXT NOT NULL,
|
|
1262
|
+
subject TEXT NOT NULL,
|
|
1263
|
+
canonical_key TEXT,
|
|
1264
|
+
content TEXT NOT NULL,
|
|
1265
|
+
importance INTEGER NOT NULL,
|
|
1266
|
+
expiry TEXT NOT NULL,
|
|
1267
|
+
scope TEXT DEFAULT 'private',
|
|
1268
|
+
platform TEXT DEFAULT NULL,
|
|
1269
|
+
project TEXT DEFAULT NULL,
|
|
1270
|
+
source_file TEXT,
|
|
1271
|
+
source_context TEXT,
|
|
1272
|
+
embedding F32_BLOB(1024),
|
|
1273
|
+
created_at TEXT NOT NULL,
|
|
1274
|
+
updated_at TEXT NOT NULL,
|
|
1275
|
+
last_recalled_at TEXT,
|
|
1276
|
+
recall_count INTEGER DEFAULT 0,
|
|
1277
|
+
confirmations INTEGER DEFAULT 0,
|
|
1278
|
+
contradictions INTEGER DEFAULT 0,
|
|
1279
|
+
quality_score REAL NOT NULL DEFAULT 0.5,
|
|
1280
|
+
superseded_by TEXT,
|
|
1281
|
+
content_hash TEXT,
|
|
1282
|
+
norm_content_hash TEXT,
|
|
1283
|
+
minhash_sig BLOB,
|
|
1284
|
+
merged_from INTEGER DEFAULT 0,
|
|
1285
|
+
consolidated_at TEXT,
|
|
1286
|
+
retired INTEGER NOT NULL DEFAULT 0,
|
|
1287
|
+
retired_at TEXT,
|
|
1288
|
+
retired_reason TEXT,
|
|
1289
|
+
suppressed_contexts TEXT,
|
|
1290
|
+
FOREIGN KEY (superseded_by) REFERENCES entries(id)
|
|
1291
|
+
)
|
|
1292
|
+
`,
|
|
1293
|
+
`
|
|
1294
|
+
CREATE TABLE IF NOT EXISTS tags (
|
|
1295
|
+
entry_id TEXT NOT NULL,
|
|
1296
|
+
tag TEXT NOT NULL,
|
|
1297
|
+
PRIMARY KEY (entry_id, tag),
|
|
1298
|
+
FOREIGN KEY (entry_id) REFERENCES entries(id) ON DELETE CASCADE
|
|
1299
|
+
)
|
|
1300
|
+
`,
|
|
1301
|
+
`
|
|
1302
|
+
CREATE TABLE IF NOT EXISTS relations (
|
|
1303
|
+
id TEXT PRIMARY KEY,
|
|
1304
|
+
source_id TEXT NOT NULL,
|
|
1305
|
+
target_id TEXT NOT NULL,
|
|
1306
|
+
relation_type TEXT NOT NULL,
|
|
1307
|
+
created_at TEXT NOT NULL,
|
|
1308
|
+
FOREIGN KEY (source_id) REFERENCES entries(id) ON DELETE CASCADE,
|
|
1309
|
+
FOREIGN KEY (target_id) REFERENCES entries(id) ON DELETE CASCADE
|
|
1310
|
+
)
|
|
1311
|
+
`,
|
|
1312
|
+
`
|
|
1313
|
+
CREATE TABLE IF NOT EXISTS ingest_log (
|
|
1314
|
+
id TEXT PRIMARY KEY,
|
|
1315
|
+
file_path TEXT NOT NULL,
|
|
1316
|
+
ingested_at TEXT NOT NULL,
|
|
1317
|
+
entries_added INTEGER NOT NULL,
|
|
1318
|
+
entries_updated INTEGER NOT NULL,
|
|
1319
|
+
entries_skipped INTEGER NOT NULL,
|
|
1320
|
+
duration_ms INTEGER NOT NULL,
|
|
1321
|
+
content_hash TEXT,
|
|
1322
|
+
entries_superseded INTEGER NOT NULL DEFAULT 0,
|
|
1323
|
+
dedup_llm_calls INTEGER NOT NULL DEFAULT 0
|
|
1324
|
+
)
|
|
1325
|
+
`,
|
|
1326
|
+
`
|
|
1327
|
+
CREATE TABLE IF NOT EXISTS entry_sources (
|
|
1328
|
+
merged_entry_id TEXT NOT NULL REFERENCES entries(id),
|
|
1329
|
+
source_entry_id TEXT NOT NULL REFERENCES entries(id),
|
|
1330
|
+
original_confirmations INTEGER NOT NULL DEFAULT 0,
|
|
1331
|
+
original_recall_count INTEGER NOT NULL DEFAULT 0,
|
|
1332
|
+
original_created_at TEXT,
|
|
1333
|
+
PRIMARY KEY (merged_entry_id, source_entry_id)
|
|
1334
|
+
)
|
|
1335
|
+
`,
|
|
1336
|
+
`
|
|
1337
|
+
CREATE TABLE IF NOT EXISTS signal_watermarks (
|
|
1338
|
+
consumer_id TEXT PRIMARY KEY,
|
|
1339
|
+
last_received_seq INTEGER NOT NULL DEFAULT 0,
|
|
1340
|
+
updated_at TEXT NOT NULL
|
|
1341
|
+
)
|
|
1342
|
+
`,
|
|
1343
|
+
`
|
|
1344
|
+
CREATE TABLE IF NOT EXISTS conflict_log (
|
|
1345
|
+
id TEXT PRIMARY KEY,
|
|
1346
|
+
entry_a TEXT NOT NULL,
|
|
1347
|
+
entry_b TEXT NOT NULL,
|
|
1348
|
+
relation TEXT NOT NULL,
|
|
1349
|
+
confidence REAL NOT NULL,
|
|
1350
|
+
resolution TEXT NOT NULL,
|
|
1351
|
+
resolved_at TEXT,
|
|
1352
|
+
created_at TEXT NOT NULL
|
|
1353
|
+
)
|
|
1354
|
+
`,
|
|
1355
|
+
`
|
|
1356
|
+
CREATE TABLE IF NOT EXISTS co_recall_edges (
|
|
1357
|
+
entry_a TEXT NOT NULL,
|
|
1358
|
+
entry_b TEXT NOT NULL,
|
|
1359
|
+
edge_type TEXT NOT NULL DEFAULT 'co_recalled',
|
|
1360
|
+
weight REAL NOT NULL DEFAULT 0.1,
|
|
1361
|
+
session_count INTEGER NOT NULL DEFAULT 1,
|
|
1362
|
+
last_co_recalled TEXT NOT NULL,
|
|
1363
|
+
created_at TEXT NOT NULL,
|
|
1364
|
+
PRIMARY KEY (entry_a, entry_b, edge_type),
|
|
1365
|
+
FOREIGN KEY (entry_a) REFERENCES entries(id),
|
|
1366
|
+
FOREIGN KEY (entry_b) REFERENCES entries(id)
|
|
1367
|
+
)
|
|
1368
|
+
`,
|
|
1369
|
+
"CREATE INDEX IF NOT EXISTS idx_co_recall_a ON co_recall_edges(entry_a)",
|
|
1370
|
+
"CREATE INDEX IF NOT EXISTS idx_co_recall_b ON co_recall_edges(entry_b)",
|
|
1371
|
+
`
|
|
1372
|
+
CREATE TABLE IF NOT EXISTS review_queue (
|
|
1373
|
+
id TEXT PRIMARY KEY,
|
|
1374
|
+
entry_id TEXT NOT NULL,
|
|
1375
|
+
reason TEXT NOT NULL,
|
|
1376
|
+
detail TEXT,
|
|
1377
|
+
suggested_action TEXT NOT NULL DEFAULT 'review',
|
|
1378
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
1379
|
+
created_at TEXT NOT NULL,
|
|
1380
|
+
resolved_at TEXT,
|
|
1381
|
+
FOREIGN KEY (entry_id) REFERENCES entries(id)
|
|
1382
|
+
)
|
|
1383
|
+
`,
|
|
1384
|
+
"CREATE INDEX IF NOT EXISTS idx_review_queue_status ON review_queue(status)",
|
|
1385
|
+
"CREATE INDEX IF NOT EXISTS idx_review_queue_entry ON review_queue(entry_id)",
|
|
1386
|
+
"CREATE UNIQUE INDEX IF NOT EXISTS idx_review_queue_pending_dedup ON review_queue(entry_id, reason) WHERE status = 'pending'",
|
|
1387
|
+
CREATE_ENTRIES_FTS_TABLE_SQL,
|
|
1388
|
+
CREATE_ENTRIES_FTS_TRIGGER_AI_SQL,
|
|
1389
|
+
CREATE_ENTRIES_FTS_TRIGGER_AD_SQL,
|
|
1390
|
+
CREATE_ENTRIES_FTS_TRIGGER_AU_SQL
|
|
1391
|
+
];
|
|
1392
|
+
var COLUMN_MIGRATIONS = [
|
|
1393
|
+
{
|
|
1394
|
+
table: "co_recall_edges",
|
|
1395
|
+
column: "edge_type",
|
|
1396
|
+
sql: "ALTER TABLE co_recall_edges ADD COLUMN edge_type TEXT NOT NULL DEFAULT 'co_recalled'"
|
|
1397
|
+
},
|
|
1398
|
+
{
|
|
1399
|
+
table: "entries",
|
|
1400
|
+
column: "importance",
|
|
1401
|
+
sql: "ALTER TABLE entries ADD COLUMN importance INTEGER NOT NULL DEFAULT 5"
|
|
1402
|
+
},
|
|
1403
|
+
{
|
|
1404
|
+
table: "entries",
|
|
1405
|
+
column: "canonical_key",
|
|
1406
|
+
sql: "ALTER TABLE entries ADD COLUMN canonical_key TEXT"
|
|
1407
|
+
},
|
|
1408
|
+
{
|
|
1409
|
+
table: "entries",
|
|
1410
|
+
column: "scope",
|
|
1411
|
+
sql: "ALTER TABLE entries ADD COLUMN scope TEXT DEFAULT 'private'"
|
|
1412
|
+
},
|
|
1413
|
+
{
|
|
1414
|
+
table: "entries",
|
|
1415
|
+
column: "content_hash",
|
|
1416
|
+
sql: "ALTER TABLE entries ADD COLUMN content_hash TEXT"
|
|
1417
|
+
},
|
|
1418
|
+
{
|
|
1419
|
+
table: "entries",
|
|
1420
|
+
column: "norm_content_hash",
|
|
1421
|
+
sql: "ALTER TABLE entries ADD COLUMN norm_content_hash TEXT"
|
|
1422
|
+
},
|
|
1423
|
+
{
|
|
1424
|
+
table: "entries",
|
|
1425
|
+
column: "idx_entries_norm_content_hash",
|
|
1426
|
+
sql: "CREATE INDEX IF NOT EXISTS idx_entries_norm_content_hash ON entries(norm_content_hash)",
|
|
1427
|
+
isIndex: true
|
|
1428
|
+
},
|
|
1429
|
+
{
|
|
1430
|
+
table: "entries",
|
|
1431
|
+
column: "minhash_sig",
|
|
1432
|
+
sql: "ALTER TABLE entries ADD COLUMN minhash_sig BLOB"
|
|
1433
|
+
},
|
|
1434
|
+
{
|
|
1435
|
+
table: "entries",
|
|
1436
|
+
column: "merged_from",
|
|
1437
|
+
sql: "ALTER TABLE entries ADD COLUMN merged_from INTEGER DEFAULT 0"
|
|
1438
|
+
},
|
|
1439
|
+
{
|
|
1440
|
+
table: "entries",
|
|
1441
|
+
column: "consolidated_at",
|
|
1442
|
+
sql: "ALTER TABLE entries ADD COLUMN consolidated_at TEXT"
|
|
1443
|
+
},
|
|
1444
|
+
{
|
|
1445
|
+
table: "entries",
|
|
1446
|
+
column: "platform",
|
|
1447
|
+
sql: "ALTER TABLE entries ADD COLUMN platform TEXT DEFAULT NULL"
|
|
1448
|
+
},
|
|
1449
|
+
{
|
|
1450
|
+
table: "entries",
|
|
1451
|
+
column: "project",
|
|
1452
|
+
sql: "ALTER TABLE entries ADD COLUMN project TEXT DEFAULT NULL"
|
|
1453
|
+
},
|
|
1454
|
+
{
|
|
1455
|
+
table: "entries",
|
|
1456
|
+
column: "retired",
|
|
1457
|
+
sql: "ALTER TABLE entries ADD COLUMN retired INTEGER NOT NULL DEFAULT 0"
|
|
1458
|
+
},
|
|
1459
|
+
{
|
|
1460
|
+
table: "entries",
|
|
1461
|
+
column: "idx_entries_retired",
|
|
1462
|
+
sql: "CREATE INDEX IF NOT EXISTS idx_entries_retired ON entries (retired) WHERE retired = 0",
|
|
1463
|
+
isIndex: true
|
|
1464
|
+
},
|
|
1465
|
+
{
|
|
1466
|
+
table: "entries",
|
|
1467
|
+
column: "retired_at",
|
|
1468
|
+
sql: "ALTER TABLE entries ADD COLUMN retired_at TEXT"
|
|
1469
|
+
},
|
|
1470
|
+
{
|
|
1471
|
+
table: "entries",
|
|
1472
|
+
column: "retired_reason",
|
|
1473
|
+
sql: "ALTER TABLE entries ADD COLUMN retired_reason TEXT"
|
|
1474
|
+
},
|
|
1475
|
+
{
|
|
1476
|
+
table: "entries",
|
|
1477
|
+
column: "suppressed_contexts",
|
|
1478
|
+
sql: "ALTER TABLE entries ADD COLUMN suppressed_contexts TEXT"
|
|
1479
|
+
},
|
|
1480
|
+
{
|
|
1481
|
+
table: "ingest_log",
|
|
1482
|
+
column: "content_hash",
|
|
1483
|
+
sql: "ALTER TABLE ingest_log ADD COLUMN content_hash TEXT"
|
|
1484
|
+
},
|
|
1485
|
+
{
|
|
1486
|
+
table: "ingest_log",
|
|
1487
|
+
column: "entries_superseded",
|
|
1488
|
+
sql: "ALTER TABLE ingest_log ADD COLUMN entries_superseded INTEGER NOT NULL DEFAULT 0"
|
|
1489
|
+
},
|
|
1490
|
+
{
|
|
1491
|
+
table: "ingest_log",
|
|
1492
|
+
column: "dedup_llm_calls",
|
|
1493
|
+
sql: "ALTER TABLE ingest_log ADD COLUMN dedup_llm_calls INTEGER NOT NULL DEFAULT 0"
|
|
1494
|
+
},
|
|
1495
|
+
{
|
|
1496
|
+
table: "entry_sources",
|
|
1497
|
+
column: "original_created_at",
|
|
1498
|
+
sql: "ALTER TABLE entry_sources ADD COLUMN original_created_at TEXT"
|
|
1499
|
+
},
|
|
1500
|
+
{
|
|
1501
|
+
table: "entries",
|
|
1502
|
+
column: "recall_intervals",
|
|
1503
|
+
sql: "ALTER TABLE entries ADD COLUMN recall_intervals TEXT DEFAULT NULL"
|
|
1504
|
+
},
|
|
1505
|
+
{
|
|
1506
|
+
table: "entries",
|
|
1507
|
+
column: "quality_score",
|
|
1508
|
+
sql: "ALTER TABLE entries ADD COLUMN quality_score REAL NOT NULL DEFAULT 0.5"
|
|
1509
|
+
},
|
|
1510
|
+
// Claim/subject fields for contradiction detection (#266)
|
|
1511
|
+
{
|
|
1512
|
+
table: "entries",
|
|
1513
|
+
column: "subject_entity",
|
|
1514
|
+
sql: "ALTER TABLE entries ADD COLUMN subject_entity TEXT"
|
|
1515
|
+
},
|
|
1516
|
+
{
|
|
1517
|
+
table: "entries",
|
|
1518
|
+
column: "subject_attribute",
|
|
1519
|
+
sql: "ALTER TABLE entries ADD COLUMN subject_attribute TEXT"
|
|
1520
|
+
},
|
|
1521
|
+
{
|
|
1522
|
+
table: "entries",
|
|
1523
|
+
column: "subject_key",
|
|
1524
|
+
sql: "ALTER TABLE entries ADD COLUMN subject_key TEXT"
|
|
1525
|
+
},
|
|
1526
|
+
{
|
|
1527
|
+
table: "entries",
|
|
1528
|
+
column: "claim_predicate",
|
|
1529
|
+
sql: "ALTER TABLE entries ADD COLUMN claim_predicate TEXT"
|
|
1530
|
+
},
|
|
1531
|
+
{
|
|
1532
|
+
table: "entries",
|
|
1533
|
+
column: "claim_object",
|
|
1534
|
+
sql: "ALTER TABLE entries ADD COLUMN claim_object TEXT"
|
|
1535
|
+
},
|
|
1536
|
+
{
|
|
1537
|
+
table: "entries",
|
|
1538
|
+
column: "claim_confidence",
|
|
1539
|
+
sql: "ALTER TABLE entries ADD COLUMN claim_confidence REAL"
|
|
1540
|
+
},
|
|
1541
|
+
{
|
|
1542
|
+
table: "entries",
|
|
1543
|
+
column: "idx_entries_subject_key",
|
|
1544
|
+
sql: "CREATE INDEX IF NOT EXISTS idx_entries_subject_key ON entries(subject_key) WHERE subject_key IS NOT NULL AND retired = 0 AND superseded_by IS NULL",
|
|
1545
|
+
isIndex: true
|
|
1546
|
+
}
|
|
1547
|
+
];
|
|
1548
|
+
var CREATE_INDEX_STATEMENTS = [
|
|
1549
|
+
CREATE_IDX_ENTRIES_EMBEDDING_SQL,
|
|
1550
|
+
"CREATE INDEX IF NOT EXISTS idx_entries_type ON entries(type)",
|
|
1551
|
+
"CREATE INDEX IF NOT EXISTS idx_entries_type_canonical_key ON entries(type, canonical_key)",
|
|
1552
|
+
"CREATE INDEX IF NOT EXISTS idx_entries_expiry ON entries(expiry)",
|
|
1553
|
+
"CREATE INDEX IF NOT EXISTS idx_entries_scope ON entries(scope)",
|
|
1554
|
+
"CREATE INDEX IF NOT EXISTS idx_entries_platform ON entries(platform)",
|
|
1555
|
+
"CREATE INDEX IF NOT EXISTS idx_entries_project ON entries(project)",
|
|
1556
|
+
"CREATE INDEX IF NOT EXISTS idx_entries_created ON entries(created_at)",
|
|
1557
|
+
"CREATE INDEX IF NOT EXISTS idx_entries_superseded ON entries(superseded_by)",
|
|
1558
|
+
"CREATE INDEX IF NOT EXISTS idx_entries_content_hash ON entries(content_hash)",
|
|
1559
|
+
"CREATE INDEX IF NOT EXISTS idx_tags_tag ON tags(tag)",
|
|
1560
|
+
"CREATE INDEX IF NOT EXISTS idx_relations_source ON relations(source_id)",
|
|
1561
|
+
"CREATE INDEX IF NOT EXISTS idx_relations_target ON relations(target_id)",
|
|
1562
|
+
"CREATE UNIQUE INDEX IF NOT EXISTS idx_ingest_log_file_hash ON ingest_log(file_path, content_hash)"
|
|
1563
|
+
];
|
|
1564
|
+
async function dropFtsTriggersAndIndex(db) {
|
|
1565
|
+
await db.execute("BEGIN IMMEDIATE");
|
|
1566
|
+
try {
|
|
1567
|
+
await db.execute("DROP TRIGGER IF EXISTS entries_ai");
|
|
1568
|
+
await db.execute("DROP TRIGGER IF EXISTS entries_ad");
|
|
1569
|
+
await db.execute("DROP TRIGGER IF EXISTS entries_au");
|
|
1570
|
+
await db.execute("DROP INDEX IF EXISTS idx_entries_embedding");
|
|
1571
|
+
await db.execute("COMMIT");
|
|
1572
|
+
} catch (error) {
|
|
1573
|
+
try {
|
|
1574
|
+
await db.execute("ROLLBACK");
|
|
1575
|
+
} catch {
|
|
1576
|
+
}
|
|
1577
|
+
throw error;
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
async function rebuildFtsAndTriggers(db) {
|
|
1581
|
+
await db.execute("BEGIN IMMEDIATE");
|
|
1582
|
+
try {
|
|
1583
|
+
await db.execute(REBUILD_ENTRIES_FTS_SQL);
|
|
1584
|
+
await db.execute(CREATE_ENTRIES_FTS_TRIGGER_AI_SQL);
|
|
1585
|
+
await db.execute(CREATE_ENTRIES_FTS_TRIGGER_AD_SQL);
|
|
1586
|
+
await db.execute(CREATE_ENTRIES_FTS_TRIGGER_AU_SQL);
|
|
1587
|
+
await db.execute("COMMIT");
|
|
1588
|
+
} catch (error) {
|
|
1589
|
+
try {
|
|
1590
|
+
await db.execute("ROLLBACK");
|
|
1591
|
+
} catch {
|
|
1592
|
+
}
|
|
1593
|
+
throw error;
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
async function rebuildVectorIndex(db) {
|
|
1597
|
+
try {
|
|
1598
|
+
await db.execute("REINDEX idx_entries_embedding");
|
|
1599
|
+
return;
|
|
1600
|
+
} catch {
|
|
1601
|
+
}
|
|
1602
|
+
await db.execute("BEGIN IMMEDIATE");
|
|
1603
|
+
try {
|
|
1604
|
+
await db.execute("DROP INDEX IF EXISTS idx_entries_embedding");
|
|
1605
|
+
await db.execute(CREATE_IDX_ENTRIES_EMBEDDING_SQL);
|
|
1606
|
+
await db.execute("COMMIT");
|
|
1607
|
+
} catch (fallbackError) {
|
|
1608
|
+
try {
|
|
1609
|
+
await db.execute("ROLLBACK");
|
|
1610
|
+
} catch {
|
|
1611
|
+
}
|
|
1612
|
+
throw fallbackError;
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
async function setBulkIngestMeta(db, phase) {
|
|
1616
|
+
await db.execute({
|
|
1617
|
+
sql: `
|
|
1618
|
+
INSERT OR REPLACE INTO _meta (key, value, updated_at)
|
|
1619
|
+
VALUES (?, json_object('phase', ?, 'started_at', datetime('now')), datetime('now'))
|
|
1620
|
+
`,
|
|
1621
|
+
args: [BULK_INGEST_META_KEY, phase]
|
|
1622
|
+
});
|
|
1623
|
+
}
|
|
1624
|
+
async function clearBulkIngestMeta(db) {
|
|
1625
|
+
await db.execute({ sql: "DELETE FROM _meta WHERE key = ?", args: [BULK_INGEST_META_KEY] });
|
|
1626
|
+
}
|
|
1627
|
+
async function getBulkIngestMeta(db) {
|
|
1628
|
+
const result = await db.execute({ sql: "SELECT value FROM _meta WHERE key = ?", args: [BULK_INGEST_META_KEY] });
|
|
1629
|
+
if (result.rows.length === 0) {
|
|
1630
|
+
return null;
|
|
1631
|
+
}
|
|
1632
|
+
try {
|
|
1633
|
+
const row = result.rows[0];
|
|
1634
|
+
const raw = row?.value ?? (row ? Object.values(row)[0] : void 0);
|
|
1635
|
+
return JSON.parse(String(raw));
|
|
1636
|
+
} catch (parseError) {
|
|
1637
|
+
process.stderr.write(
|
|
1638
|
+
`[agenr] Warning: failed to parse bulk_ingest_state metadata: ${parseError instanceof Error ? parseError.message : String(parseError)}. Crash recovery may be skipped.
|
|
1639
|
+
`
|
|
1640
|
+
);
|
|
1641
|
+
return null;
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
async function initSchema(client) {
|
|
1645
|
+
for (const statement of CREATE_TABLE_AND_TRIGGER_STATEMENTS) {
|
|
1646
|
+
await client.execute(statement);
|
|
1647
|
+
}
|
|
1648
|
+
let willRunLegacyBackfill = false;
|
|
1649
|
+
try {
|
|
1650
|
+
const earlyEntriesInfo = await client.execute("PRAGMA table_info(entries)");
|
|
1651
|
+
const earlyColumns = new Set(earlyEntriesInfo.rows.map((row) => String(row.name)));
|
|
1652
|
+
if (earlyColumns.has("confidence")) {
|
|
1653
|
+
const sentinel = await client.execute({
|
|
1654
|
+
sql: "SELECT 1 AS found FROM _meta WHERE key = ? LIMIT 1",
|
|
1655
|
+
args: [LEGACY_IMPORTANCE_BACKFILL_META_KEY]
|
|
1656
|
+
});
|
|
1657
|
+
const alreadyBackfilled = sentinel.rows.length > 0;
|
|
1658
|
+
willRunLegacyBackfill = !alreadyBackfilled;
|
|
1659
|
+
}
|
|
1660
|
+
} catch {
|
|
1661
|
+
}
|
|
1662
|
+
try {
|
|
1663
|
+
const entriesCountResult = await client.execute("SELECT COUNT(*) AS count FROM entries");
|
|
1664
|
+
const entriesCount = Number(entriesCountResult.rows[0]?.count ?? 0);
|
|
1665
|
+
const ftsCountResult = await client.execute("SELECT COUNT(*) AS count FROM entries_fts");
|
|
1666
|
+
const ftsCount = Number(ftsCountResult.rows[0]?.count ?? 0);
|
|
1667
|
+
if (entriesCount > 0 && ftsCount === 0 && !willRunLegacyBackfill) {
|
|
1668
|
+
await client.execute(REBUILD_ENTRIES_FTS_SQL);
|
|
1669
|
+
}
|
|
1670
|
+
} catch {
|
|
1671
|
+
}
|
|
1672
|
+
for (const migration of COLUMN_MIGRATIONS) {
|
|
1673
|
+
if (migration.isIndex) {
|
|
1674
|
+
const existingIndex = await client.execute({
|
|
1675
|
+
sql: "SELECT name FROM sqlite_master WHERE type='index' AND name=?",
|
|
1676
|
+
args: [migration.column]
|
|
1677
|
+
});
|
|
1678
|
+
if (existingIndex.rows.length > 0) {
|
|
1679
|
+
continue;
|
|
1680
|
+
}
|
|
1681
|
+
await client.execute(migration.sql);
|
|
1682
|
+
continue;
|
|
1683
|
+
}
|
|
1684
|
+
const info = await client.execute(`PRAGMA table_info(${migration.table})`);
|
|
1685
|
+
const hasColumn = info.rows.some((row) => String(row.name) === migration.column);
|
|
1686
|
+
if (!hasColumn) {
|
|
1687
|
+
await client.execute(migration.sql);
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
const entriesInfo = await client.execute("PRAGMA table_info(entries)");
|
|
1691
|
+
const entryColumns = new Set(entriesInfo.rows.map((row) => String(row.name)));
|
|
1692
|
+
const backfillSentinel = await client.execute({
|
|
1693
|
+
sql: "SELECT 1 AS found FROM _meta WHERE key = ? LIMIT 1",
|
|
1694
|
+
args: [LEGACY_IMPORTANCE_BACKFILL_META_KEY]
|
|
1695
|
+
});
|
|
1696
|
+
const legacyBackfillDone = backfillSentinel.rows.length > 0;
|
|
1697
|
+
if (entryColumns.has("confidence") && entryColumns.has("importance") && !legacyBackfillDone) {
|
|
1698
|
+
try {
|
|
1699
|
+
await client.execute("DROP TRIGGER IF EXISTS entries_ai");
|
|
1700
|
+
await client.execute("DROP TRIGGER IF EXISTS entries_ad");
|
|
1701
|
+
await client.execute("DROP TRIGGER IF EXISTS entries_au");
|
|
1702
|
+
await client.execute("DROP TABLE IF EXISTS entries_fts");
|
|
1703
|
+
} catch {
|
|
1704
|
+
}
|
|
1705
|
+
await client.execute(`
|
|
1706
|
+
UPDATE entries
|
|
1707
|
+
SET importance = CASE lower(trim(confidence))
|
|
1708
|
+
WHEN 'low' THEN 3
|
|
1709
|
+
WHEN 'medium' THEN 6
|
|
1710
|
+
WHEN 'high' THEN 8
|
|
1711
|
+
ELSE
|
|
1712
|
+
CASE
|
|
1713
|
+
WHEN CAST(confidence AS INTEGER) BETWEEN 1 AND 10 THEN CAST(confidence AS INTEGER)
|
|
1714
|
+
ELSE 5
|
|
1715
|
+
END
|
|
1716
|
+
END
|
|
1717
|
+
WHERE importance = 5
|
|
1718
|
+
`);
|
|
1719
|
+
try {
|
|
1720
|
+
await client.execute(CREATE_ENTRIES_FTS_TABLE_SQL);
|
|
1721
|
+
await client.execute(CREATE_ENTRIES_FTS_TRIGGER_AI_SQL);
|
|
1722
|
+
await client.execute(CREATE_ENTRIES_FTS_TRIGGER_AD_SQL);
|
|
1723
|
+
await client.execute(CREATE_ENTRIES_FTS_TRIGGER_AU_SQL);
|
|
1724
|
+
await client.execute(REBUILD_ENTRIES_FTS_SQL);
|
|
1725
|
+
} catch {
|
|
1726
|
+
}
|
|
1727
|
+
try {
|
|
1728
|
+
await client.execute({
|
|
1729
|
+
sql: `
|
|
1730
|
+
INSERT INTO _meta (key, value, updated_at)
|
|
1731
|
+
VALUES (?, datetime('now'), datetime('now'))
|
|
1732
|
+
ON CONFLICT(key) DO NOTHING
|
|
1733
|
+
`,
|
|
1734
|
+
args: [LEGACY_IMPORTANCE_BACKFILL_META_KEY]
|
|
1735
|
+
});
|
|
1736
|
+
} catch {
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
for (const statement of CREATE_INDEX_STATEMENTS) {
|
|
1740
|
+
await client.execute(statement);
|
|
1741
|
+
}
|
|
1742
|
+
await client.execute({
|
|
1743
|
+
sql: `
|
|
1744
|
+
INSERT INTO _meta (key, value, updated_at)
|
|
1745
|
+
VALUES ('db_created_at', datetime('now'), datetime('now'))
|
|
1746
|
+
ON CONFLICT(key) DO NOTHING
|
|
1747
|
+
`,
|
|
1748
|
+
args: []
|
|
1749
|
+
});
|
|
1750
|
+
await client.execute({
|
|
1751
|
+
sql: `
|
|
1752
|
+
INSERT INTO _meta (key, value, updated_at)
|
|
1753
|
+
VALUES ('schema_version', ?, datetime('now'))
|
|
1754
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at
|
|
1755
|
+
`,
|
|
1756
|
+
args: [APP_VERSION]
|
|
1757
|
+
});
|
|
1758
|
+
}
|
|
1759
|
+
async function resetDb(db) {
|
|
1760
|
+
const foreignKeysResult = await db.execute("PRAGMA foreign_keys");
|
|
1761
|
+
const foreignKeysRow = foreignKeysResult.rows[0];
|
|
1762
|
+
const previousForeignKeys = Number(foreignKeysRow?.foreign_keys ?? (foreignKeysRow ? Object.values(foreignKeysRow)[0] : 1)) === 0 ? 0 : 1;
|
|
1763
|
+
await db.execute("PRAGMA foreign_keys=OFF");
|
|
1764
|
+
try {
|
|
1765
|
+
const schemaObjects = await db.execute(`
|
|
1766
|
+
SELECT type, name
|
|
1767
|
+
FROM sqlite_master
|
|
1768
|
+
WHERE name NOT LIKE 'sqlite_%'
|
|
1769
|
+
ORDER BY
|
|
1770
|
+
CASE type
|
|
1771
|
+
WHEN 'trigger' THEN 1
|
|
1772
|
+
WHEN 'index' THEN 2
|
|
1773
|
+
WHEN 'table' THEN 3
|
|
1774
|
+
ELSE 4
|
|
1775
|
+
END,
|
|
1776
|
+
name
|
|
1777
|
+
`);
|
|
1778
|
+
for (const row of schemaObjects.rows) {
|
|
1779
|
+
const type = String(row.type ?? "");
|
|
1780
|
+
const name = String(row.name ?? "");
|
|
1781
|
+
if (!type || !name) {
|
|
1782
|
+
continue;
|
|
1783
|
+
}
|
|
1784
|
+
const safeName = name.replace(/"/g, '""');
|
|
1785
|
+
if (type === "trigger") {
|
|
1786
|
+
await db.execute(`DROP TRIGGER IF EXISTS "${safeName}"`);
|
|
1787
|
+
continue;
|
|
1788
|
+
}
|
|
1789
|
+
if (type === "index") {
|
|
1790
|
+
await db.execute(`DROP INDEX IF EXISTS "${safeName}"`);
|
|
1791
|
+
continue;
|
|
1792
|
+
}
|
|
1793
|
+
if (type === "table") {
|
|
1794
|
+
await db.execute(`DROP TABLE IF EXISTS "${safeName}"`);
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
await initSchema(db);
|
|
1798
|
+
} finally {
|
|
1799
|
+
await db.execute(`PRAGMA foreign_keys=${previousForeignKeys === 0 ? "OFF" : "ON"}`);
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
// src/db/client.ts
|
|
1804
|
+
var DEFAULT_DB_PATH = path3.join(os3.homedir(), ".agenr", "knowledge.db");
|
|
1805
|
+
var walInitByClient = /* @__PURE__ */ new WeakMap();
|
|
1806
|
+
var didWarnVectorIndexCorruption = false;
|
|
1807
|
+
var WAL_CHECKPOINT_MAX_ATTEMPTS = 5;
|
|
1808
|
+
var WAL_CHECKPOINT_RETRY_MS = 50;
|
|
1809
|
+
function resolveUserPath3(inputPath) {
|
|
1810
|
+
if (!inputPath.startsWith("~")) {
|
|
1811
|
+
return inputPath;
|
|
1812
|
+
}
|
|
1813
|
+
return path3.join(os3.homedir(), inputPath.slice(1));
|
|
1814
|
+
}
|
|
1815
|
+
function resolveDbPath(dbPath) {
|
|
1816
|
+
if (dbPath === ":memory:") {
|
|
1817
|
+
return dbPath;
|
|
1818
|
+
}
|
|
1819
|
+
return resolveUserPath3(dbPath);
|
|
1820
|
+
}
|
|
1821
|
+
function normalizeBackupSourcePath(dbPath) {
|
|
1822
|
+
const strippedDbPath = dbPath.startsWith("file:") ? dbPath.slice("file:".length) : dbPath;
|
|
1823
|
+
return path3.resolve(resolveDbPath(strippedDbPath));
|
|
1824
|
+
}
|
|
1825
|
+
function isErrnoCode(error, code) {
|
|
1826
|
+
return error?.code === code;
|
|
1827
|
+
}
|
|
1828
|
+
async function copySidecarIfPresent(sourcePath, targetPath) {
|
|
1829
|
+
try {
|
|
1830
|
+
await fs3.copyFile(sourcePath, targetPath);
|
|
1831
|
+
} catch (error) {
|
|
1832
|
+
if (isErrnoCode(error, "ENOENT")) {
|
|
1833
|
+
return;
|
|
1834
|
+
}
|
|
1835
|
+
throw error;
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
function buildBackupPath(dbPath) {
|
|
1839
|
+
const resolvedDbPath = normalizeBackupSourcePath(dbPath);
|
|
1840
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").replace(/Z$/, "");
|
|
1841
|
+
return path3.join(
|
|
1842
|
+
path3.dirname(resolvedDbPath),
|
|
1843
|
+
`${path3.basename(resolvedDbPath)}.backup-pre-reset-${timestamp}Z`
|
|
1844
|
+
);
|
|
1845
|
+
}
|
|
1846
|
+
function getDb(dbPath) {
|
|
1847
|
+
const rawPath = dbPath?.trim() ? dbPath.trim() : DEFAULT_DB_PATH;
|
|
1848
|
+
if (rawPath === ":memory:") {
|
|
1849
|
+
return createClient({ url: ":memory:" });
|
|
1850
|
+
}
|
|
1851
|
+
if (rawPath.startsWith("file:")) {
|
|
1852
|
+
const client2 = createClient({ url: rawPath });
|
|
1853
|
+
walInitByClient.set(
|
|
1854
|
+
client2,
|
|
1855
|
+
client2.execute("PRAGMA journal_mode=WAL").then(() => client2.execute("PRAGMA busy_timeout=3000")).then(() => void 0)
|
|
1856
|
+
);
|
|
1857
|
+
return client2;
|
|
1858
|
+
}
|
|
1859
|
+
const resolvedPath = resolveDbPath(rawPath);
|
|
1860
|
+
const client = createClient({ url: `file:${resolvedPath}` });
|
|
1861
|
+
walInitByClient.set(
|
|
1862
|
+
client,
|
|
1863
|
+
client.execute("PRAGMA journal_mode=WAL").then(() => client.execute("PRAGMA busy_timeout=3000")).then(() => void 0)
|
|
1864
|
+
);
|
|
1865
|
+
return client;
|
|
1866
|
+
}
|
|
1867
|
+
function readCount(row, key = "cnt") {
|
|
1868
|
+
if (!row || typeof row !== "object") {
|
|
1869
|
+
return 0;
|
|
1870
|
+
}
|
|
1871
|
+
const record = row;
|
|
1872
|
+
const raw = record[key] ?? Object.values(record)[0];
|
|
1873
|
+
return toFiniteNumber(raw) ?? 0;
|
|
1874
|
+
}
|
|
1875
|
+
function readIntegrityValue(row) {
|
|
1876
|
+
if (!row || typeof row !== "object") {
|
|
1877
|
+
return "";
|
|
1878
|
+
}
|
|
1879
|
+
const record = row;
|
|
1880
|
+
const raw = record.integrity_check ?? Object.values(record)[0];
|
|
1881
|
+
return String(raw ?? "").trim();
|
|
1882
|
+
}
|
|
1883
|
+
async function checkAndRecoverBulkIngest(client) {
|
|
1884
|
+
const meta = await getBulkIngestMeta(client);
|
|
1885
|
+
if (!meta) {
|
|
1886
|
+
return;
|
|
1887
|
+
}
|
|
1888
|
+
try {
|
|
1889
|
+
const triggerCountResult = await client.execute(`
|
|
1890
|
+
SELECT COUNT(*) AS cnt
|
|
1891
|
+
FROM sqlite_master
|
|
1892
|
+
WHERE type = 'trigger' AND name IN ('entries_ai', 'entries_ad', 'entries_au')
|
|
1893
|
+
`);
|
|
1894
|
+
const triggerCount = readCount(triggerCountResult.rows[0]);
|
|
1895
|
+
const indexCountResult = await client.execute(`
|
|
1896
|
+
SELECT COUNT(*) AS cnt
|
|
1897
|
+
FROM sqlite_master
|
|
1898
|
+
WHERE type = 'index' AND name = 'idx_entries_embedding'
|
|
1899
|
+
`);
|
|
1900
|
+
const indexCount = readCount(indexCountResult.rows[0]);
|
|
1901
|
+
process.stderr.write(
|
|
1902
|
+
`[agenr] Interrupted bulk ingest detected (phase: ${meta.phase}). Recovering...
|
|
1903
|
+
`
|
|
1904
|
+
);
|
|
1905
|
+
if (triggerCount < 3) {
|
|
1906
|
+
await rebuildFtsAndTriggers(client);
|
|
1907
|
+
} else if (indexCount < 1) {
|
|
1908
|
+
const entriesCountResult = await client.execute("SELECT COUNT(*) AS cnt FROM entries");
|
|
1909
|
+
const ftsCountResult = await client.execute("SELECT COUNT(*) AS cnt FROM entries_fts");
|
|
1910
|
+
const entriesCount = readCount(entriesCountResult.rows[0]);
|
|
1911
|
+
const ftsCount = readCount(ftsCountResult.rows[0]);
|
|
1912
|
+
if (entriesCount > 0 && ftsCount === 0) {
|
|
1913
|
+
await client.execute("INSERT INTO entries_fts(entries_fts) VALUES('rebuild')");
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
if (indexCount < 1) {
|
|
1917
|
+
await rebuildVectorIndex(client);
|
|
1918
|
+
}
|
|
1919
|
+
try {
|
|
1920
|
+
const integrityResult = await client.execute("PRAGMA integrity_check");
|
|
1921
|
+
const integrity = readIntegrityValue(integrityResult.rows[0]);
|
|
1922
|
+
process.stderr.write(`[agenr] integrity_check: ${integrity || "unknown"}
|
|
1923
|
+
`);
|
|
1924
|
+
} catch (integrityError) {
|
|
1925
|
+
process.stderr.write(
|
|
1926
|
+
`[agenr] integrity_check failed: ${integrityError instanceof Error ? integrityError.message : String(integrityError)}
|
|
1927
|
+
`
|
|
1928
|
+
);
|
|
1929
|
+
}
|
|
1930
|
+
await clearBulkIngestMeta(client);
|
|
1931
|
+
process.stderr.write("[agenr] Recovery complete.\n");
|
|
1932
|
+
} catch (error) {
|
|
1933
|
+
throw error;
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
async function initDb(client, opts) {
|
|
1937
|
+
const walInit = walInitByClient.get(client);
|
|
1938
|
+
if (walInit) {
|
|
1939
|
+
await walInit;
|
|
1940
|
+
await client.execute("PRAGMA wal_autocheckpoint=1000");
|
|
1941
|
+
}
|
|
1942
|
+
await initSchema(client);
|
|
1943
|
+
if (opts?.checkBulkRecovery === true) {
|
|
1944
|
+
try {
|
|
1945
|
+
await checkAndRecoverBulkIngest(client);
|
|
1946
|
+
} catch (recoveryError) {
|
|
1947
|
+
process.stderr.write(
|
|
1948
|
+
`[agenr] Warning: bulk ingest recovery failed: ${recoveryError instanceof Error ? recoveryError.stack ?? recoveryError.message : String(recoveryError)}
|
|
1949
|
+
[agenr] Continuing without recovery. Run 'agenr ingest --bulk' again if FTS or vector search is degraded.
|
|
1950
|
+
`
|
|
1951
|
+
);
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
try {
|
|
1955
|
+
const hasEntries = await client.execute(
|
|
1956
|
+
"SELECT 1 FROM entries WHERE embedding IS NOT NULL LIMIT 1"
|
|
1957
|
+
);
|
|
1958
|
+
if (hasEntries.rows.length > 0) {
|
|
1959
|
+
await client.execute(`
|
|
1960
|
+
SELECT count(*) FROM vector_top_k(
|
|
1961
|
+
'idx_entries_embedding',
|
|
1962
|
+
(SELECT embedding FROM entries WHERE embedding IS NOT NULL LIMIT 1),
|
|
1963
|
+
1
|
|
1964
|
+
)
|
|
1965
|
+
`);
|
|
1966
|
+
}
|
|
1967
|
+
} catch {
|
|
1968
|
+
if (!didWarnVectorIndexCorruption) {
|
|
1969
|
+
didWarnVectorIndexCorruption = true;
|
|
1970
|
+
process.stderr.write(
|
|
1971
|
+
"\n\u26A0\uFE0F Vector index may be corrupted. Run `agenr db rebuild-index` to fix.\n\n"
|
|
1972
|
+
);
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
function toFiniteNumber(value) {
|
|
1977
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
1978
|
+
return value;
|
|
1979
|
+
}
|
|
1980
|
+
if (typeof value === "bigint") {
|
|
1981
|
+
return Number(value);
|
|
1982
|
+
}
|
|
1983
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
1984
|
+
const parsed = Number(value);
|
|
1985
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
1986
|
+
}
|
|
1987
|
+
return null;
|
|
1988
|
+
}
|
|
1989
|
+
function extractBusyFromCheckpointRow(row) {
|
|
1990
|
+
if (!row || typeof row !== "object") {
|
|
1991
|
+
return null;
|
|
1992
|
+
}
|
|
1993
|
+
const record = row;
|
|
1994
|
+
const busyValue = record.busy ?? Object.values(record)[0];
|
|
1995
|
+
return toFiniteNumber(busyValue);
|
|
1996
|
+
}
|
|
1997
|
+
async function validatedWalCheckpoint(client) {
|
|
1998
|
+
for (let attempt = 1; attempt <= WAL_CHECKPOINT_MAX_ATTEMPTS; attempt += 1) {
|
|
1999
|
+
const result = await client.execute("PRAGMA wal_checkpoint(TRUNCATE)");
|
|
2000
|
+
const busy = extractBusyFromCheckpointRow(result.rows[0]);
|
|
2001
|
+
if (busy === null) {
|
|
2002
|
+
throw new Error("WAL checkpoint returned an unexpected result and could not be validated.");
|
|
2003
|
+
}
|
|
2004
|
+
if (busy === 0) {
|
|
2005
|
+
return;
|
|
2006
|
+
}
|
|
2007
|
+
if (attempt < WAL_CHECKPOINT_MAX_ATTEMPTS) {
|
|
2008
|
+
await new Promise((resolve) => setTimeout(resolve, WAL_CHECKPOINT_RETRY_MS * attempt));
|
|
2009
|
+
continue;
|
|
2010
|
+
}
|
|
2011
|
+
throw new Error(
|
|
2012
|
+
`WAL checkpoint did not finish (busy=${busy}). Active readers are blocking backup safety.`
|
|
2013
|
+
);
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
async function walCheckpoint(client) {
|
|
2017
|
+
await validatedWalCheckpoint(client);
|
|
2018
|
+
}
|
|
2019
|
+
async function backupDb(dbPath) {
|
|
2020
|
+
if (dbPath === ":memory:") {
|
|
2021
|
+
throw new Error("Cannot back up in-memory databases.");
|
|
2022
|
+
}
|
|
2023
|
+
const checkpointClient = getDb(dbPath);
|
|
2024
|
+
try {
|
|
2025
|
+
await walCheckpoint(checkpointClient);
|
|
2026
|
+
} finally {
|
|
2027
|
+
closeDb(checkpointClient);
|
|
2028
|
+
}
|
|
2029
|
+
const resolvedDbPath = normalizeBackupSourcePath(dbPath);
|
|
2030
|
+
const backupPath = buildBackupPath(dbPath);
|
|
2031
|
+
await fs3.copyFile(resolvedDbPath, backupPath);
|
|
2032
|
+
await copySidecarIfPresent(`${resolvedDbPath}-wal`, `${backupPath}-wal`);
|
|
2033
|
+
await copySidecarIfPresent(`${resolvedDbPath}-shm`, `${backupPath}-shm`);
|
|
2034
|
+
return backupPath;
|
|
2035
|
+
}
|
|
2036
|
+
function closeDb(client) {
|
|
2037
|
+
client.close();
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
// src/db/feedback.ts
|
|
2041
|
+
var RESPONSE_MIN_CHARS = 50;
|
|
2042
|
+
var RESPONSE_MAX_CHARS = 8e3;
|
|
2043
|
+
var SIGNAL_USED = 1;
|
|
2044
|
+
var SIGNAL_UNCLEAR = 0.4;
|
|
2045
|
+
var SIGNAL_CORRECTED = 0;
|
|
2046
|
+
var RESPONSE_USED_THRESHOLD = 0.5;
|
|
2047
|
+
var CORRECTION_THRESHOLD = 0.6;
|
|
2048
|
+
function isRecord(value) {
|
|
2049
|
+
return typeof value === "object" && value !== null;
|
|
2050
|
+
}
|
|
2051
|
+
function clamp01(value) {
|
|
2052
|
+
if (!Number.isFinite(value)) {
|
|
2053
|
+
return 0;
|
|
2054
|
+
}
|
|
2055
|
+
if (value <= 0) {
|
|
2056
|
+
return 0;
|
|
2057
|
+
}
|
|
2058
|
+
if (value >= 1) {
|
|
2059
|
+
return 1;
|
|
2060
|
+
}
|
|
2061
|
+
return value;
|
|
2062
|
+
}
|
|
2063
|
+
function clamp(value, min, max) {
|
|
2064
|
+
if (!Number.isFinite(value)) {
|
|
2065
|
+
return min;
|
|
2066
|
+
}
|
|
2067
|
+
if (value <= min) {
|
|
2068
|
+
return min;
|
|
2069
|
+
}
|
|
2070
|
+
if (value >= max) {
|
|
2071
|
+
return max;
|
|
2072
|
+
}
|
|
2073
|
+
return value;
|
|
2074
|
+
}
|
|
2075
|
+
function qualityFloorByType(type) {
|
|
2076
|
+
if (type === "fact" || type === "preference") {
|
|
2077
|
+
return 0.35;
|
|
2078
|
+
}
|
|
2079
|
+
if (type === "decision" || type === "lesson") {
|
|
2080
|
+
return 0.25;
|
|
2081
|
+
}
|
|
2082
|
+
if (type === "todo" || type === "event" || type === "relationship") {
|
|
2083
|
+
return 0.15;
|
|
2084
|
+
}
|
|
2085
|
+
return 0.1;
|
|
2086
|
+
}
|
|
2087
|
+
function computeDecayPenalty(daysSinceLastRecall) {
|
|
2088
|
+
if (daysSinceLastRecall <= 60) {
|
|
2089
|
+
return 1;
|
|
2090
|
+
}
|
|
2091
|
+
return Math.max(0.3, 1 - (daysSinceLastRecall - 60) / 180);
|
|
2092
|
+
}
|
|
2093
|
+
function mapBufferToVector(value) {
|
|
2094
|
+
if (value instanceof ArrayBuffer) {
|
|
2095
|
+
return Array.from(new Float32Array(value));
|
|
2096
|
+
}
|
|
2097
|
+
if (ArrayBuffer.isView(value)) {
|
|
2098
|
+
const view = value;
|
|
2099
|
+
return Array.from(
|
|
2100
|
+
new Float32Array(view.buffer, view.byteOffset, Math.floor(view.byteLength / Float32Array.BYTES_PER_ELEMENT))
|
|
2101
|
+
);
|
|
2102
|
+
}
|
|
2103
|
+
return [];
|
|
2104
|
+
}
|
|
2105
|
+
function cosineSimilarity(a, b) {
|
|
2106
|
+
const size = Math.min(a.length, b.length);
|
|
2107
|
+
if (size === 0) {
|
|
2108
|
+
return 0;
|
|
2109
|
+
}
|
|
2110
|
+
let dot = 0;
|
|
2111
|
+
let normA = 0;
|
|
2112
|
+
let normB = 0;
|
|
2113
|
+
for (let i = 0; i < size; i += 1) {
|
|
2114
|
+
const av = a[i] ?? 0;
|
|
2115
|
+
const bv = b[i] ?? 0;
|
|
2116
|
+
dot += av * bv;
|
|
2117
|
+
normA += av * av;
|
|
2118
|
+
normB += bv * bv;
|
|
2119
|
+
}
|
|
2120
|
+
if (normA <= 0 || normB <= 0) {
|
|
2121
|
+
return 0;
|
|
2122
|
+
}
|
|
2123
|
+
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
|
|
2124
|
+
}
|
|
2125
|
+
function extractTextFromContent(content, separator) {
|
|
2126
|
+
if (typeof content === "string") {
|
|
2127
|
+
return content.trim();
|
|
2128
|
+
}
|
|
2129
|
+
if (!Array.isArray(content)) {
|
|
2130
|
+
return "";
|
|
2131
|
+
}
|
|
2132
|
+
const parts = [];
|
|
2133
|
+
for (const block of content) {
|
|
2134
|
+
if (!isRecord(block) || block.type !== "text") {
|
|
2135
|
+
continue;
|
|
2136
|
+
}
|
|
2137
|
+
const text = block.text;
|
|
2138
|
+
if (typeof text !== "string") {
|
|
2139
|
+
continue;
|
|
2140
|
+
}
|
|
2141
|
+
const trimmed = text.trim();
|
|
2142
|
+
if (trimmed) {
|
|
2143
|
+
parts.push(trimmed);
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
return parts.join(separator).trim();
|
|
2147
|
+
}
|
|
2148
|
+
function collectAssistantText(messages) {
|
|
2149
|
+
const parts = [];
|
|
2150
|
+
for (const message of messages) {
|
|
2151
|
+
if (!isRecord(message) || message.role !== "assistant") {
|
|
2152
|
+
continue;
|
|
2153
|
+
}
|
|
2154
|
+
const text = extractTextFromContent(message.content, "\n");
|
|
2155
|
+
if (text) {
|
|
2156
|
+
parts.push(text);
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
return parts.join("\n").trim();
|
|
2160
|
+
}
|
|
2161
|
+
function extractStoreContentsFromInput(input) {
|
|
2162
|
+
if (!isRecord(input)) {
|
|
2163
|
+
return [];
|
|
2164
|
+
}
|
|
2165
|
+
const out = [];
|
|
2166
|
+
const directContent = input.content;
|
|
2167
|
+
if (typeof directContent === "string" && directContent.trim()) {
|
|
2168
|
+
out.push(directContent.trim());
|
|
2169
|
+
}
|
|
2170
|
+
const entries = input.entries;
|
|
2171
|
+
if (Array.isArray(entries)) {
|
|
2172
|
+
for (const item of entries) {
|
|
2173
|
+
if (!isRecord(item)) {
|
|
2174
|
+
continue;
|
|
2175
|
+
}
|
|
2176
|
+
const content = item.content;
|
|
2177
|
+
if (typeof content === "string" && content.trim()) {
|
|
2178
|
+
out.push(content.trim());
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
return out;
|
|
2183
|
+
}
|
|
2184
|
+
function parseFunctionArguments(rawArgs) {
|
|
2185
|
+
if (isRecord(rawArgs)) {
|
|
2186
|
+
return rawArgs;
|
|
2187
|
+
}
|
|
2188
|
+
if (typeof rawArgs !== "string" || !rawArgs.trim()) {
|
|
2189
|
+
return null;
|
|
2190
|
+
}
|
|
2191
|
+
try {
|
|
2192
|
+
const parsed = JSON.parse(rawArgs);
|
|
2193
|
+
return isRecord(parsed) ? parsed : null;
|
|
2194
|
+
} catch {
|
|
2195
|
+
return null;
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
function collectAgenrStoreContents(messages) {
|
|
2199
|
+
const contents = [];
|
|
2200
|
+
for (const message of messages) {
|
|
2201
|
+
if (!isRecord(message) || message.role !== "assistant") {
|
|
2202
|
+
continue;
|
|
2203
|
+
}
|
|
2204
|
+
const contentBlocks = message.content;
|
|
2205
|
+
if (Array.isArray(contentBlocks)) {
|
|
2206
|
+
for (const block of contentBlocks) {
|
|
2207
|
+
if (!isRecord(block) || block.type !== "tool_use" || block.name !== "agenr_store") {
|
|
2208
|
+
continue;
|
|
2209
|
+
}
|
|
2210
|
+
contents.push(...extractStoreContentsFromInput(block.input));
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
const toolCalls = message.tool_calls;
|
|
2214
|
+
if (!Array.isArray(toolCalls)) {
|
|
2215
|
+
continue;
|
|
2216
|
+
}
|
|
2217
|
+
for (const toolCall of toolCalls) {
|
|
2218
|
+
if (!isRecord(toolCall)) {
|
|
2219
|
+
continue;
|
|
2220
|
+
}
|
|
2221
|
+
if (typeof toolCall.name === "string" && toolCall.name === "agenr_store") {
|
|
2222
|
+
const namedArgs = parseFunctionArguments(toolCall.arguments);
|
|
2223
|
+
if (namedArgs) {
|
|
2224
|
+
contents.push(...extractStoreContentsFromInput(namedArgs));
|
|
2225
|
+
} else {
|
|
2226
|
+
contents.push(...extractStoreContentsFromInput(toolCall.input));
|
|
2227
|
+
}
|
|
2228
|
+
continue;
|
|
2229
|
+
}
|
|
2230
|
+
const fn = toolCall.function;
|
|
2231
|
+
if (!isRecord(fn) || fn.name !== "agenr_store") {
|
|
2232
|
+
continue;
|
|
2233
|
+
}
|
|
2234
|
+
const args = parseFunctionArguments(fn.arguments);
|
|
2235
|
+
if (args) {
|
|
2236
|
+
contents.push(...extractStoreContentsFromInput(args));
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
return Array.from(new Set(contents));
|
|
2241
|
+
}
|
|
2242
|
+
async function fetchEmbeddedEntries(db, ids) {
|
|
2243
|
+
const idList = Array.from(ids).filter((id) => id.trim().length > 0);
|
|
2244
|
+
if (idList.length === 0) {
|
|
2245
|
+
return [];
|
|
2246
|
+
}
|
|
2247
|
+
const placeholders = idList.map(() => "?").join(", ");
|
|
2248
|
+
const result = await db.execute({
|
|
2249
|
+
sql: `
|
|
2250
|
+
SELECT id, embedding
|
|
2251
|
+
FROM entries
|
|
2252
|
+
WHERE id IN (${placeholders})
|
|
2253
|
+
AND embedding IS NOT NULL
|
|
2254
|
+
`,
|
|
2255
|
+
args: idList
|
|
2256
|
+
});
|
|
2257
|
+
const embeddedEntries = [];
|
|
2258
|
+
for (const row of result.rows) {
|
|
2259
|
+
const id = toStringValue(row.id);
|
|
2260
|
+
const embedding = mapBufferToVector(row.embedding);
|
|
2261
|
+
if (!id || embedding.length === 0) {
|
|
2262
|
+
continue;
|
|
2263
|
+
}
|
|
2264
|
+
embeddedEntries.push({ id, embedding });
|
|
2265
|
+
}
|
|
2266
|
+
return embeddedEntries;
|
|
2267
|
+
}
|
|
2268
|
+
async function updateQualityScores(db, updates) {
|
|
2269
|
+
if (updates.length === 0) {
|
|
2270
|
+
return;
|
|
2271
|
+
}
|
|
2272
|
+
const normalizedUpdates = /* @__PURE__ */ new Map();
|
|
2273
|
+
for (const update of updates) {
|
|
2274
|
+
const id = update.id.trim();
|
|
2275
|
+
if (!id) {
|
|
2276
|
+
continue;
|
|
2277
|
+
}
|
|
2278
|
+
normalizedUpdates.set(id, clamp01(update.signal));
|
|
2279
|
+
}
|
|
2280
|
+
if (normalizedUpdates.size === 0) {
|
|
2281
|
+
return;
|
|
2282
|
+
}
|
|
2283
|
+
const sql = `
|
|
2284
|
+
UPDATE entries
|
|
2285
|
+
SET quality_score = MIN(
|
|
2286
|
+
1.0,
|
|
2287
|
+
MAX(
|
|
2288
|
+
0.8 * COALESCE(quality_score, 0.5) + 0.2 * ?,
|
|
2289
|
+
CASE WHEN type IN ('fact', 'preference') THEN 0.35 ELSE 0.1 END
|
|
2290
|
+
)
|
|
2291
|
+
)
|
|
2292
|
+
WHERE id = ?
|
|
2293
|
+
`;
|
|
2294
|
+
await db.execute("BEGIN");
|
|
2295
|
+
try {
|
|
2296
|
+
for (const [id, signal] of normalizedUpdates.entries()) {
|
|
2297
|
+
await db.execute({ sql, args: [signal, id] });
|
|
2298
|
+
}
|
|
2299
|
+
await db.execute("COMMIT");
|
|
2300
|
+
} catch (error) {
|
|
2301
|
+
try {
|
|
2302
|
+
await db.execute("ROLLBACK");
|
|
2303
|
+
} catch {
|
|
2304
|
+
}
|
|
2305
|
+
throw error;
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
async function evolveQualityScores(db, options = {}) {
|
|
2309
|
+
const result = await db.execute({
|
|
2310
|
+
sql: `
|
|
2311
|
+
SELECT
|
|
2312
|
+
id,
|
|
2313
|
+
type,
|
|
2314
|
+
importance,
|
|
2315
|
+
recall_count,
|
|
2316
|
+
confirmations,
|
|
2317
|
+
quality_score,
|
|
2318
|
+
last_recalled_at,
|
|
2319
|
+
created_at
|
|
2320
|
+
FROM entries
|
|
2321
|
+
WHERE superseded_by IS NULL
|
|
2322
|
+
AND COALESCE(retired, 0) = 0
|
|
2323
|
+
`,
|
|
2324
|
+
args: []
|
|
2325
|
+
});
|
|
2326
|
+
const rows = result.rows.map((row) => ({
|
|
2327
|
+
id: toStringValue(row.id).trim(),
|
|
2328
|
+
type: toStringValue(row.type),
|
|
2329
|
+
importance: Math.max(0, toNumber(row.importance)),
|
|
2330
|
+
recallCount: Math.max(0, toNumber(row.recall_count)),
|
|
2331
|
+
confirmations: Math.max(0, toNumber(row.confirmations)),
|
|
2332
|
+
qualityScore: toNumber(row.quality_score),
|
|
2333
|
+
lastRecalledAt: toStringValue(row.last_recalled_at),
|
|
2334
|
+
createdAt: toStringValue(row.created_at)
|
|
2335
|
+
})).filter((row) => row.id.length > 0);
|
|
2336
|
+
if (rows.length === 0) {
|
|
2337
|
+
return {
|
|
2338
|
+
updated: 0,
|
|
2339
|
+
stats: {
|
|
2340
|
+
high: 0,
|
|
2341
|
+
medium: 0,
|
|
2342
|
+
low: 0,
|
|
2343
|
+
avgBefore: 0,
|
|
2344
|
+
avgAfter: 0
|
|
2345
|
+
}
|
|
2346
|
+
};
|
|
2347
|
+
}
|
|
2348
|
+
const edgeCounts = await getCoRecallEdgeCounts(db);
|
|
2349
|
+
const maxRecallCount = rows.reduce((max, row) => Math.max(max, row.recallCount), 0);
|
|
2350
|
+
const maxEdgeCount = rows.reduce((max, row) => Math.max(max, edgeCounts.get(row.id) ?? 0), 0);
|
|
2351
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
2352
|
+
let high = 0;
|
|
2353
|
+
let medium = 0;
|
|
2354
|
+
let low = 0;
|
|
2355
|
+
let totalBefore = 0;
|
|
2356
|
+
let totalAfter = 0;
|
|
2357
|
+
const updates = [];
|
|
2358
|
+
for (const row of rows) {
|
|
2359
|
+
const recallSignal = maxRecallCount > 0 ? clamp(row.recallCount / maxRecallCount, 0, 1) : 0;
|
|
2360
|
+
const edgeSignal = maxEdgeCount > 0 ? clamp((edgeCounts.get(row.id) ?? 0) / maxEdgeCount, 0, 1) : 0;
|
|
2361
|
+
const confirmSignal = clamp(row.confirmations / 3, 0, 1);
|
|
2362
|
+
const importanceNormalized = clamp(row.importance / 10, 0, 1);
|
|
2363
|
+
const sourceDate = row.lastRecalledAt || row.createdAt;
|
|
2364
|
+
const daysSinceLastRecall = parseDaysBetween(now, sourceDate || void 0);
|
|
2365
|
+
const decayPenalty = computeDecayPenalty(daysSinceLastRecall);
|
|
2366
|
+
const rawQuality = (0.35 * recallSignal + 0.25 * edgeSignal + 0.2 * confirmSignal + 0.2 * importanceNormalized) * decayPenalty;
|
|
2367
|
+
const evolved = clamp(rawQuality, 0.05, 1);
|
|
2368
|
+
const existingQuality = clamp(Number.isFinite(row.qualityScore) ? row.qualityScore : 0.5, 0, 1);
|
|
2369
|
+
const blendedQuality = 0.6 * evolved + 0.4 * existingQuality;
|
|
2370
|
+
const finalQuality = clamp(Math.max(blendedQuality, qualityFloorByType(row.type)), 0.05, 1);
|
|
2371
|
+
totalBefore += existingQuality;
|
|
2372
|
+
totalAfter += finalQuality;
|
|
2373
|
+
if (finalQuality >= 0.7) {
|
|
2374
|
+
high += 1;
|
|
2375
|
+
} else if (finalQuality >= 0.3) {
|
|
2376
|
+
medium += 1;
|
|
2377
|
+
} else {
|
|
2378
|
+
low += 1;
|
|
2379
|
+
}
|
|
2380
|
+
updates.push({ id: row.id, qualityScore: finalQuality });
|
|
2381
|
+
}
|
|
2382
|
+
if (updates.length > 0) {
|
|
2383
|
+
await db.execute("BEGIN");
|
|
2384
|
+
try {
|
|
2385
|
+
for (const update of updates) {
|
|
2386
|
+
await db.execute({
|
|
2387
|
+
sql: `
|
|
2388
|
+
UPDATE entries
|
|
2389
|
+
SET quality_score = ?
|
|
2390
|
+
WHERE id = ?
|
|
2391
|
+
`,
|
|
2392
|
+
args: [update.qualityScore, update.id]
|
|
2393
|
+
});
|
|
2394
|
+
}
|
|
2395
|
+
if (options.dryRun === true) {
|
|
2396
|
+
await db.execute("ROLLBACK");
|
|
2397
|
+
} else {
|
|
2398
|
+
await db.execute("COMMIT");
|
|
2399
|
+
}
|
|
2400
|
+
} catch (error) {
|
|
2401
|
+
try {
|
|
2402
|
+
await db.execute("ROLLBACK");
|
|
2403
|
+
} catch {
|
|
2404
|
+
}
|
|
2405
|
+
throw error;
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
return {
|
|
2409
|
+
updated: updates.length,
|
|
2410
|
+
stats: {
|
|
2411
|
+
high,
|
|
2412
|
+
medium,
|
|
2413
|
+
low,
|
|
2414
|
+
avgBefore: totalBefore / updates.length,
|
|
2415
|
+
avgAfter: totalAfter / updates.length
|
|
2416
|
+
}
|
|
2417
|
+
};
|
|
2418
|
+
}
|
|
2419
|
+
async function computeRecallFeedback(db, sessionKey, messages, recalledEntryIds, config, logger) {
|
|
2420
|
+
const emptyResult = {
|
|
2421
|
+
usedIds: [],
|
|
2422
|
+
correctedIds: [],
|
|
2423
|
+
updatedIds: []
|
|
2424
|
+
};
|
|
2425
|
+
if (recalledEntryIds.size === 0) {
|
|
2426
|
+
return emptyResult;
|
|
2427
|
+
}
|
|
2428
|
+
const assistantText = collectAssistantText(messages);
|
|
2429
|
+
if (assistantText.length < RESPONSE_MIN_CHARS) {
|
|
2430
|
+
return emptyResult;
|
|
2431
|
+
}
|
|
2432
|
+
const responseCorpus = assistantText.slice(0, RESPONSE_MAX_CHARS);
|
|
2433
|
+
let apiKey;
|
|
2434
|
+
try {
|
|
2435
|
+
apiKey = resolveEmbeddingApiKey(config, process.env);
|
|
2436
|
+
} catch (error) {
|
|
2437
|
+
logger.warn(
|
|
2438
|
+
`[agenr] before_reset: feedback skipped for session=${sessionKey} - ${error instanceof Error ? error.message : String(error)}`
|
|
2439
|
+
);
|
|
2440
|
+
return emptyResult;
|
|
2441
|
+
}
|
|
2442
|
+
let responseEmbedding = null;
|
|
2443
|
+
try {
|
|
2444
|
+
const vectors = await embed([responseCorpus], apiKey);
|
|
2445
|
+
responseEmbedding = vectors[0] ?? null;
|
|
2446
|
+
} catch (error) {
|
|
2447
|
+
logger.warn(
|
|
2448
|
+
`[agenr] before_reset: feedback embedding failed for session=${sessionKey}: ${error instanceof Error ? error.message : String(error)}`
|
|
2449
|
+
);
|
|
2450
|
+
return emptyResult;
|
|
2451
|
+
}
|
|
2452
|
+
if (!responseEmbedding || responseEmbedding.length === 0) {
|
|
2453
|
+
return emptyResult;
|
|
2454
|
+
}
|
|
2455
|
+
const recalledEntries = await fetchEmbeddedEntries(db, recalledEntryIds);
|
|
2456
|
+
if (recalledEntries.length === 0) {
|
|
2457
|
+
return emptyResult;
|
|
2458
|
+
}
|
|
2459
|
+
const storeContents = collectAgenrStoreContents(messages);
|
|
2460
|
+
let correctionEmbeddings = [];
|
|
2461
|
+
if (storeContents.length > 0) {
|
|
2462
|
+
try {
|
|
2463
|
+
correctionEmbeddings = await embed(storeContents, apiKey);
|
|
2464
|
+
} catch (error) {
|
|
2465
|
+
logger.warn(
|
|
2466
|
+
`[agenr] before_reset: correction embedding failed for session=${sessionKey}: ${error instanceof Error ? error.message : String(error)}`
|
|
2467
|
+
);
|
|
2468
|
+
correctionEmbeddings = [];
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
const correctedIds = /* @__PURE__ */ new Set();
|
|
2472
|
+
if (correctionEmbeddings.length > 0) {
|
|
2473
|
+
for (const entry of recalledEntries) {
|
|
2474
|
+
for (const correctionEmbedding of correctionEmbeddings) {
|
|
2475
|
+
if (cosineSimilarity(entry.embedding, correctionEmbedding) >= CORRECTION_THRESHOLD) {
|
|
2476
|
+
correctedIds.add(entry.id);
|
|
2477
|
+
break;
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
const updates = recalledEntries.map((entry) => {
|
|
2483
|
+
if (correctedIds.has(entry.id)) {
|
|
2484
|
+
return { id: entry.id, signal: SIGNAL_CORRECTED };
|
|
2485
|
+
}
|
|
2486
|
+
const sim = cosineSimilarity(entry.embedding, responseEmbedding ?? []);
|
|
2487
|
+
return {
|
|
2488
|
+
id: entry.id,
|
|
2489
|
+
signal: sim >= RESPONSE_USED_THRESHOLD ? SIGNAL_USED : SIGNAL_UNCLEAR
|
|
2490
|
+
};
|
|
2491
|
+
});
|
|
2492
|
+
if (updates.length > 0) {
|
|
2493
|
+
await updateQualityScores(db, updates);
|
|
2494
|
+
}
|
|
2495
|
+
return {
|
|
2496
|
+
usedIds: updates.filter((update) => update.signal === SIGNAL_USED).map((update) => update.id),
|
|
2497
|
+
correctedIds: Array.from(correctedIds),
|
|
2498
|
+
updatedIds: updates.map((update) => update.id)
|
|
2499
|
+
};
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
// src/types.ts
|
|
2503
|
+
var KNOWLEDGE_TYPES = [
|
|
2504
|
+
"fact",
|
|
2505
|
+
"decision",
|
|
2506
|
+
"preference",
|
|
2507
|
+
"todo",
|
|
2508
|
+
"relationship",
|
|
2509
|
+
"event",
|
|
2510
|
+
"lesson"
|
|
2511
|
+
];
|
|
2512
|
+
var IMPORTANCE_MIN = 1;
|
|
2513
|
+
var IMPORTANCE_MAX = 10;
|
|
2514
|
+
var EXPIRY_LEVELS = ["core", "permanent", "temporary"];
|
|
2515
|
+
var SCOPE_LEVELS = ["private", "personal", "public"];
|
|
2516
|
+
var KNOWLEDGE_PLATFORMS = ["openclaw", "claude-code", "codex", "plaud"];
|
|
2517
|
+
|
|
2518
|
+
// src/llm/client.ts
|
|
2519
|
+
function resolveProviderAndModel(input) {
|
|
2520
|
+
const config = input.config ?? readConfig(input.env);
|
|
2521
|
+
const auth = config?.auth;
|
|
2522
|
+
if (!auth) {
|
|
2523
|
+
throw new Error("Not configured. Run `agenr setup`.");
|
|
2524
|
+
}
|
|
2525
|
+
const providerRaw = input.provider?.trim() || config.provider?.trim();
|
|
2526
|
+
const modelRaw = input.model?.trim() || config?.models?.extraction?.trim();
|
|
2527
|
+
if (!providerRaw || !modelRaw) {
|
|
2528
|
+
throw new Error(
|
|
2529
|
+
[
|
|
2530
|
+
"Provider/model are not configured.",
|
|
2531
|
+
"Run `agenr setup`, or pass --provider and --model to override this run."
|
|
2532
|
+
].join("\n")
|
|
2533
|
+
);
|
|
2534
|
+
}
|
|
2535
|
+
const provider = normalizeProvider(providerRaw);
|
|
2536
|
+
const expectedProvider = authMethodToProvider(auth);
|
|
2537
|
+
if (provider !== expectedProvider) {
|
|
2538
|
+
throw new Error(
|
|
2539
|
+
[
|
|
2540
|
+
`Configured auth method "${auth}" requires provider "${expectedProvider}".`,
|
|
2541
|
+
"Use `agenr config set auth <method>` to switch auth/provider together."
|
|
2542
|
+
].join(" ")
|
|
2543
|
+
);
|
|
2544
|
+
}
|
|
2545
|
+
const resolvedModel = resolveModel(provider, modelRaw);
|
|
2546
|
+
return {
|
|
2547
|
+
auth,
|
|
2548
|
+
provider: resolvedModel.provider,
|
|
2549
|
+
model: resolvedModel.modelId,
|
|
2550
|
+
storedCredentials: config?.credentials
|
|
2551
|
+
};
|
|
2552
|
+
}
|
|
2553
|
+
function createLlmClient(input) {
|
|
2554
|
+
const resolved = resolveProviderAndModel(input);
|
|
2555
|
+
const resolvedModel = resolveModel(resolved.provider, resolved.model);
|
|
2556
|
+
const credentials = resolveCredentials({
|
|
2557
|
+
auth: resolved.auth,
|
|
2558
|
+
storedCredentials: resolved.storedCredentials,
|
|
2559
|
+
env: input.env
|
|
2560
|
+
});
|
|
2561
|
+
return {
|
|
2562
|
+
auth: resolved.auth,
|
|
2563
|
+
resolvedModel,
|
|
2564
|
+
credentials
|
|
2565
|
+
};
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2568
|
+
// src/db/review-queue.ts
|
|
2569
|
+
import { randomUUID } from "crypto";
|
|
2570
|
+
var REVIEW_REASONS = ["low_quality", "contradicted", "stale", "manual"];
|
|
2571
|
+
var REVIEW_ACTIONS = ["retire", "review", "merge"];
|
|
2572
|
+
function ensureReason(reason) {
|
|
2573
|
+
if (REVIEW_REASONS.includes(reason)) {
|
|
2574
|
+
return reason;
|
|
2575
|
+
}
|
|
2576
|
+
throw new Error(`Invalid review reason: ${reason}`);
|
|
2577
|
+
}
|
|
2578
|
+
function ensureAction(action) {
|
|
2579
|
+
if (REVIEW_ACTIONS.includes(action)) {
|
|
2580
|
+
return action;
|
|
2581
|
+
}
|
|
2582
|
+
throw new Error(`Invalid review suggested action: ${action}`);
|
|
2583
|
+
}
|
|
2584
|
+
async function flagForReview(db, entryId, reason, detail, suggestedAction) {
|
|
2585
|
+
const normalizedEntryId = entryId.trim();
|
|
2586
|
+
const normalizedReason = ensureReason(reason);
|
|
2587
|
+
const normalizedAction = ensureAction(suggestedAction);
|
|
2588
|
+
if (!normalizedEntryId) {
|
|
2589
|
+
throw new Error("entryId is required");
|
|
2590
|
+
}
|
|
2591
|
+
const id = randomUUID();
|
|
2592
|
+
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2593
|
+
const inserted = await db.execute({
|
|
2594
|
+
sql: `
|
|
2595
|
+
INSERT INTO review_queue (
|
|
2596
|
+
id,
|
|
2597
|
+
entry_id,
|
|
2598
|
+
reason,
|
|
2599
|
+
detail,
|
|
2600
|
+
suggested_action,
|
|
2601
|
+
status,
|
|
2602
|
+
created_at
|
|
2603
|
+
)
|
|
2604
|
+
VALUES (?, ?, ?, ?, ?, 'pending', ?)
|
|
2605
|
+
ON CONFLICT DO NOTHING
|
|
2606
|
+
`,
|
|
2607
|
+
args: [id, normalizedEntryId, normalizedReason, detail, normalizedAction, createdAt]
|
|
2608
|
+
});
|
|
2609
|
+
if (toRowsAffected(inserted.rowsAffected) === 0) {
|
|
2610
|
+
return {
|
|
2611
|
+
created: false,
|
|
2612
|
+
id: null
|
|
2613
|
+
};
|
|
2614
|
+
}
|
|
2615
|
+
return { created: true, id };
|
|
2616
|
+
}
|
|
2617
|
+
async function getPendingReviews(db, limit = 20) {
|
|
2618
|
+
const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : 20;
|
|
2619
|
+
const result = await db.execute({
|
|
2620
|
+
sql: `
|
|
2621
|
+
SELECT
|
|
2622
|
+
review_queue.id,
|
|
2623
|
+
review_queue.entry_id,
|
|
2624
|
+
review_queue.reason,
|
|
2625
|
+
review_queue.detail,
|
|
2626
|
+
review_queue.suggested_action,
|
|
2627
|
+
review_queue.status,
|
|
2628
|
+
review_queue.created_at,
|
|
2629
|
+
review_queue.resolved_at,
|
|
2630
|
+
entries.subject,
|
|
2631
|
+
entries.content
|
|
2632
|
+
FROM review_queue
|
|
2633
|
+
LEFT JOIN entries ON entries.id = review_queue.entry_id
|
|
2634
|
+
WHERE review_queue.status = 'pending'
|
|
2635
|
+
ORDER BY review_queue.created_at ASC
|
|
2636
|
+
LIMIT ?
|
|
2637
|
+
`,
|
|
2638
|
+
args: [safeLimit]
|
|
2639
|
+
});
|
|
2640
|
+
return result.rows.map((row) => ({
|
|
2641
|
+
id: toStringValue(row.id),
|
|
2642
|
+
entryId: toStringValue(row.entry_id),
|
|
2643
|
+
reason: ensureReason(toStringValue(row.reason)),
|
|
2644
|
+
detail: toStringValue(row.detail),
|
|
2645
|
+
suggestedAction: ensureAction(toStringValue(row.suggested_action)),
|
|
2646
|
+
status: toStringValue(row.status) || "pending",
|
|
2647
|
+
createdAt: toStringValue(row.created_at),
|
|
2648
|
+
resolvedAt: toStringValue(row.resolved_at),
|
|
2649
|
+
entrySubject: toStringValue(row.subject),
|
|
2650
|
+
entryContent: toStringValue(row.content)
|
|
2651
|
+
}));
|
|
2652
|
+
}
|
|
2653
|
+
async function resolveReview(db, reviewId, status) {
|
|
2654
|
+
const normalizedReviewId = reviewId.trim();
|
|
2655
|
+
if (!normalizedReviewId) {
|
|
2656
|
+
return false;
|
|
2657
|
+
}
|
|
2658
|
+
const resolvedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2659
|
+
const result = await db.execute({
|
|
2660
|
+
sql: `
|
|
2661
|
+
UPDATE review_queue
|
|
2662
|
+
SET status = ?,
|
|
2663
|
+
resolved_at = ?
|
|
2664
|
+
WHERE id = ?
|
|
2665
|
+
AND status = 'pending'
|
|
2666
|
+
`,
|
|
2667
|
+
args: [status, resolvedAt, normalizedReviewId]
|
|
2668
|
+
});
|
|
2669
|
+
return toRowsAffected(result.rowsAffected) > 0;
|
|
2670
|
+
}
|
|
2671
|
+
async function checkAndFlagLowQuality(db, entryId, qualityScore, recallCount) {
|
|
2672
|
+
if (!Number.isFinite(qualityScore) || !Number.isFinite(recallCount)) {
|
|
2673
|
+
return;
|
|
2674
|
+
}
|
|
2675
|
+
if (qualityScore >= 0.2 || recallCount < 10) {
|
|
2676
|
+
return;
|
|
2677
|
+
}
|
|
2678
|
+
const roundedQuality = Math.max(0, Math.min(1, qualityScore));
|
|
2679
|
+
const detail = `quality_score ${roundedQuality.toFixed(3)} after ${Math.floor(recallCount)} recalls`;
|
|
2680
|
+
await flagForReview(db, entryId, "low_quality", detail, "retire");
|
|
2681
|
+
}
|
|
2682
|
+
async function getPendingReviewById(db, reviewId) {
|
|
2683
|
+
const normalizedReviewId = reviewId.trim();
|
|
2684
|
+
if (!normalizedReviewId) {
|
|
2685
|
+
return null;
|
|
2686
|
+
}
|
|
2687
|
+
const result = await db.execute({
|
|
2688
|
+
sql: `
|
|
2689
|
+
SELECT
|
|
2690
|
+
review_queue.id,
|
|
2691
|
+
review_queue.entry_id,
|
|
2692
|
+
review_queue.reason,
|
|
2693
|
+
review_queue.detail,
|
|
2694
|
+
review_queue.suggested_action,
|
|
2695
|
+
review_queue.status,
|
|
2696
|
+
review_queue.created_at,
|
|
2697
|
+
review_queue.resolved_at,
|
|
2698
|
+
entries.subject,
|
|
2699
|
+
entries.content
|
|
2700
|
+
FROM review_queue
|
|
2701
|
+
LEFT JOIN entries ON entries.id = review_queue.entry_id
|
|
2702
|
+
WHERE review_queue.id = ?
|
|
2703
|
+
LIMIT 1
|
|
2704
|
+
`,
|
|
2705
|
+
args: [normalizedReviewId]
|
|
2706
|
+
});
|
|
2707
|
+
const row = result.rows[0];
|
|
2708
|
+
if (!row) {
|
|
2709
|
+
return null;
|
|
2710
|
+
}
|
|
2711
|
+
return {
|
|
2712
|
+
id: toStringValue(row.id),
|
|
2713
|
+
entryId: toStringValue(row.entry_id),
|
|
2714
|
+
reason: ensureReason(toStringValue(row.reason)),
|
|
2715
|
+
detail: toStringValue(row.detail),
|
|
2716
|
+
suggestedAction: ensureAction(toStringValue(row.suggested_action)),
|
|
2717
|
+
status: toStringValue(row.status) || "pending",
|
|
2718
|
+
createdAt: toStringValue(row.created_at),
|
|
2719
|
+
resolvedAt: toStringValue(row.resolved_at),
|
|
2720
|
+
entrySubject: toStringValue(row.subject),
|
|
2721
|
+
entryContent: toStringValue(row.content)
|
|
2722
|
+
};
|
|
2723
|
+
}
|
|
2724
|
+
async function getPendingReviewCountsByReason(db) {
|
|
2725
|
+
const result = await db.execute({
|
|
2726
|
+
sql: `
|
|
2727
|
+
SELECT reason, COUNT(*) AS count
|
|
2728
|
+
FROM review_queue
|
|
2729
|
+
WHERE status = 'pending'
|
|
2730
|
+
GROUP BY reason
|
|
2731
|
+
ORDER BY count DESC, reason ASC
|
|
2732
|
+
`,
|
|
2733
|
+
args: []
|
|
2734
|
+
});
|
|
2735
|
+
return result.rows.map((row) => ({
|
|
2736
|
+
reason: ensureReason(toStringValue(row.reason)),
|
|
2737
|
+
count: toNumber(row.count)
|
|
2738
|
+
}));
|
|
2739
|
+
}
|
|
2740
|
+
async function getOldestPendingReviewCreatedAt(db) {
|
|
2741
|
+
const result = await db.execute({
|
|
2742
|
+
sql: `
|
|
2743
|
+
SELECT MIN(created_at) AS oldest_created_at
|
|
2744
|
+
FROM review_queue
|
|
2745
|
+
WHERE status = 'pending'
|
|
2746
|
+
`,
|
|
2747
|
+
args: []
|
|
2748
|
+
});
|
|
2749
|
+
const oldest = toStringValue(result.rows[0]?.oldest_created_at);
|
|
2750
|
+
return oldest || null;
|
|
2751
|
+
}
|
|
2752
|
+
async function rehabilitateEntry(db, entryId, floor = 0.3) {
|
|
2753
|
+
const normalizedEntryId = entryId.trim();
|
|
2754
|
+
if (!normalizedEntryId || !Number.isFinite(floor)) {
|
|
2755
|
+
return;
|
|
2756
|
+
}
|
|
2757
|
+
await db.execute({
|
|
2758
|
+
sql: `
|
|
2759
|
+
UPDATE entries
|
|
2760
|
+
SET quality_score = MAX(COALESCE(quality_score, 0.5), ?)
|
|
2761
|
+
WHERE id = ?
|
|
2762
|
+
`,
|
|
2763
|
+
args: [floor, normalizedEntryId]
|
|
2764
|
+
});
|
|
2765
|
+
}
|
|
2766
|
+
|
|
2767
|
+
export {
|
|
2768
|
+
resolveModel,
|
|
2769
|
+
normalizeLabel,
|
|
2770
|
+
DEFAULT_TASK_MODEL,
|
|
2771
|
+
resolveDefaultKnowledgeDbPath,
|
|
2772
|
+
resolveConfigPath,
|
|
2773
|
+
getAuthMethodDefinition,
|
|
2774
|
+
readConfig,
|
|
2775
|
+
writeConfig,
|
|
2776
|
+
resolveProjectFromGlobalConfig,
|
|
2777
|
+
mergeConfigPatch,
|
|
2778
|
+
isCompleteConfig,
|
|
2779
|
+
setConfigKey,
|
|
2780
|
+
setStoredCredential,
|
|
2781
|
+
maskSecret,
|
|
2782
|
+
describeAuth,
|
|
2783
|
+
resolveModelForTask,
|
|
2784
|
+
composeEmbeddingText,
|
|
2785
|
+
embed,
|
|
2786
|
+
resolveEmbeddingApiKey,
|
|
2787
|
+
probeCredentials,
|
|
2788
|
+
runSimpleStream,
|
|
2789
|
+
APP_VERSION,
|
|
2790
|
+
CREATE_IDX_ENTRIES_EMBEDDING_SQL,
|
|
2791
|
+
dropFtsTriggersAndIndex,
|
|
2792
|
+
rebuildFtsAndTriggers,
|
|
2793
|
+
rebuildVectorIndex,
|
|
2794
|
+
setBulkIngestMeta,
|
|
2795
|
+
clearBulkIngestMeta,
|
|
2796
|
+
initSchema,
|
|
2797
|
+
resetDb,
|
|
2798
|
+
DEFAULT_DB_PATH,
|
|
2799
|
+
buildBackupPath,
|
|
2800
|
+
getDb,
|
|
2801
|
+
initDb,
|
|
2802
|
+
walCheckpoint,
|
|
2803
|
+
backupDb,
|
|
2804
|
+
closeDb,
|
|
2805
|
+
evolveQualityScores,
|
|
2806
|
+
computeRecallFeedback,
|
|
2807
|
+
KNOWLEDGE_TYPES,
|
|
2808
|
+
IMPORTANCE_MIN,
|
|
2809
|
+
IMPORTANCE_MAX,
|
|
2810
|
+
EXPIRY_LEVELS,
|
|
2811
|
+
SCOPE_LEVELS,
|
|
2812
|
+
KNOWLEDGE_PLATFORMS,
|
|
2813
|
+
createLlmClient,
|
|
2814
|
+
getPendingReviews,
|
|
2815
|
+
resolveReview,
|
|
2816
|
+
checkAndFlagLowQuality,
|
|
2817
|
+
getPendingReviewById,
|
|
2818
|
+
getPendingReviewCountsByReason,
|
|
2819
|
+
getOldestPendingReviewCreatedAt,
|
|
2820
|
+
rehabilitateEntry
|
|
2821
|
+
};
|