@edihasaj/recall 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +409 -0
  3. package/dist/chunk-4CV4JOE5.js +27 -0
  4. package/dist/chunk-4CV4JOE5.js.map +1 -0
  5. package/dist/chunk-A5UIRZU6.js +469 -0
  6. package/dist/chunk-A5UIRZU6.js.map +1 -0
  7. package/dist/chunk-AYHFPCGY.js +964 -0
  8. package/dist/chunk-AYHFPCGY.js.map +1 -0
  9. package/dist/chunk-DNFKAHS6.js +204 -0
  10. package/dist/chunk-DNFKAHS6.js.map +1 -0
  11. package/dist/chunk-GC5XMBG4.js +551 -0
  12. package/dist/chunk-GC5XMBG4.js.map +1 -0
  13. package/dist/chunk-IILLSHLM.js +3021 -0
  14. package/dist/chunk-IILLSHLM.js.map +1 -0
  15. package/dist/chunk-LVQW6WHK.js +146 -0
  16. package/dist/chunk-LVQW6WHK.js.map +1 -0
  17. package/dist/chunk-LZ6PMQRX.js +955 -0
  18. package/dist/chunk-LZ6PMQRX.js.map +1 -0
  19. package/dist/chunk-PC43MBX5.js +2960 -0
  20. package/dist/chunk-PC43MBX5.js.map +1 -0
  21. package/dist/chunk-VEPXEHRZ.js +1763 -0
  22. package/dist/chunk-VEPXEHRZ.js.map +1 -0
  23. package/dist/cleanup-TVOX2S2S.js +28 -0
  24. package/dist/cleanup-TVOX2S2S.js.map +1 -0
  25. package/dist/cli.js +3425 -0
  26. package/dist/cli.js.map +1 -0
  27. package/dist/daemon.js +1298 -0
  28. package/dist/daemon.js.map +1 -0
  29. package/dist/dispatcher-UGMU6THT.js +15 -0
  30. package/dist/dispatcher-UGMU6THT.js.map +1 -0
  31. package/dist/keychain-5QG52ANO.js +22 -0
  32. package/dist/keychain-5QG52ANO.js.map +1 -0
  33. package/dist/mcp.js +21 -0
  34. package/dist/mcp.js.map +1 -0
  35. package/dist/quality-Z7LPMMBC.js +17 -0
  36. package/dist/quality-Z7LPMMBC.js.map +1 -0
  37. package/dist/sync-server.js +225 -0
  38. package/dist/sync-server.js.map +1 -0
  39. package/dist/tasks-UOLSPXJQ.js +61 -0
  40. package/dist/tasks-UOLSPXJQ.js.map +1 -0
  41. package/dist/usage-CY3V72YN.js +101 -0
  42. package/dist/usage-CY3V72YN.js.map +1 -0
  43. package/drizzle/0000_initial_create.sql +240 -0
  44. package/drizzle/0001_rich_liz_osborn.sql +21 -0
  45. package/drizzle/0002_unknown_spot.sql +18 -0
  46. package/drizzle/0003_red_wendigo.sql +19 -0
  47. package/drizzle/0004_early_carlie_cooper.sql +1 -0
  48. package/drizzle/0005_simple_emma_frost.sql +96 -0
  49. package/drizzle/0006_keen_mongoose.sql +2 -0
  50. package/drizzle/0007_flawless_maximus.sql +15 -0
  51. package/drizzle/meta/0000_snapshot.json +1630 -0
  52. package/drizzle/meta/0001_snapshot.json +1773 -0
  53. package/drizzle/meta/0002_snapshot.json +1891 -0
  54. package/drizzle/meta/0003_snapshot.json +2014 -0
  55. package/drizzle/meta/0004_snapshot.json +2022 -0
  56. package/drizzle/meta/0005_snapshot.json +2064 -0
  57. package/drizzle/meta/0006_snapshot.json +2078 -0
  58. package/drizzle/meta/0007_snapshot.json +2183 -0
  59. package/drizzle/meta/_journal.json +62 -0
  60. package/package.json +64 -0
  61. package/scripts/recall-claude +7 -0
  62. package/scripts/recall-codex +7 -0
  63. package/scripts/recall-session +71 -0
@@ -0,0 +1,2960 @@
1
+ import {
2
+ computeHealthScore,
3
+ getRepoQualityProfile,
4
+ processCorrection,
5
+ seedScannedConfidence
6
+ } from "./chunk-VEPXEHRZ.js";
7
+ import {
8
+ CONFIDENCE,
9
+ RetrievalEvalCase,
10
+ RetrievalEvalFile,
11
+ activityEventDedupeKey,
12
+ bootstrapEmbeddings,
13
+ confirmMemory,
14
+ createMemory,
15
+ demoteMemory,
16
+ feedbackWeightedScore,
17
+ generateEmbedding,
18
+ generateEmbeddings,
19
+ getEmbeddingCacheRoot,
20
+ getMemory,
21
+ getMemoryFeedbackSummaries,
22
+ historySnippetDedupeKey,
23
+ hybridSearch,
24
+ loadEmbeddingConfigFromEnv,
25
+ projectEmbeddingToIndex,
26
+ promoteMemory,
27
+ queryMemories,
28
+ queueMemoryEmbeddingSync,
29
+ recordAudit,
30
+ recordFeedback,
31
+ rejectMemory,
32
+ resolveProvider,
33
+ statusFromConfidence,
34
+ tagActivitySource
35
+ } from "./chunk-IILLSHLM.js";
36
+ import {
37
+ activityEvents,
38
+ approvalRequests,
39
+ auditTrail,
40
+ evalSessions,
41
+ feedbackEvents,
42
+ historyInjections,
43
+ historySnippetEmbeddings,
44
+ historySnippets,
45
+ implicitSignals,
46
+ memories,
47
+ memoryInjections,
48
+ memoryMaintenanceTasks,
49
+ policyRules,
50
+ schema_exports
51
+ } from "./chunk-A5UIRZU6.js";
52
+
53
+ // src/db/client.ts
54
+ import Database from "better-sqlite3";
55
+ import { drizzle } from "drizzle-orm/better-sqlite3";
56
+ import { migrate } from "drizzle-orm/better-sqlite3/migrator";
57
+ import { join, dirname } from "path";
58
+ import { mkdirSync, existsSync, rmSync } from "fs";
59
+ import { fileURLToPath } from "url";
60
+ var __dirname = dirname(fileURLToPath(import.meta.url));
61
+ var RECALL_DB_USER_VERSION = 9;
62
+ function getDbPath() {
63
+ const dataDir = process.env.RECALL_DATA_DIR ?? join(
64
+ process.env.HOME ?? process.env.USERPROFILE ?? ".",
65
+ ".recall"
66
+ );
67
+ mkdirSync(dataDir, { recursive: true });
68
+ return join(dataDir, "recall.db");
69
+ }
70
+ function getMigrationsPath() {
71
+ let dir = __dirname;
72
+ for (let i = 0; i < 5; i++) {
73
+ const candidate = join(dir, "drizzle");
74
+ if (existsSync(join(candidate, "meta", "_journal.json"))) {
75
+ return candidate;
76
+ }
77
+ dir = dirname(dir);
78
+ }
79
+ return join(__dirname, "..", "drizzle");
80
+ }
81
+ var _sqlite = null;
82
+ var _db = null;
83
+ var _dbPath = null;
84
+ function makeDb(sqlite) {
85
+ return drizzle(sqlite, { schema: schema_exports });
86
+ }
87
+ function applyPragmas(sqlite) {
88
+ sqlite.pragma("journal_mode = WAL");
89
+ sqlite.pragma("foreign_keys = ON");
90
+ }
91
+ function setDbUserVersion(sqlite, version2 = RECALL_DB_USER_VERSION) {
92
+ sqlite.pragma(`user_version = ${version2}`);
93
+ }
94
+ function getDb(dbPath) {
95
+ if (!_db) {
96
+ const path = dbPath ?? getDbPath();
97
+ _sqlite = new Database(path);
98
+ applyPragmas(_sqlite);
99
+ _db = makeDb(_sqlite);
100
+ _dbPath = path;
101
+ }
102
+ return _db;
103
+ }
104
+ function initDb(dbPath) {
105
+ const db = getDb(dbPath);
106
+ migrate(db, { migrationsFolder: getMigrationsPath() });
107
+ setDbUserVersion(db.$client);
108
+ return db;
109
+ }
110
+ function closeDb() {
111
+ if (_sqlite) {
112
+ _sqlite.close();
113
+ }
114
+ _sqlite = null;
115
+ _db = null;
116
+ _dbPath = null;
117
+ }
118
+ function getDbUserVersion(dbPath) {
119
+ const path = dbPath ?? getDbPath();
120
+ if (!existsSync(path)) return 0;
121
+ const sqlite = new Database(path, { readonly: true, fileMustExist: true });
122
+ try {
123
+ return Number(sqlite.pragma("user_version", { simple: true }) ?? 0);
124
+ } finally {
125
+ sqlite.close();
126
+ }
127
+ }
128
+ function resetDb(dbPath, options = {}) {
129
+ const path = dbPath ?? getDbPath();
130
+ if (_dbPath === path) {
131
+ closeDb();
132
+ }
133
+ for (const suffix of ["", "-shm", "-wal"]) {
134
+ const candidate = `${path}${suffix}`;
135
+ if (existsSync(candidate)) {
136
+ rmSync(candidate, { force: true });
137
+ }
138
+ }
139
+ if (options.purgeModels) {
140
+ rmSync(getEmbeddingCacheRoot(), { recursive: true, force: true });
141
+ }
142
+ }
143
+
144
+ // src/scanner/repo.ts
145
+ import { readFileSync, existsSync as existsSync2 } from "fs";
146
+ import { join as join2, basename } from "path";
147
+ import { execSync } from "child_process";
148
+ import { eq } from "drizzle-orm";
149
+
150
+ // src/scanner/signal.ts
151
+ var ACTIVE_COMMAND_PATTERNS = [
152
+ /^use\b/i,
153
+ /^(test|build|lint|dev|start|typecheck|check):\s*`.+`$/i,
154
+ /^makefile targets:/i
155
+ ];
156
+ var CANDIDATE_GOTCHA_PATTERNS = [
157
+ /^next\.js project$/i,
158
+ /^react project\b/i,
159
+ /^vue\.js project$/i,
160
+ /^svelte project$/i,
161
+ /^server framework:/i,
162
+ /^uses alembic\b/i
163
+ ];
164
+ var ACTIONABLE_RULE_PATTERN = /\b(always|never|must|don't|do not|required|prefer|avoid|use|keep|run|update|add|remove|check|only)\b/i;
165
+ function evaluateScannedMemory(input) {
166
+ const normalized = normalizeScannedText(input.text);
167
+ const lower = normalized.toLowerCase();
168
+ if (!normalized || normalized.length < 12) {
169
+ return reject(normalized, "too_short");
170
+ }
171
+ if (lower.startsWith("setup commands from readme:")) {
172
+ return reject(normalized, "readme_setup_noise");
173
+ }
174
+ if (lower === "what we do not build") {
175
+ return reject(normalized, "section_heading");
176
+ }
177
+ if (/^ci:\s*(github actions|gitlab ci)\b/i.test(normalized)) {
178
+ return reject(normalized, "generic_ci");
179
+ }
180
+ if (/^req-[a-z0-9-]+:/i.test(lower)) {
181
+ return reject(normalized, "spec_requirement");
182
+ }
183
+ if (/^[A-Z][A-Za-z0-9 /_-]{1,80}:$/.test(normalized)) {
184
+ return reject(normalized, "heading");
185
+ }
186
+ if (input.source === "config_parse" && lower.startsWith("linting/formatting: python project")) {
187
+ return reject(normalized, "generic_tooling");
188
+ }
189
+ if (input.source === "config_parse" && lower.startsWith("linting/formatting:")) {
190
+ return keep(normalized, toCandidateConfidence(input.confidence));
191
+ }
192
+ if (ACTIVE_COMMAND_PATTERNS.some((pattern) => pattern.test(normalized))) {
193
+ return keep(normalized, Math.max(input.confidence, 0.62));
194
+ }
195
+ if (input.type === "command") {
196
+ return keep(normalized, toCandidateConfidence(input.confidence));
197
+ }
198
+ if (input.type === "gotcha") {
199
+ if (CANDIDATE_GOTCHA_PATTERNS.some((pattern) => pattern.test(normalized))) {
200
+ return keep(normalized, toCandidateConfidence(input.confidence));
201
+ }
202
+ return reject(normalized, "generic_gotcha");
203
+ }
204
+ if (input.type === "rule") {
205
+ if (!ACTIONABLE_RULE_PATTERN.test(normalized)) {
206
+ return reject(normalized, "non_actionable_rule");
207
+ }
208
+ return keep(normalized, toCandidateConfidence(input.confidence));
209
+ }
210
+ return keep(normalized, toCandidateConfidence(input.confidence));
211
+ }
212
+ function normalizeScannedText(text) {
213
+ return text.replace(/\*\*/g, "").split("\n").map((line) => line.replace(/^[-*#>\s]+/, "").replace(/^\d+\.\s+/, "").trim()).filter(Boolean).join("\n").replace(/[ \t]+/g, " ").trim();
214
+ }
215
+ function toCandidateConfidence(confidence) {
216
+ return clamp(confidence, CONFIDENCE.TRANSIENT_MAX + 0.05, CONFIDENCE.ACTIVE_MIN - 0.01);
217
+ }
218
+ function keep(text, confidence) {
219
+ return { action: "keep", text, confidence: clamp(confidence) };
220
+ }
221
+ function reject(text, reason) {
222
+ return { action: "reject", text, confidence: 0, reason };
223
+ }
224
+ function clamp(n, min = 0, max = 1) {
225
+ return Math.max(min, Math.min(max, n));
226
+ }
227
+
228
+ // src/scanner/repo.ts
229
+ function scanRepo(repoPath) {
230
+ const repoName = inferRepoName(repoPath);
231
+ const candidates = [];
232
+ candidates.push(...scanPackageJson(repoPath, repoName));
233
+ candidates.push(...scanMakefile(repoPath, repoName));
234
+ candidates.push(...scanCIConfig(repoPath, repoName));
235
+ candidates.push(...scanInstructionFiles(repoPath, repoName));
236
+ candidates.push(...scanLinterConfigs(repoPath, repoName));
237
+ candidates.push(...scanReadme(repoPath, repoName));
238
+ candidates.push(...scanPythonProject(repoPath, repoName));
239
+ return { candidates, repo: repoName };
240
+ }
241
+ function scanAndStore(db, repoPath) {
242
+ const { candidates, repo } = scanRepo(repoPath);
243
+ const profile = getRepoQualityProfile(db, repo);
244
+ const existing = queryMemories(db, { repo }).filter((mem) => mem.status !== "rejected");
245
+ const ids = [];
246
+ for (const candidate of candidates) {
247
+ const evaluated = evaluateScannedMemory({
248
+ text: candidate.text,
249
+ type: candidate.type,
250
+ source: candidate.source,
251
+ confidence: seedScannedConfidence(
252
+ candidate.confidence ?? 0.5,
253
+ profile
254
+ )
255
+ });
256
+ if (evaluated.action === "reject") {
257
+ continue;
258
+ }
259
+ const seededConfidence = evaluated.confidence;
260
+ const normalizedCandidate = {
261
+ ...candidate,
262
+ text: evaluated.text
263
+ };
264
+ const duplicate = existing.find(
265
+ (mem) => mem.type === normalizedCandidate.type && mem.source === normalizedCandidate.source && mem.text === normalizedCandidate.text
266
+ );
267
+ if (duplicate) {
268
+ if (duplicate.confidence < seededConfidence) {
269
+ db.update(memories).set({
270
+ confidence: seededConfidence,
271
+ status: statusFromConfidence(seededConfidence),
272
+ text: normalizedCandidate.text,
273
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
274
+ }).where(eq(memories.id, duplicate.id)).run();
275
+ queueMemoryEmbeddingSync(db, duplicate.id);
276
+ }
277
+ ids.push(duplicate.id);
278
+ continue;
279
+ }
280
+ normalizedCandidate.confidence = seededConfidence;
281
+ const id = createMemory(db, normalizedCandidate);
282
+ ids.push(id);
283
+ existing.push({
284
+ ...queryMemories(db, { repo }).find((mem) => mem.id === id),
285
+ confidence: seededConfidence,
286
+ status: statusFromConfidence(seededConfidence)
287
+ });
288
+ }
289
+ return ids;
290
+ }
291
+ function scanPackageJson(repoPath, repo) {
292
+ const pkgPath = join2(repoPath, "package.json");
293
+ if (!existsSync2(pkgPath)) return [];
294
+ const results = [];
295
+ try {
296
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
297
+ if (pkg.packageManager) {
298
+ const pm = pkg.packageManager.split("@")[0];
299
+ results.push(makeCommand(
300
+ `Use ${pm} as the package manager (lockfile: ${pm === "pnpm" ? "pnpm-lock.yaml" : pm === "yarn" ? "yarn.lock" : "package-lock.json"})`,
301
+ repo,
302
+ "package.json"
303
+ ));
304
+ } else if (existsSync2(join2(repoPath, "pnpm-lock.yaml"))) {
305
+ results.push(makeCommand("Use pnpm as the package manager", repo, "package.json"));
306
+ } else if (existsSync2(join2(repoPath, "yarn.lock"))) {
307
+ results.push(makeCommand("Use yarn as the package manager", repo, "package.json"));
308
+ } else if (existsSync2(join2(repoPath, "bun.lockb")) || existsSync2(join2(repoPath, "bun.lock"))) {
309
+ results.push(makeCommand("Use bun as the package manager", repo, "package.json"));
310
+ }
311
+ const scripts = pkg.scripts ?? {};
312
+ const importantScripts = ["test", "build", "lint", "dev", "start", "typecheck", "check"];
313
+ for (const name of importantScripts) {
314
+ if (scripts[name]) {
315
+ results.push({
316
+ type: "command",
317
+ text: `${name}: \`${scripts[name]}\``,
318
+ scope: "repo",
319
+ repo,
320
+ source: "config_parse",
321
+ confidence: 0.65,
322
+ evidence: [
323
+ { type: "repo_scan", file: "package.json", timestamp: now() }
324
+ ]
325
+ });
326
+ }
327
+ }
328
+ const allDeps = {
329
+ ...pkg.dependencies,
330
+ ...pkg.devDependencies
331
+ };
332
+ if (allDeps.next) results.push(makeGotcha("Next.js project", repo, "package.json"));
333
+ if (allDeps.react && !allDeps.next) results.push(makeGotcha("React project (no Next.js)", repo, "package.json"));
334
+ if (allDeps.vue) results.push(makeGotcha("Vue.js project", repo, "package.json"));
335
+ if (allDeps.svelte) results.push(makeGotcha("Svelte project", repo, "package.json"));
336
+ if (allDeps.express || allDeps.fastify || allDeps.hono)
337
+ results.push(makeGotcha(`Server framework: ${allDeps.express ? "Express" : allDeps.fastify ? "Fastify" : "Hono"}`, repo, "package.json"));
338
+ } catch {
339
+ }
340
+ return results;
341
+ }
342
+ function scanMakefile(repoPath, repo) {
343
+ const mkPath = join2(repoPath, "Makefile");
344
+ if (!existsSync2(mkPath)) return [];
345
+ const results = [];
346
+ try {
347
+ const content = readFileSync(mkPath, "utf-8");
348
+ const targets = content.match(/^([a-zA-Z_-]+):/gm);
349
+ if (targets) {
350
+ const key = targets.map((t) => t.replace(":", "")).filter(
351
+ (t) => ["test", "build", "lint", "dev", "run", "deploy", "install", "setup", "clean"].includes(t)
352
+ );
353
+ if (key.length > 0) {
354
+ results.push({
355
+ type: "command",
356
+ text: `Makefile targets: ${key.map((t) => `\`make ${t}\``).join(", ")}`,
357
+ scope: "repo",
358
+ repo,
359
+ source: "config_parse",
360
+ confidence: 0.65,
361
+ evidence: [{ type: "repo_scan", file: "Makefile", timestamp: now() }]
362
+ });
363
+ }
364
+ }
365
+ } catch {
366
+ }
367
+ return results;
368
+ }
369
+ function scanCIConfig(repoPath, repo) {
370
+ const results = [];
371
+ const ghDir = join2(repoPath, ".github", "workflows");
372
+ if (existsSync2(ghDir)) {
373
+ results.push({
374
+ type: "gotcha",
375
+ text: "CI: GitHub Actions (check .github/workflows/ for pipeline config)",
376
+ scope: "repo",
377
+ repo,
378
+ source: "repo_scan",
379
+ confidence: 0.6,
380
+ evidence: [{ type: "repo_scan", file: ".github/workflows/", timestamp: now() }]
381
+ });
382
+ }
383
+ if (existsSync2(join2(repoPath, ".gitlab-ci.yml"))) {
384
+ results.push(makeGotcha("CI: GitLab CI", repo, ".gitlab-ci.yml"));
385
+ }
386
+ return results;
387
+ }
388
+ function scanInstructionFiles(repoPath, repo) {
389
+ const results = [];
390
+ const instructionFiles = [
391
+ "CLAUDE.md",
392
+ "AGENTS.md",
393
+ ".github/copilot-instructions.md",
394
+ ".cursorrules"
395
+ ];
396
+ for (const file of instructionFiles) {
397
+ const fPath = join2(repoPath, file);
398
+ if (!existsSync2(fPath)) continue;
399
+ try {
400
+ const content = readFileSync(fPath, "utf-8");
401
+ const rules = content.split("\n").filter(
402
+ (line) => /\b(always|never|must|don't|do not|required|forbidden)\b/i.test(line)
403
+ ).map((l) => l.replace(/^[-*#>\s]+/, "").trim()).filter((l) => l.length > 10 && l.length < 200);
404
+ for (const rule of rules.slice(0, 5)) {
405
+ results.push({
406
+ type: "rule",
407
+ text: rule,
408
+ scope: "repo",
409
+ repo,
410
+ source: "repo_scan",
411
+ confidence: 0.7,
412
+ // high — explicit instruction files
413
+ evidence: [{ type: "repo_scan", file, timestamp: now() }]
414
+ });
415
+ }
416
+ } catch {
417
+ }
418
+ }
419
+ return results;
420
+ }
421
+ function scanLinterConfigs(repoPath, repo) {
422
+ const results = [];
423
+ const configs = [
424
+ [".eslintrc.json", "ESLint"],
425
+ [".eslintrc.js", "ESLint"],
426
+ [".eslintrc.cjs", "ESLint"],
427
+ ["eslint.config.js", "ESLint (flat config)"],
428
+ ["eslint.config.mjs", "ESLint (flat config)"],
429
+ [".prettierrc", "Prettier"],
430
+ ["prettier.config.js", "Prettier"],
431
+ ["biome.json", "Biome"],
432
+ ["biome.jsonc", "Biome"],
433
+ [".rustfmt.toml", "rustfmt"],
434
+ ["ruff.toml", "Ruff"],
435
+ ["pyproject.toml", "Python project (pyproject.toml)"]
436
+ ];
437
+ const found = [];
438
+ for (const [file, name] of configs) {
439
+ if (existsSync2(join2(repoPath, file))) {
440
+ found.push(name);
441
+ }
442
+ }
443
+ if (found.length > 0) {
444
+ results.push({
445
+ type: "rule",
446
+ text: `Linting/formatting: ${[...new Set(found)].join(", ")}`,
447
+ scope: "repo",
448
+ repo,
449
+ source: "config_parse",
450
+ confidence: 0.65,
451
+ evidence: [{ type: "repo_scan", file: "config files", timestamp: now() }]
452
+ });
453
+ }
454
+ return results;
455
+ }
456
+ function scanReadme(repoPath, repo) {
457
+ const results = [];
458
+ const readmePath = join2(repoPath, "README.md");
459
+ if (!existsSync2(readmePath)) return [];
460
+ try {
461
+ const content = readFileSync(readmePath, "utf-8");
462
+ const setupMatch = content.match(
463
+ /^##\s*(setup|install|getting.started|quick.start|development)\s*\n([\s\S]*?)(?=^##\s|\z)/im
464
+ );
465
+ if (setupMatch) {
466
+ const codeBlocks = setupMatch[2].match(/```(?:sh|bash|shell|zsh)?\n([\s\S]*?)```/g);
467
+ if (codeBlocks && codeBlocks.length > 0) {
468
+ const commands = codeBlocks.map((b) => b.replace(/```(?:sh|bash|shell|zsh)?\n?/, "").replace(/```$/, "").trim()).join("\n");
469
+ if (commands.length < 500) {
470
+ results.push({
471
+ type: "command",
472
+ text: `Setup commands from README:
473
+ ${commands}`,
474
+ scope: "repo",
475
+ repo,
476
+ source: "repo_scan",
477
+ confidence: 0.5,
478
+ evidence: [{ type: "repo_scan", file: "README.md", timestamp: now() }]
479
+ });
480
+ }
481
+ }
482
+ }
483
+ } catch {
484
+ }
485
+ return results;
486
+ }
487
+ function scanPythonProject(repoPath, repo) {
488
+ const results = [];
489
+ if (existsSync2(join2(repoPath, "pyproject.toml"))) {
490
+ if (existsSync2(join2(repoPath, "uv.lock"))) {
491
+ results.push(makeCommand("Use `uv` for Python dependency management", repo, "uv.lock"));
492
+ } else if (existsSync2(join2(repoPath, "poetry.lock"))) {
493
+ results.push(makeCommand("Use `poetry` for Python dependency management", repo, "poetry.lock"));
494
+ }
495
+ if (existsSync2(join2(repoPath, "alembic.ini")) || existsSync2(join2(repoPath, "alembic"))) {
496
+ results.push(makeGotcha("Uses Alembic for database migrations", repo, "alembic.ini"));
497
+ }
498
+ }
499
+ return results;
500
+ }
501
+ function now() {
502
+ return (/* @__PURE__ */ new Date()).toISOString();
503
+ }
504
+ function inferRepoName(repoPath) {
505
+ try {
506
+ const remote = execSync("git remote get-url origin", {
507
+ cwd: repoPath,
508
+ encoding: "utf-8",
509
+ stdio: ["pipe", "pipe", "pipe"]
510
+ }).trim();
511
+ const repo = extractRepoSlugFromRemote(remote);
512
+ if (repo) return repo;
513
+ } catch {
514
+ }
515
+ return basename(repoPath);
516
+ }
517
+ function extractRepoSlugFromRemote(remote) {
518
+ const trimmed = remote.trim().replace(/\.git$/, "");
519
+ const parts = trimmed.split(/[:/]/).filter(Boolean);
520
+ if (parts.length < 2) return null;
521
+ return `${parts.at(-2)}/${parts.at(-1)}`;
522
+ }
523
+ function makeCommand(text, repo, file) {
524
+ return {
525
+ type: "command",
526
+ text,
527
+ scope: "repo",
528
+ repo,
529
+ source: "config_parse",
530
+ confidence: 0.65,
531
+ evidence: [{ type: "repo_scan", file, timestamp: now() }]
532
+ };
533
+ }
534
+ function makeGotcha(text, repo, file) {
535
+ return {
536
+ type: "gotcha",
537
+ text,
538
+ scope: "repo",
539
+ repo,
540
+ source: "repo_scan",
541
+ confidence: 0.6,
542
+ evidence: [{ type: "repo_scan", file, timestamp: now() }]
543
+ };
544
+ }
545
+
546
+ // src/history/snippets.ts
547
+ import { desc, eq as eq2, and } from "drizzle-orm";
548
+ import { randomUUID } from "crypto";
549
+ function createHistorySnippet(db, input) {
550
+ const dedupeKey = historySnippetDedupeKey(input);
551
+ const existing = db.select().from(historySnippets).where(eq2(historySnippets.dedupe_key, dedupeKey)).get();
552
+ if (existing) return existing.id;
553
+ const id = randomUUID();
554
+ const now2 = (/* @__PURE__ */ new Date()).toISOString();
555
+ db.insert(historySnippets).values({
556
+ id,
557
+ repo: input.repo ?? null,
558
+ session_id: input.session_id ?? null,
559
+ kind: input.kind,
560
+ text: input.text,
561
+ dedupe_key: dedupeKey,
562
+ source_activity_ids: input.source_activity_ids ?? [],
563
+ created_at: now2,
564
+ updated_at: now2
565
+ }).run();
566
+ return id;
567
+ }
568
+ function getHistorySnippet(db, id) {
569
+ const row = db.select().from(historySnippets).where(eq2(historySnippets.id, id)).get();
570
+ return row ? rowToHistorySnippet(row) : void 0;
571
+ }
572
+ function listHistorySnippets(db, query = {}) {
573
+ const conditions = [];
574
+ if (query.repo) conditions.push(eq2(historySnippets.repo, query.repo));
575
+ if (query.session_id) conditions.push(eq2(historySnippets.session_id, query.session_id));
576
+ if (query.kind) conditions.push(eq2(historySnippets.kind, query.kind));
577
+ let stmt = db.select().from(historySnippets).$dynamic();
578
+ if (conditions.length > 0) {
579
+ stmt = stmt.where(and(...conditions));
580
+ }
581
+ stmt = stmt.orderBy(desc(historySnippets.updated_at));
582
+ if (query.limit != null) {
583
+ stmt = stmt.limit(query.limit);
584
+ }
585
+ return stmt.all().map(rowToHistorySnippet);
586
+ }
587
+ function findHistorySnippetBySession(db, sessionId, kind = "session_summary") {
588
+ const row = db.select().from(historySnippets).where(and(
589
+ eq2(historySnippets.session_id, sessionId),
590
+ eq2(historySnippets.kind, kind)
591
+ )).get();
592
+ return row ? rowToHistorySnippet(row) : void 0;
593
+ }
594
+ function findHistorySnippetByRepoKind(db, repo, kind) {
595
+ const row = db.select().from(historySnippets).where(and(
596
+ eq2(historySnippets.repo, repo),
597
+ eq2(historySnippets.kind, kind)
598
+ )).get();
599
+ return row ? rowToHistorySnippet(row) : void 0;
600
+ }
601
+ function updateHistorySnippet(db, id, updates) {
602
+ const current = getHistorySnippet(db, id);
603
+ if (!current) return;
604
+ const nextText = updates.text ?? current.text;
605
+ const dedupeKey = historySnippetDedupeKey({
606
+ repo: current.repo,
607
+ session_id: current.session_id,
608
+ kind: current.kind,
609
+ text: nextText
610
+ });
611
+ const collision = db.select().from(historySnippets).where(eq2(historySnippets.dedupe_key, dedupeKey)).get();
612
+ if (collision && collision.id !== id) return;
613
+ db.update(historySnippets).set({
614
+ ...updates.text != null ? { text: updates.text } : {},
615
+ dedupe_key: dedupeKey,
616
+ ...updates.source_activity_ids ? { source_activity_ids: updates.source_activity_ids } : {},
617
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
618
+ }).where(eq2(historySnippets.id, id)).run();
619
+ }
620
+ function rowToHistorySnippet(row) {
621
+ const sourceActivityIds = typeof row.source_activity_ids === "string" ? JSON.parse(row.source_activity_ids) : Array.isArray(row.source_activity_ids) ? row.source_activity_ids : [];
622
+ return {
623
+ id: row.id,
624
+ repo: row.repo,
625
+ session_id: row.session_id,
626
+ kind: row.kind,
627
+ text: row.text,
628
+ source_activity_ids: sourceActivityIds,
629
+ created_at: row.created_at,
630
+ updated_at: row.updated_at
631
+ };
632
+ }
633
+
634
+ // src/history/retrieval.ts
635
+ import { createHash } from "crypto";
636
+ import { eq as eq5 } from "drizzle-orm";
637
+
638
+ // src/vector/sqlite-vec-history.ts
639
+ import * as sqliteVec from "sqlite-vec";
640
+ import { eq as eq3 } from "drizzle-orm";
641
+ var VEC_HISTORY_INDEX = "vec_history_index";
642
+ var loadedClients = /* @__PURE__ */ new WeakSet();
643
+ function getSqlite(db) {
644
+ return db.$client;
645
+ }
646
+ function hasHistoryVecIndex(db) {
647
+ return Boolean(
648
+ getSqlite(db).prepare("select 1 from sqlite_master where type = 'table' and name = ?").get(VEC_HISTORY_INDEX)
649
+ );
650
+ }
651
+ function ensureLoaded(db) {
652
+ const sqlite = getSqlite(db);
653
+ if (loadedClients.has(sqlite)) return;
654
+ sqliteVec.load(sqlite);
655
+ loadedClients.add(sqlite);
656
+ }
657
+ function getHistoryVecDimension(rows) {
658
+ const dimensions = [...new Set(rows.map((row) => row.index_dimensions))];
659
+ if (dimensions.length === 0) return null;
660
+ if (dimensions.length > 1) {
661
+ throw new Error(
662
+ `sqlite-vec history index rebuild refused mixed history embedding dimensions: ${dimensions.join(", ")}.`
663
+ );
664
+ }
665
+ return dimensions[0];
666
+ }
667
+ function ensureHistoryVecIndex(db, dimensions) {
668
+ ensureLoaded(db);
669
+ const sqlite = getSqlite(db);
670
+ const existing = sqlite.prepare("select sql from sqlite_master where type = 'table' and name = ?").get(VEC_HISTORY_INDEX);
671
+ const expectedDimension = `float[${dimensions}]`;
672
+ if (existing?.sql && !existing.sql.includes(expectedDimension)) {
673
+ throw new Error(
674
+ `sqlite-vec history index dimension mismatch. Expected ${expectedDimension}. Run history index rebuild.`
675
+ );
676
+ }
677
+ sqlite.exec(`
678
+ create virtual table if not exists ${VEC_HISTORY_INDEX} using vec0(
679
+ embedding float[${dimensions}] distance_metric=cosine,
680
+ snippet_id text,
681
+ repo text,
682
+ kind text
683
+ );
684
+ `);
685
+ }
686
+ function removeHistoryVecRow(db, snippetId) {
687
+ ensureLoaded(db);
688
+ if (!hasHistoryVecIndex(db)) return;
689
+ getSqlite(db).prepare(`delete from ${VEC_HISTORY_INDEX} where snippet_id = ?`).run(snippetId);
690
+ }
691
+ function rebuildHistoryVecIndex(db, config, options = {}) {
692
+ const rows = db.select({
693
+ id: historySnippets.id,
694
+ repo: historySnippets.repo,
695
+ kind: historySnippets.kind,
696
+ index_dimensions: historySnippetEmbeddings.index_dimensions,
697
+ embedding: historySnippetEmbeddings.embedding
698
+ }).from(historySnippets).innerJoin(historySnippetEmbeddings, eq3(historySnippetEmbeddings.snippet_id, historySnippets.id)).all().filter((row) => !options.repo || row.repo === options.repo);
699
+ const storedDimension = getHistoryVecDimension(rows);
700
+ const targetDimension = storedDimension ?? config.dimensions;
701
+ const sqlite = getSqlite(db);
702
+ if (options.repo) {
703
+ if (rows.length > 0) {
704
+ ensureHistoryVecIndex(db, targetDimension);
705
+ }
706
+ if (!hasHistoryVecIndex(db)) return 0;
707
+ sqlite.prepare(`delete from ${VEC_HISTORY_INDEX} where repo = ?`).run(options.repo);
708
+ if (rows.length === 0) return 0;
709
+ } else {
710
+ sqlite.exec(`drop table if exists ${VEC_HISTORY_INDEX};`);
711
+ ensureHistoryVecIndex(db, targetDimension);
712
+ }
713
+ const stmt = getSqlite(db).prepare(`
714
+ insert into ${VEC_HISTORY_INDEX} (
715
+ embedding,
716
+ snippet_id,
717
+ repo,
718
+ kind
719
+ ) values (?, ?, ?, ?)
720
+ `);
721
+ const insertMany = getSqlite(db).transaction((batch) => {
722
+ for (const row of batch) {
723
+ stmt.run(projectIndexBuffer(row.embedding, row.index_dimensions), row.id, row.repo ?? "", row.kind);
724
+ }
725
+ });
726
+ insertMany(rows);
727
+ return rows.length;
728
+ }
729
+ function projectIndexBuffer(buffer, indexDimensions) {
730
+ const embedding = new Float32Array(
731
+ buffer.buffer,
732
+ buffer.byteOffset,
733
+ buffer.byteLength / Float32Array.BYTES_PER_ELEMENT
734
+ );
735
+ if (embedding.length === indexDimensions) {
736
+ return buffer;
737
+ }
738
+ if (embedding.length < indexDimensions) {
739
+ throw new Error(`Canonical history embedding width ${embedding.length} is smaller than index width ${indexDimensions}.`);
740
+ }
741
+ const sliced = embedding.slice(0, indexDimensions);
742
+ let norm = 0;
743
+ for (const value of sliced) norm += value * value;
744
+ const scale = Math.sqrt(norm) || 1;
745
+ for (let i = 0; i < sliced.length; i++) {
746
+ sliced[i] /= scale;
747
+ }
748
+ return Buffer.from(sliced.buffer, sliced.byteOffset, sliced.byteLength);
749
+ }
750
+ function verifyHistoryVecIndex(db, options = {}) {
751
+ ensureLoaded(db);
752
+ const sqlite = getSqlite(db);
753
+ const exists = sqlite.prepare("select 1 from sqlite_master where type = 'table' and name = ?").get(VEC_HISTORY_INDEX);
754
+ const expected = db.select({
755
+ snippet_id: historySnippetEmbeddings.snippet_id,
756
+ repo: historySnippets.repo
757
+ }).from(historySnippetEmbeddings).innerJoin(historySnippets, eq3(historySnippets.id, historySnippetEmbeddings.snippet_id)).all().filter((row) => !options.repo || row.repo === options.repo).length;
758
+ let indexed = 0;
759
+ if (exists) {
760
+ if (options.repo) {
761
+ indexed = sqlite.prepare(`select count(*) as count from ${VEC_HISTORY_INDEX} where repo = ?`).get(options.repo).count;
762
+ } else {
763
+ indexed = sqlite.prepare(`select count(*) as count from ${VEC_HISTORY_INDEX}`).get().count;
764
+ }
765
+ }
766
+ return { expected, indexed, drift: expected - indexed };
767
+ }
768
+ function searchHistoryVecIndex(db, queryEmbedding, options = {}) {
769
+ ensureLoaded(db);
770
+ const sqlite = getSqlite(db);
771
+ const exists = sqlite.prepare("select 1 from sqlite_master where type = 'table' and name = ?").get(VEC_HISTORY_INDEX);
772
+ if (!exists) return [];
773
+ const limit = options.limit ?? 10;
774
+ if (options.repo) {
775
+ return sqlite.prepare(`
776
+ select snippet_id, distance
777
+ from ${VEC_HISTORY_INDEX}
778
+ where embedding match ?
779
+ and k = ?
780
+ and repo = ?
781
+ order by distance
782
+ `).all(queryEmbedding, limit, options.repo);
783
+ }
784
+ return sqlite.prepare(`
785
+ select snippet_id, distance
786
+ from ${VEC_HISTORY_INDEX}
787
+ where embedding match ?
788
+ and k = ?
789
+ order by distance
790
+ `).all(queryEmbedding, limit);
791
+ }
792
+
793
+ // src/vector/sqlite-fts-history.ts
794
+ import { eq as eq4 } from "drizzle-orm";
795
+ var FTS_HISTORY_INDEX = "fts_history_index";
796
+ function getSqlite2(db) {
797
+ return db.$client;
798
+ }
799
+ function ensureHistoryFtsIndex(db) {
800
+ getSqlite2(db).exec(`
801
+ create virtual table if not exists ${FTS_HISTORY_INDEX} using fts5(
802
+ snippet_id UNINDEXED,
803
+ text,
804
+ repo UNINDEXED,
805
+ kind UNINDEXED
806
+ );
807
+ `);
808
+ }
809
+ function removeHistoryFtsRow(db, snippetId) {
810
+ const sqlite = getSqlite2(db);
811
+ const exists = sqlite.prepare("select 1 from sqlite_master where type = 'table' and name = ?").get(FTS_HISTORY_INDEX);
812
+ if (!exists) return;
813
+ sqlite.prepare(`delete from ${FTS_HISTORY_INDEX} where snippet_id = ?`).run(snippetId);
814
+ }
815
+ function upsertHistoryFtsRow(db, snippet) {
816
+ ensureHistoryFtsIndex(db);
817
+ const sqlite = getSqlite2(db);
818
+ sqlite.prepare(`delete from ${FTS_HISTORY_INDEX} where snippet_id = ?`).run(snippet.id);
819
+ sqlite.prepare(`
820
+ insert into ${FTS_HISTORY_INDEX} (
821
+ snippet_id,
822
+ text,
823
+ repo,
824
+ kind
825
+ ) values (?, ?, ?, ?)
826
+ `).run(snippet.id, snippet.text, snippet.repo ?? "", snippet.kind);
827
+ }
828
+ function syncHistoryFtsIndex(db, snippetId) {
829
+ const snippet = db.select().from(historySnippets).where(eq4(historySnippets.id, snippetId)).get();
830
+ if (!snippet) {
831
+ removeHistoryFtsRow(db, snippetId);
832
+ return "removed";
833
+ }
834
+ upsertHistoryFtsRow(db, snippet);
835
+ return "stored";
836
+ }
837
+ function rebuildHistoryFtsIndex(db, options = {}) {
838
+ const sqlite = getSqlite2(db);
839
+ if (options.repo) {
840
+ ensureHistoryFtsIndex(db);
841
+ sqlite.prepare(`delete from ${FTS_HISTORY_INDEX} where repo = ?`).run(options.repo);
842
+ } else {
843
+ sqlite.exec(`drop table if exists ${FTS_HISTORY_INDEX};`);
844
+ ensureHistoryFtsIndex(db);
845
+ }
846
+ const rows = db.select().from(historySnippets).all().filter((row) => !options.repo || row.repo === options.repo);
847
+ const stmt = sqlite.prepare(`
848
+ insert into ${FTS_HISTORY_INDEX} (
849
+ snippet_id,
850
+ text,
851
+ repo,
852
+ kind
853
+ ) values (?, ?, ?, ?)
854
+ `);
855
+ const insertMany = sqlite.transaction((batch) => {
856
+ for (const row of batch) {
857
+ stmt.run(row.id, row.text, row.repo ?? "", row.kind);
858
+ }
859
+ });
860
+ insertMany(rows);
861
+ return rows.length;
862
+ }
863
+ function verifyHistoryFtsIndex(db, options = {}) {
864
+ const sqlite = getSqlite2(db);
865
+ const exists = sqlite.prepare("select 1 from sqlite_master where type = 'table' and name = ?").get(FTS_HISTORY_INDEX);
866
+ const expected = db.select().from(historySnippets).all().filter((row) => !options.repo || row.repo === options.repo).length;
867
+ let indexed = 0;
868
+ if (exists) {
869
+ if (options.repo) {
870
+ indexed = sqlite.prepare(`select count(*) as count from ${FTS_HISTORY_INDEX} where repo = ?`).get(options.repo).count;
871
+ } else {
872
+ indexed = sqlite.prepare(`select count(*) as count from ${FTS_HISTORY_INDEX}`).get().count;
873
+ }
874
+ }
875
+ return { expected, indexed, drift: expected - indexed };
876
+ }
877
+ function buildFtsQuery(query) {
878
+ const tokens = query.match(/[A-Za-z0-9_.:/-]+/g)?.map((token) => token.replace(/"/g, '""')).filter(Boolean) ?? [];
879
+ if (tokens.length === 0) return null;
880
+ return tokens.map((token) => `"${token}"`).join(" ");
881
+ }
882
+ function searchHistoryFtsIndex(db, query, options = {}) {
883
+ ensureHistoryFtsIndex(db);
884
+ const ftsQuery = buildFtsQuery(query);
885
+ if (!ftsQuery) return [];
886
+ const sqlite = getSqlite2(db);
887
+ const limit = options.limit ?? 10;
888
+ if (options.repo) {
889
+ return sqlite.prepare(`
890
+ select snippet_id, bm25(${FTS_HISTORY_INDEX}) as lexical_rank
891
+ from ${FTS_HISTORY_INDEX}
892
+ where ${FTS_HISTORY_INDEX} match ?
893
+ and repo = ?
894
+ order by lexical_rank
895
+ limit ?
896
+ `).all(ftsQuery, options.repo, limit);
897
+ }
898
+ return sqlite.prepare(`
899
+ select snippet_id, bm25(${FTS_HISTORY_INDEX}) as lexical_rank
900
+ from ${FTS_HISTORY_INDEX}
901
+ where ${FTS_HISTORY_INDEX} match ?
902
+ order by lexical_rank
903
+ limit ?
904
+ `).all(ftsQuery, limit);
905
+ }
906
+
907
+ // src/history/retrieval.ts
908
+ function hashText(text) {
909
+ return createHash("sha256").update(text).digest("hex");
910
+ }
911
+ function version(config) {
912
+ return config.version || `${config.provider}:${config.model}:${config.dimensions}`;
913
+ }
914
+ function rowNeedsRefresh(row, existing, config) {
915
+ const metadata = resolveProvider(config).metadata();
916
+ if (!existing) return true;
917
+ return existing.model !== config.model || existing.embedding_dimensions !== metadata.canonical_dimensions || existing.index_dimensions !== metadata.index_dimensions || existing.version !== version(config) || existing.content_hash !== hashText(row.text);
918
+ }
919
+ function rowToHistorySnippet2(row) {
920
+ const sourceActivityIds = typeof row.source_activity_ids === "string" ? JSON.parse(row.source_activity_ids) : Array.isArray(row.source_activity_ids) ? row.source_activity_ids : [];
921
+ return {
922
+ id: row.id,
923
+ repo: row.repo,
924
+ session_id: row.session_id,
925
+ kind: row.kind,
926
+ text: row.text,
927
+ source_activity_ids: sourceActivityIds,
928
+ created_at: row.created_at,
929
+ updated_at: row.updated_at
930
+ };
931
+ }
932
+ function storeHistoryEmbedding(db, snippetId, text, embedding, config) {
933
+ const now2 = (/* @__PURE__ */ new Date()).toISOString();
934
+ const metadata = resolveProvider(config).metadata();
935
+ const payload = {
936
+ snippet_id: snippetId,
937
+ model: config.model,
938
+ embedding_dimensions: metadata.canonical_dimensions,
939
+ index_dimensions: metadata.index_dimensions,
940
+ version: version(config),
941
+ content_hash: hashText(text),
942
+ updated_at: now2,
943
+ embedding: Buffer.from(embedding.buffer, embedding.byteOffset, embedding.byteLength)
944
+ };
945
+ db.insert(historySnippetEmbeddings).values(payload).onConflictDoUpdate({
946
+ target: historySnippetEmbeddings.snippet_id,
947
+ set: {
948
+ model: payload.model,
949
+ embedding_dimensions: payload.embedding_dimensions,
950
+ index_dimensions: payload.index_dimensions,
951
+ version: payload.version,
952
+ content_hash: payload.content_hash,
953
+ updated_at: payload.updated_at,
954
+ embedding: payload.embedding
955
+ }
956
+ }).run();
957
+ }
958
+ async function bootstrapHistoryEmbeddings(db, config, options = {}) {
959
+ const rows = db.select().from(historySnippets).all().filter((row) => !options.repo || row.repo === options.repo);
960
+ const existing = new Map(
961
+ db.select().from(historySnippetEmbeddings).all().map((row) => [row.snippet_id, row])
962
+ );
963
+ const pending = rows.filter((row) => rowNeedsRefresh(row, existing.get(row.id), config));
964
+ for (const row of rows) {
965
+ syncHistoryFtsIndex(db, row.id);
966
+ }
967
+ const BATCH_SIZE = 100;
968
+ let total = 0;
969
+ for (let i = 0; i < pending.length; i += BATCH_SIZE) {
970
+ const batch = pending.slice(i, i + BATCH_SIZE);
971
+ const embeddings = await generateEmbeddings(batch.map((row) => row.text), config, "document");
972
+ for (let j = 0; j < batch.length; j++) {
973
+ storeHistoryEmbedding(db, batch[j].id, batch[j].text, embeddings[j], config);
974
+ total++;
975
+ }
976
+ }
977
+ rebuildHistoryFtsIndex(db, options);
978
+ rebuildHistoryVecIndex(db, config, options);
979
+ return total;
980
+ }
981
+ function verifyHistoryEmbeddings(db, config, options = {}) {
982
+ const rows = db.select().from(historySnippets).all().filter((row) => !options.repo || row.repo === options.repo);
983
+ const embeddings = db.select().from(historySnippetEmbeddings).all();
984
+ const byId = new Map(embeddings.map((row) => [row.snippet_id, row]));
985
+ let eligible = 0;
986
+ let stale = 0;
987
+ for (const row of rows) {
988
+ eligible++;
989
+ if (rowNeedsRefresh(row, byId.get(row.id), config)) stale++;
990
+ }
991
+ const vec = verifyHistoryVecIndex(db, options);
992
+ const fts = verifyHistoryFtsIndex(db, options);
993
+ return {
994
+ eligible,
995
+ stored: embeddings.filter((row) => {
996
+ if (!options.repo) return true;
997
+ const snippet = rows.find((item) => item.id === row.snippet_id);
998
+ return snippet?.repo === options.repo;
999
+ }).length,
1000
+ stale,
1001
+ indexed: vec.indexed,
1002
+ index_drift: vec.drift,
1003
+ lexical_indexed: fts.indexed,
1004
+ lexical_drift: fts.drift
1005
+ };
1006
+ }
1007
+ function lexicalRankToScore(rank, position) {
1008
+ const safeRank = Number.isFinite(rank) ? Math.abs(rank) : position + 1;
1009
+ return 1 / (1 + safeRank + position);
1010
+ }
1011
+ async function searchHistorySnippets(db, query, options = {}) {
1012
+ const limit = options.limit ?? 10;
1013
+ const lexicalMatches = searchHistoryFtsIndex(db, query, {
1014
+ repo: options.repo,
1015
+ limit: Math.max(limit * 2, 20)
1016
+ });
1017
+ const config = loadEmbeddingConfigFromEnv();
1018
+ const vectorMatches = config ? searchHistoryVecIndex(db, projectEmbeddingToIndex(
1019
+ await generateEmbedding(query, config, "query"),
1020
+ resolveProvider(config).metadata().index_dimensions
1021
+ ), {
1022
+ repo: options.repo,
1023
+ limit: Math.max(limit * 2, 20)
1024
+ }) : [];
1025
+ const rowsById = new Map(
1026
+ db.select().from(historySnippets).all().map((row) => [row.id, row])
1027
+ );
1028
+ const merged = /* @__PURE__ */ new Map();
1029
+ for (let i = 0; i < lexicalMatches.length; i++) {
1030
+ const match = lexicalMatches[i];
1031
+ const row = rowsById.get(match.snippet_id);
1032
+ if (!row) continue;
1033
+ const lexicalScore = lexicalRankToScore(match.lexical_rank, i);
1034
+ merged.set(match.snippet_id, {
1035
+ snippet: rowToHistorySnippet2(row),
1036
+ score: lexicalScore * 0.35,
1037
+ similarity: 0,
1038
+ lexical_score: lexicalScore
1039
+ });
1040
+ }
1041
+ for (const match of vectorMatches) {
1042
+ const row = rowsById.get(match.snippet_id);
1043
+ if (!row) continue;
1044
+ const similarity = Math.max(0, 1 - match.distance);
1045
+ const existing = merged.get(match.snippet_id);
1046
+ if (existing) {
1047
+ existing.similarity = similarity;
1048
+ existing.score = similarity * 0.65 + existing.lexical_score * 0.35;
1049
+ } else {
1050
+ merged.set(match.snippet_id, {
1051
+ snippet: rowToHistorySnippet2(row),
1052
+ score: similarity * 0.65,
1053
+ similarity,
1054
+ lexical_score: 0
1055
+ });
1056
+ }
1057
+ }
1058
+ return [...merged.values()].sort((a, b) => b.score - a.score).slice(0, limit);
1059
+ }
1060
+
1061
+ // src/models/memory-injections.ts
1062
+ import { and as and2, asc, eq as eq6, gt, isNull } from "drizzle-orm";
1063
+ import { randomUUID as randomUUID2 } from "crypto";
1064
+ function recordMemoryInjections(db, input) {
1065
+ if (!input.session_id || input.memory_ids.length === 0) return 0;
1066
+ const injectedAt = (/* @__PURE__ */ new Date()).toISOString();
1067
+ let inserted = 0;
1068
+ for (const memoryId of input.memory_ids) {
1069
+ const result = db.insert(memoryInjections).values({
1070
+ id: randomUUID2(),
1071
+ memory_id: memoryId,
1072
+ session_id: input.session_id,
1073
+ repo: input.repo ?? null,
1074
+ injected_at: injectedAt,
1075
+ outcome: null,
1076
+ outcome_at: null
1077
+ }).onConflictDoNothing({
1078
+ target: [memoryInjections.memory_id, memoryInjections.session_id]
1079
+ }).run();
1080
+ inserted += Number(result.changes ?? 0);
1081
+ }
1082
+ return inserted;
1083
+ }
1084
+ function listInjectedMemoryIdsForSession(db, sessionId) {
1085
+ const rows = db.select({ memory_id: memoryInjections.memory_id }).from(memoryInjections).where(eq6(memoryInjections.session_id, sessionId)).all();
1086
+ return new Set(rows.map((row) => row.memory_id));
1087
+ }
1088
+ function listPendingMemoryInjections(db, sessionId) {
1089
+ const rows = db.select().from(memoryInjections).where(and2(
1090
+ eq6(memoryInjections.session_id, sessionId),
1091
+ isNull(memoryInjections.outcome)
1092
+ )).orderBy(asc(memoryInjections.injected_at)).all();
1093
+ return rows.map((row) => ({
1094
+ ...rowToMemoryInjection(row),
1095
+ memory: getMemory(db, row.memory_id) ?? null
1096
+ }));
1097
+ }
1098
+ function resolveMemoryInjectionOutcome(db, memoryId, sessionId, outcome) {
1099
+ const outcomeAt = (/* @__PURE__ */ new Date()).toISOString();
1100
+ const result = db.update(memoryInjections).set({
1101
+ outcome,
1102
+ outcome_at: outcomeAt
1103
+ }).where(and2(
1104
+ eq6(memoryInjections.memory_id, memoryId),
1105
+ eq6(memoryInjections.session_id, sessionId),
1106
+ isNull(memoryInjections.outcome)
1107
+ )).run();
1108
+ return Number(result.changes ?? 0) > 0;
1109
+ }
1110
+ function pathMatchesMemory(mem, targetPath) {
1111
+ if (!targetPath) return mem.scope === "repo" || mem.scope === "team";
1112
+ if (mem.scope === "repo" || mem.scope === "team") return true;
1113
+ if (!mem.path_scope) return true;
1114
+ const pattern = mem.path_scope;
1115
+ if (pattern.endsWith("**")) {
1116
+ return targetPath.startsWith(pattern.slice(0, -2));
1117
+ }
1118
+ if (pattern.includes("*")) {
1119
+ const regex = new RegExp(
1120
+ "^" + pattern.replace(/\*/g, "[^/]*").replace(/\*\*/g, ".*") + "$"
1121
+ );
1122
+ return regex.test(targetPath);
1123
+ }
1124
+ return targetPath.startsWith(pattern);
1125
+ }
1126
+ function toolCallTouchesMemory(mem, toolCall) {
1127
+ if (toolCall.path && pathMatchesMemory(mem, toolCall.path)) return true;
1128
+ if (toolCall.input_summary) {
1129
+ const inferredPath = extractPath(toolCall.input_summary);
1130
+ if (inferredPath && pathMatchesMemory(mem, inferredPath)) return true;
1131
+ }
1132
+ return mem.scope === "repo" || mem.scope === "team";
1133
+ }
1134
+ function rowToMemoryInjection(row) {
1135
+ return {
1136
+ id: row.id,
1137
+ memory_id: row.memory_id,
1138
+ session_id: row.session_id,
1139
+ repo: row.repo,
1140
+ injected_at: row.injected_at,
1141
+ outcome: row.outcome,
1142
+ outcome_at: row.outcome_at
1143
+ };
1144
+ }
1145
+ function extractPath(text) {
1146
+ const match = text.match(
1147
+ /\b((?:src|lib|app|components|utils|test|spec)\/[\w./-]+|[\w./-]+\.(?:ts|tsx|js|jsx|py|rs|go|swift|java|rb|json|toml|ya?ml))\b/
1148
+ );
1149
+ return match?.[1];
1150
+ }
1151
+
1152
+ // src/models/history-injections.ts
1153
+ import { eq as eq7 } from "drizzle-orm";
1154
+ import { randomUUID as randomUUID3 } from "crypto";
1155
+ function recordHistoryInjections(db, input) {
1156
+ if (!input.session_id || input.snippet_ids.length === 0) return 0;
1157
+ const injectedAt = (/* @__PURE__ */ new Date()).toISOString();
1158
+ let inserted = 0;
1159
+ for (const snippetId of input.snippet_ids) {
1160
+ const result = db.insert(historyInjections).values({
1161
+ id: randomUUID3(),
1162
+ snippet_id: snippetId,
1163
+ session_id: input.session_id,
1164
+ repo: input.repo ?? null,
1165
+ injected_at: injectedAt
1166
+ }).onConflictDoNothing({
1167
+ target: [historyInjections.snippet_id, historyInjections.session_id]
1168
+ }).run();
1169
+ inserted += Number(result.changes ?? 0);
1170
+ }
1171
+ return inserted;
1172
+ }
1173
+ function listInjectedHistoryIdsForSession(db, sessionId) {
1174
+ const rows = db.select({ snippet_id: historyInjections.snippet_id }).from(historyInjections).where(eq7(historyInjections.session_id, sessionId)).all();
1175
+ return new Set(rows.map((row) => row.snippet_id));
1176
+ }
1177
+
1178
+ // src/compiler/context.ts
1179
+ var DEFAULT_CONFIG = {
1180
+ confidence_threshold: CONFIDENCE.ACTIVE_MIN,
1181
+ max_lines: 15,
1182
+ max_commands: 3,
1183
+ max_gotchas: 3,
1184
+ max_history_snippets: 2,
1185
+ token_budget: 2e3,
1186
+ include_candidates: false
1187
+ };
1188
+ var QUERY_RESULT_LIMIT = 2;
1189
+ var QUERY_VECTOR_RELEVANCE_FLOOR = 0.7;
1190
+ function compileContext(db, req) {
1191
+ const profile = getRepoQualityProfile(db, req.repo);
1192
+ const config = {
1193
+ ...DEFAULT_CONFIG,
1194
+ ...req.config,
1195
+ confidence_threshold: req.config?.confidence_threshold ?? profile.compile_confidence_threshold
1196
+ };
1197
+ const selectedHistory = selectRepoHistory(db, req.repo, config.max_history_snippets);
1198
+ const repoActive = queryMemories(db, {
1199
+ repo: req.repo,
1200
+ status: "active",
1201
+ auto_inject: true
1202
+ });
1203
+ const globalActive = queryMemories(db, {
1204
+ scope: "global",
1205
+ status: "active",
1206
+ auto_inject: true
1207
+ });
1208
+ const allActive = dedupeById([...repoActive, ...globalActive]);
1209
+ const scoped = req.path ? allActive.filter((m) => pathMatches(m, req.path)) : allActive;
1210
+ const passing = scoped.filter(
1211
+ (m) => m.confidence >= config.confidence_threshold
1212
+ );
1213
+ const dropped = scoped.filter(
1214
+ (m) => m.confidence < config.confidence_threshold
1215
+ );
1216
+ if (passing.length === 0 && selectedHistory.length === 0) {
1217
+ return {
1218
+ text: "",
1219
+ memories_included: [],
1220
+ memories_dropped: dropped.map((m) => m.id),
1221
+ history_included: [],
1222
+ token_estimate: 0
1223
+ };
1224
+ }
1225
+ const summaries = getMemoryFeedbackSummaries(db, passing.map((m) => m.id));
1226
+ const scored = passing.map((m) => ({
1227
+ mem: m,
1228
+ score: feedbackWeightedScore(m.confidence, summaries.get(m.id) ?? {
1229
+ followed: 0,
1230
+ overridden: 0,
1231
+ contradicted: 0,
1232
+ ignored: 0,
1233
+ resolved: 0
1234
+ })
1235
+ }));
1236
+ const sorted = scored.sort((a, b) => {
1237
+ const typePrio = typePriority(a.mem.type) - typePriority(b.mem.type);
1238
+ if (typePrio !== 0) return typePrio;
1239
+ return b.score - a.score;
1240
+ }).map((s) => s.mem);
1241
+ const deduped = dedupeMemoriesForInjection(sorted);
1242
+ const selected = [];
1243
+ let commandCount = 0;
1244
+ let gotchaCount = 0;
1245
+ let lineCount = 0;
1246
+ for (const mem of deduped) {
1247
+ const memLines = renderMemoryText(mem).split("\n").length;
1248
+ if (lineCount + memLines > config.max_lines) continue;
1249
+ if (mem.type === "command" && commandCount >= config.max_commands) continue;
1250
+ if (mem.type === "gotcha" && gotchaCount >= config.max_gotchas) continue;
1251
+ selected.push(mem);
1252
+ lineCount += memLines;
1253
+ if (mem.type === "command") commandCount++;
1254
+ if (mem.type === "gotcha") gotchaCount++;
1255
+ }
1256
+ const text = renderPack(selected, req.repo, selectedHistory);
1257
+ const tokenEstimate = Math.ceil(text.length / 4);
1258
+ if (tokenEstimate > config.token_budget) {
1259
+ while (selected.length > 1 && Math.ceil(renderPack(selected, req.repo, selectedHistory).length / 4) > config.token_budget) {
1260
+ selected.pop();
1261
+ }
1262
+ }
1263
+ const finalText = renderPack(selected, req.repo, selectedHistory);
1264
+ recordMemoryInjections(db, {
1265
+ memory_ids: selected.map((memory) => memory.id),
1266
+ session_id: req.session_id,
1267
+ repo: req.repo
1268
+ });
1269
+ recordHistoryInjections(db, {
1270
+ snippet_ids: selectedHistory.map((snippet) => snippet.id),
1271
+ session_id: req.session_id,
1272
+ repo: req.repo
1273
+ });
1274
+ return {
1275
+ text: finalText,
1276
+ memories_included: selected.map((m) => m.id),
1277
+ memories_dropped: [
1278
+ ...dropped.map((m) => m.id),
1279
+ ...sorted.filter((m) => !selected.includes(m)).map((m) => m.id)
1280
+ ],
1281
+ history_included: selectedHistory.map((snippet) => snippet.id),
1282
+ token_estimate: Math.ceil(finalText.length / 4)
1283
+ };
1284
+ }
1285
+ async function compileContextHybrid(db, req) {
1286
+ const embeddingConfig = req.embedding_config ?? loadEmbeddingConfigFromEnv();
1287
+ const profile = getRepoQualityProfile(db, req.repo);
1288
+ const config = {
1289
+ ...DEFAULT_CONFIG,
1290
+ ...req.config,
1291
+ confidence_threshold: req.config?.confidence_threshold ?? profile.compile_confidence_threshold
1292
+ };
1293
+ const selectedHistory = req.query_text ? await selectRelevantHistory(db, req.repo, req.query_text, config.max_history_snippets) : selectRepoHistory(db, req.repo, config.max_history_snippets);
1294
+ const repoMemories = queryMemories(db, { repo: req.repo });
1295
+ const globalMemories = queryMemories(db, { scope: "global" });
1296
+ const allMemories = dedupeById([...repoMemories, ...globalMemories]).filter(
1297
+ (memory) => memory.auto_inject && (memory.status === "active" || config.include_candidates && memory.status === "candidate")
1298
+ );
1299
+ const scoped = req.path ? allMemories.filter((memory) => pathMatches(memory, req.path)) : allMemories;
1300
+ const candidateConfidenceFloor = Math.min(config.confidence_threshold, 0.45);
1301
+ const passing = scoped.filter((memory) => {
1302
+ if (memory.status === "active") {
1303
+ return memory.confidence >= config.confidence_threshold;
1304
+ }
1305
+ if (memory.status === "candidate" && config.include_candidates) {
1306
+ return memory.confidence >= candidateConfidenceFloor;
1307
+ }
1308
+ return false;
1309
+ });
1310
+ const dropped = scoped.filter((memory) => !passing.includes(memory));
1311
+ if (passing.length === 0 && selectedHistory.length === 0) {
1312
+ return {
1313
+ text: "",
1314
+ memories_included: [],
1315
+ memories_dropped: dropped.map((m) => m.id),
1316
+ history_included: [],
1317
+ token_estimate: 0
1318
+ };
1319
+ }
1320
+ const retrieval = req.query_text ? await hybridSearch(db, req.query_text, embeddingConfig, {
1321
+ repo: req.repo,
1322
+ limit: QUERY_RESULT_LIMIT
1323
+ }) : [];
1324
+ const retrievalById = new Map(
1325
+ retrieval.map((item) => [item.memory.id, item])
1326
+ );
1327
+ const summaries = getMemoryFeedbackSummaries(db, passing.map((m) => m.id));
1328
+ const emptySummary = { followed: 0, overridden: 0, contradicted: 0, ignored: 0, resolved: 0 };
1329
+ const ranked = passing.filter((memory) => {
1330
+ const retrievalItem = retrievalById.get(memory.id);
1331
+ if (req.query_text) {
1332
+ if (!retrievalItem) return false;
1333
+ if (embeddingConfig && retrievalItem.similarity < QUERY_VECTOR_RELEVANCE_FLOOR) {
1334
+ return false;
1335
+ }
1336
+ return true;
1337
+ }
1338
+ if (memory.status !== "candidate") return true;
1339
+ const retrievalScore = retrievalItem?.score ?? 0;
1340
+ return retrievalScore >= 0.2;
1341
+ }).map((memory) => {
1342
+ const retrievalScore = retrievalById.get(memory.id)?.score ?? 0;
1343
+ const weighted = feedbackWeightedScore(memory.confidence, summaries.get(memory.id) ?? emptySummary);
1344
+ const score = req.query_text ? retrievalScore * 0.45 + weighted * 0.25 + scopeScore(memory, req.path) * 0.15 + freshnessScore(memory) * 0.05 + typeScore(memory.type) * 0.1 : weighted * 0.55 + scopeScore(memory, req.path) * 0.2 + freshnessScore(memory) * 0.1 + typeScore(memory.type) * 0.15;
1345
+ return { memory, score };
1346
+ }).sort((a, b) => b.score - a.score);
1347
+ const dedupedRanked = dedupeRankedMemoriesForInjection(ranked);
1348
+ const selected = [];
1349
+ let commandCount = 0;
1350
+ let gotchaCount = 0;
1351
+ let lineCount = 0;
1352
+ for (const item of dedupedRanked) {
1353
+ const memory = item.memory;
1354
+ const memLines = renderMemoryText(memory).split("\n").length;
1355
+ if (lineCount + memLines > config.max_lines) continue;
1356
+ if (memory.type === "command" && commandCount >= config.max_commands) continue;
1357
+ if (memory.type === "gotcha" && gotchaCount >= config.max_gotchas) continue;
1358
+ selected.push(memory);
1359
+ lineCount += memLines;
1360
+ if (memory.type === "command") commandCount++;
1361
+ if (memory.type === "gotcha") gotchaCount++;
1362
+ }
1363
+ if (selected.length === 0) {
1364
+ const historyOnlyText = renderPack([], req.repo, selectedHistory);
1365
+ if (historyOnlyText) {
1366
+ recordHistoryInjections(db, {
1367
+ snippet_ids: selectedHistory.map((snippet) => snippet.id),
1368
+ session_id: req.session_id,
1369
+ repo: req.repo
1370
+ });
1371
+ return {
1372
+ text: historyOnlyText,
1373
+ memories_included: [],
1374
+ memories_dropped: [...dropped, ...passing].map((m) => m.id),
1375
+ history_included: selectedHistory.map((snippet) => snippet.id),
1376
+ token_estimate: Math.ceil(historyOnlyText.length / 4)
1377
+ };
1378
+ }
1379
+ return {
1380
+ text: "",
1381
+ memories_included: [],
1382
+ memories_dropped: [...dropped, ...passing].map((m) => m.id),
1383
+ history_included: [],
1384
+ token_estimate: 0
1385
+ };
1386
+ }
1387
+ while (selected.length > 1 && Math.ceil(renderPack(selected, req.repo, selectedHistory).length / 4) > config.token_budget) {
1388
+ selected.pop();
1389
+ }
1390
+ const finalText = renderPack(selected, req.repo, selectedHistory);
1391
+ recordMemoryInjections(db, {
1392
+ memory_ids: selected.map((memory) => memory.id),
1393
+ session_id: req.session_id,
1394
+ repo: req.repo
1395
+ });
1396
+ recordHistoryInjections(db, {
1397
+ snippet_ids: selectedHistory.map((snippet) => snippet.id),
1398
+ session_id: req.session_id,
1399
+ repo: req.repo
1400
+ });
1401
+ return {
1402
+ text: finalText,
1403
+ memories_included: selected.map((m) => m.id),
1404
+ memories_dropped: [
1405
+ ...dropped.map((m) => m.id),
1406
+ ...ranked.map((item) => item.memory).filter((memory) => !selected.includes(memory)).map((memory) => memory.id)
1407
+ ],
1408
+ history_included: selectedHistory.map((snippet) => snippet.id),
1409
+ token_estimate: Math.ceil(finalText.length / 4)
1410
+ };
1411
+ }
1412
+ function renderPack(items, repo, history = []) {
1413
+ if (items.length === 0 && history.length === 0) return "";
1414
+ const rules = items.filter((m) => m.type === "rule" || m.type === "decision");
1415
+ const commands = items.filter((m) => m.type === "command");
1416
+ const gotchas = items.filter(
1417
+ (m) => m.type === "gotcha" || m.type === "review_pattern"
1418
+ );
1419
+ const sections = [];
1420
+ if (rules.length > 0) {
1421
+ sections.push(
1422
+ "## Rules\n" + rules.map(renderMemoryBullet).join("\n")
1423
+ );
1424
+ }
1425
+ if (commands.length > 0) {
1426
+ sections.push(
1427
+ "## Commands\n" + commands.map(renderMemoryBullet).join("\n")
1428
+ );
1429
+ }
1430
+ if (gotchas.length > 0) {
1431
+ sections.push(
1432
+ "## Gotchas\n" + gotchas.map(renderMemoryBullet).join("\n")
1433
+ );
1434
+ }
1435
+ if (history.length > 0) {
1436
+ sections.push(
1437
+ "## History\n" + history.map(renderHistorySnippet).join("\n")
1438
+ );
1439
+ }
1440
+ return `# Recall: ${repo}
1441
+
1442
+ ${sections.join("\n\n")}
1443
+ `;
1444
+ }
1445
+ function renderMemoryBullet(memory) {
1446
+ const prefix = memory.scope === "global" ? "[global] " : "";
1447
+ return `- ${prefix}${renderMemoryText(memory)}`;
1448
+ }
1449
+ function renderMemoryText(memory) {
1450
+ const text = memory.text.replace(/\r\n/g, "\n").trim();
1451
+ const injectedHeading = text.search(/##\s+(Rules|Commands|Gotchas|History)\b/i);
1452
+ const stripped = injectedHeading > 0 ? text.slice(0, injectedHeading).trim() : text;
1453
+ return stripped.replace(/\s+/g, " ");
1454
+ }
1455
+ function dedupeMemoriesForInjection(memories4) {
1456
+ const seen = /* @__PURE__ */ new Set();
1457
+ return memories4.filter((memory) => {
1458
+ const key = canonicalInjectionText(memory);
1459
+ if (!key) return false;
1460
+ if (seen.has(key)) return false;
1461
+ seen.add(key);
1462
+ return true;
1463
+ });
1464
+ }
1465
+ function dedupeRankedMemoriesForInjection(ranked) {
1466
+ const seen = /* @__PURE__ */ new Set();
1467
+ return ranked.filter((item) => {
1468
+ const key = canonicalInjectionText(item.memory);
1469
+ if (!key) return false;
1470
+ if (seen.has(key)) return false;
1471
+ seen.add(key);
1472
+ return true;
1473
+ });
1474
+ }
1475
+ function canonicalInjectionText(memory) {
1476
+ return renderMemoryText(memory).toLowerCase().replace(/[`*_]/g, "").replace(/\s+/g, " ").trim();
1477
+ }
1478
+ var HISTORY_KINDS = /* @__PURE__ */ new Set([
1479
+ "decision_summary",
1480
+ "correction_summary",
1481
+ "review_summary",
1482
+ "repo_synthesis"
1483
+ ]);
1484
+ function selectRepoHistory(db, repo, limit) {
1485
+ if (limit <= 0) return [];
1486
+ return listHistorySnippets(db, { repo, limit: Math.max(limit * 3, 6) }).filter((snippet) => !snippet.session_id && HISTORY_KINDS.has(snippet.kind)).slice(0, limit);
1487
+ }
1488
+ async function selectRelevantHistory(db, repo, query, limit) {
1489
+ if (limit <= 0) return [];
1490
+ const results = await searchHistorySnippets(db, query, {
1491
+ repo,
1492
+ limit: Math.max(limit * 3, 6)
1493
+ });
1494
+ return results.map((result) => result.snippet).filter((snippet) => HISTORY_KINDS.has(snippet.kind)).slice(0, limit);
1495
+ }
1496
+ var HISTORY_ENTRY_MAX_CHARS = 120;
1497
+ var HISTORY_MAX_ENTRIES_PER_SNIPPET = 2;
1498
+ function renderHistorySnippet(snippet) {
1499
+ const lines = snippet.text.split("\n").map((line) => line.trim()).filter(Boolean).filter((line) => !line.startsWith("Repo: ") && !line.endsWith(":"));
1500
+ const entries = lines.slice(0, HISTORY_MAX_ENTRIES_PER_SNIPPET).map(
1501
+ (line) => line.length > HISTORY_ENTRY_MAX_CHARS ? line.slice(0, HISTORY_ENTRY_MAX_CHARS - 1).trimEnd() + "\u2026" : line
1502
+ );
1503
+ const body = entries.length > 0 ? entries.join(" | ") : "";
1504
+ return `- [${snippet.kind}] ${body}`;
1505
+ }
1506
+ function pathMatches(mem, targetPath) {
1507
+ if (mem.scope === "repo" || mem.scope === "team" || mem.scope === "global") return true;
1508
+ if (!mem.path_scope) return true;
1509
+ const pattern = mem.path_scope;
1510
+ if (pattern.endsWith("**")) {
1511
+ const prefix = pattern.slice(0, -2);
1512
+ return targetPath.startsWith(prefix);
1513
+ }
1514
+ if (pattern.includes("*")) {
1515
+ const regex = new RegExp(
1516
+ "^" + pattern.replace(/\*/g, "[^/]*").replace(/\*\*/g, ".*") + "$"
1517
+ );
1518
+ return regex.test(targetPath);
1519
+ }
1520
+ return targetPath.startsWith(pattern);
1521
+ }
1522
+ function scopeScore(mem, targetPath) {
1523
+ if (!targetPath) {
1524
+ if (mem.scope === "repo" || mem.scope === "team") return 0.9;
1525
+ if (mem.scope === "global") return 0.8;
1526
+ return 0.7;
1527
+ }
1528
+ if (mem.scope === "global") return 0.8;
1529
+ if (mem.scope === "path" && mem.path_scope) return 1;
1530
+ if (mem.scope === "repo" || mem.scope === "team") return 0.75;
1531
+ return pathMatches(mem, targetPath) ? 0.6 : 0;
1532
+ }
1533
+ function freshnessScore(mem) {
1534
+ const basis = mem.last_validated_at ?? mem.last_injected_at ?? mem.updated_at;
1535
+ const ageMs = Date.now() - new Date(basis).getTime();
1536
+ const ageDays = ageMs / 864e5;
1537
+ return Math.max(0, 1 - ageDays / 180);
1538
+ }
1539
+ function typePriority(type) {
1540
+ switch (type) {
1541
+ case "rule":
1542
+ return 0;
1543
+ case "command":
1544
+ return 1;
1545
+ case "gotcha":
1546
+ return 2;
1547
+ case "review_pattern":
1548
+ return 3;
1549
+ case "decision":
1550
+ return 4;
1551
+ default:
1552
+ return 5;
1553
+ }
1554
+ }
1555
+ function typeScore(type) {
1556
+ switch (type) {
1557
+ case "rule":
1558
+ return 1;
1559
+ case "command":
1560
+ return 0.95;
1561
+ case "decision":
1562
+ return 0.9;
1563
+ case "gotcha":
1564
+ return 0.8;
1565
+ case "review_pattern":
1566
+ return 0.75;
1567
+ default:
1568
+ return 0.5;
1569
+ }
1570
+ }
1571
+ function dedupeById(memories4) {
1572
+ const seen = /* @__PURE__ */ new Set();
1573
+ const out = [];
1574
+ for (const m of memories4) {
1575
+ if (seen.has(m.id)) continue;
1576
+ seen.add(m.id);
1577
+ out.push(m);
1578
+ }
1579
+ return out;
1580
+ }
1581
+
1582
+ // src/adapters/markdown.ts
1583
+ function exportMarkdown(db, repo) {
1584
+ const result = compileContext(db, { repo });
1585
+ if (!result.text) {
1586
+ return `# ${repo}
1587
+
1588
+ No active memories above confidence threshold.
1589
+ `;
1590
+ }
1591
+ return result.text;
1592
+ }
1593
+ function exportClaude(db, repo) {
1594
+ const result = compileContext(db, { repo });
1595
+ if (!result.text) return "";
1596
+ return `# CLAUDE.md \u2014 Auto-generated by Recall
1597
+ # Do not edit manually. Run \`recall export -r ${repo} -f claude\` to regenerate.
1598
+
1599
+ ${result.text}`;
1600
+ }
1601
+ function exportCodex(db, repo) {
1602
+ const result = compileContext(db, { repo });
1603
+ if (!result.text) return "";
1604
+ return `# AGENTS.md \u2014 Auto-generated by Recall
1605
+ # Do not edit manually. Run \`recall export -r ${repo} -f codex\` to regenerate.
1606
+
1607
+ ${result.text}`;
1608
+ }
1609
+
1610
+ // src/repo/discovery.ts
1611
+ import { existsSync as existsSync3, readdirSync } from "fs";
1612
+ import { execFileSync } from "child_process";
1613
+ import { join as join3, resolve } from "path";
1614
+ var repoPathCache = /* @__PURE__ */ new Map();
1615
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
1616
+ ".git",
1617
+ "node_modules",
1618
+ "dist",
1619
+ "build",
1620
+ "coverage",
1621
+ ".next",
1622
+ ".turbo",
1623
+ ".venv",
1624
+ "venv"
1625
+ ]);
1626
+ function ensureRepoBootstrapped(db, opts) {
1627
+ const repo = normalizeRepoSlug(opts.repo);
1628
+ const repoPathHint = opts.repoPathHint ?? null;
1629
+ if (!repo && !repoPathHint) {
1630
+ return {
1631
+ repo: null,
1632
+ repo_path: null,
1633
+ created_ids: [],
1634
+ status: "skipped"
1635
+ };
1636
+ }
1637
+ const resolvedRepo = repo ?? inferRepoSlugFromPath(repoPathHint);
1638
+ if (!resolvedRepo) {
1639
+ return {
1640
+ repo: null,
1641
+ repo_path: null,
1642
+ created_ids: [],
1643
+ status: "unresolved"
1644
+ };
1645
+ }
1646
+ if (queryMemories(db, { repo: resolvedRepo }).length > 0) {
1647
+ return {
1648
+ repo: resolvedRepo,
1649
+ repo_path: null,
1650
+ created_ids: [],
1651
+ status: "already_known"
1652
+ };
1653
+ }
1654
+ const repoPath = resolveLocalRepoPath(resolvedRepo, {
1655
+ repoPathHint,
1656
+ searchRoots: opts.searchRoots
1657
+ });
1658
+ if (!repoPath) {
1659
+ return {
1660
+ repo: resolvedRepo,
1661
+ repo_path: null,
1662
+ created_ids: [],
1663
+ status: "unresolved"
1664
+ };
1665
+ }
1666
+ const createdIds = scanAndStore(db, repoPath);
1667
+ return {
1668
+ repo: resolvedRepo,
1669
+ repo_path: repoPath,
1670
+ created_ids: createdIds,
1671
+ status: createdIds.length > 0 ? "bootstrapped" : "scanned_empty"
1672
+ };
1673
+ }
1674
+ function resolveLocalRepoPath(repo, opts = {}) {
1675
+ const normalizedRepo = normalizeRepoSlug(repo);
1676
+ if (!normalizedRepo) return null;
1677
+ if (repoPathCache.has(normalizedRepo)) {
1678
+ return repoPathCache.get(normalizedRepo) ?? null;
1679
+ }
1680
+ const directHint = normalizeRepoPathHint(opts.repoPathHint);
1681
+ if (directHint && pathMatchesRepo(directHint, normalizedRepo)) {
1682
+ repoPathCache.set(normalizedRepo, directHint);
1683
+ return directHint;
1684
+ }
1685
+ const candidates = collectCandidateRepos(opts.searchRoots ?? getDefaultSearchRoots());
1686
+ const basenameMatches = [];
1687
+ for (const candidate of candidates) {
1688
+ const candidateRepo = inferRepoSlugFromPath(candidate);
1689
+ if (candidateRepo === normalizedRepo) {
1690
+ repoPathCache.set(normalizedRepo, candidate);
1691
+ return candidate;
1692
+ }
1693
+ if (candidate.endsWith(`/${normalizedRepo.split("/").at(-1)}`)) {
1694
+ basenameMatches.push(candidate);
1695
+ }
1696
+ }
1697
+ const fallback = basenameMatches.length === 1 ? basenameMatches[0] : null;
1698
+ repoPathCache.set(normalizedRepo, fallback);
1699
+ return fallback;
1700
+ }
1701
+ function inferRepoSlugFromPath(repoPath) {
1702
+ const root = normalizeRepoPathHint(repoPath);
1703
+ if (!root) return null;
1704
+ try {
1705
+ const remote = execFileSync(
1706
+ "git",
1707
+ ["-C", root, "remote", "get-url", "origin"],
1708
+ { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }
1709
+ ).trim();
1710
+ return extractRepoSlugFromRemote2(remote);
1711
+ } catch {
1712
+ const parts = root.split("/").filter(Boolean);
1713
+ return parts.at(-1) ?? null;
1714
+ }
1715
+ }
1716
+ function extractRepoSlugFromRemote2(remote) {
1717
+ const trimmed = remote.trim().replace(/\.git$/, "");
1718
+ const parts = trimmed.split(/[:/]/).filter(Boolean);
1719
+ if (parts.length < 2) return null;
1720
+ return `${parts.at(-2)}/${parts.at(-1)}`;
1721
+ }
1722
+ function pathMatchesRepo(repoPath, repo) {
1723
+ const inferred = inferRepoSlugFromPath(repoPath);
1724
+ if (inferred === repo) return true;
1725
+ return repoPath.endsWith(`/${repo.split("/").at(-1)}`);
1726
+ }
1727
+ function normalizeRepoSlug(repo) {
1728
+ if (!repo) return null;
1729
+ const trimmed = repo.trim().replace(/\.git$/, "").replace(/^https?:\/\/[^/]+\//, "");
1730
+ if (!trimmed.includes("/")) return null;
1731
+ return trimmed.replace(/^git@[^:]+:/, "");
1732
+ }
1733
+ function normalizeRepoPathHint(repoPath) {
1734
+ if (!repoPath) return null;
1735
+ const expanded = repoPath.trim().replace(/^~(?=\/)/, process.env.HOME ?? "~");
1736
+ try {
1737
+ const root = execFileSync(
1738
+ "git",
1739
+ ["-C", expanded, "rev-parse", "--show-toplevel"],
1740
+ { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }
1741
+ ).trim();
1742
+ return root || null;
1743
+ } catch {
1744
+ const resolved = resolve(expanded);
1745
+ return existsSync3(join3(resolved, ".git")) ? resolved : null;
1746
+ }
1747
+ }
1748
+ function getDefaultSearchRoots() {
1749
+ const configured = process.env.RECALL_REPO_ROOTS?.split(",").map((value) => value.trim()).filter(Boolean);
1750
+ if (configured?.length) return configured;
1751
+ const home = process.env.HOME ?? process.cwd();
1752
+ return [join3(home, "Projects")];
1753
+ }
1754
+ function collectCandidateRepos(searchRoots) {
1755
+ const seen = /* @__PURE__ */ new Set();
1756
+ const repos = [];
1757
+ for (const root of searchRoots) {
1758
+ walkRepos(resolve(root), 4, seen, repos);
1759
+ }
1760
+ return repos;
1761
+ }
1762
+ function walkRepos(dir, depthRemaining, seen, repos) {
1763
+ if (depthRemaining < 0 || seen.has(dir) || !existsSync3(dir)) return;
1764
+ seen.add(dir);
1765
+ if (existsSync3(join3(dir, ".git"))) {
1766
+ repos.push(dir);
1767
+ return;
1768
+ }
1769
+ let entries;
1770
+ try {
1771
+ entries = readdirSync(dir, { withFileTypes: true });
1772
+ } catch {
1773
+ return;
1774
+ }
1775
+ for (const entry of entries) {
1776
+ if (!entry.isDirectory()) continue;
1777
+ if (SKIP_DIRS.has(entry.name)) continue;
1778
+ walkRepos(join3(dir, entry.name), depthRemaining - 1, seen, repos);
1779
+ }
1780
+ }
1781
+
1782
+ // src/artifacts/context.ts
1783
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
1784
+ import { join as join4 } from "path";
1785
+ import { execFileSync as execFileSync2 } from "child_process";
1786
+ function getRepoContextArtifactPath(repoPath) {
1787
+ return join4(repoPath, ".recall", "context.md");
1788
+ }
1789
+ function renderRepoContextArtifact(db, repo) {
1790
+ const compiled = exportMarkdown(db, repo).trimEnd();
1791
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
1792
+ return [
1793
+ "<!-- Auto-generated by Recall. Do not edit manually. -->",
1794
+ `<!-- Generated at: ${generatedAt} -->`,
1795
+ "",
1796
+ "# Recall Context",
1797
+ "",
1798
+ `Repo: \`${repo}\``,
1799
+ "",
1800
+ "Read this file before making repo-specific assumptions.",
1801
+ "",
1802
+ compiled,
1803
+ ""
1804
+ ].join("\n");
1805
+ }
1806
+ function writeRepoContextArtifact(db, input) {
1807
+ const repo = input.repo ?? inferRepoSlugFromPath(input.repo_path) ?? null;
1808
+ const repoPath = input.repo_path ?? (repo ? resolveLocalRepoPath(repo) : null);
1809
+ if (!repo || !repoPath) {
1810
+ return {
1811
+ repo,
1812
+ repo_path: repoPath,
1813
+ output_path: null,
1814
+ written: false
1815
+ };
1816
+ }
1817
+ const outputPath = getRepoContextArtifactPath(repoPath);
1818
+ mkdirSync2(join4(repoPath, ".recall"), { recursive: true });
1819
+ ensureRepoContextExcluded(repoPath);
1820
+ const content = renderRepoContextArtifact(db, repo);
1821
+ const existing = existsSync4(outputPath) ? readFileSync2(outputPath, "utf-8") : null;
1822
+ if (existing !== content) {
1823
+ writeFileSync(outputPath, content);
1824
+ }
1825
+ return {
1826
+ repo,
1827
+ repo_path: repoPath,
1828
+ output_path: outputPath,
1829
+ written: existing !== content
1830
+ };
1831
+ }
1832
+ function ensureRepoContextExcluded(repoPath) {
1833
+ try {
1834
+ const excludePath = execFileSync2(
1835
+ "git",
1836
+ ["-C", repoPath, "rev-parse", "--git-path", "info/exclude"],
1837
+ { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }
1838
+ ).trim();
1839
+ if (!excludePath) return;
1840
+ const existing = existsSync4(excludePath) ? readFileSync2(excludePath, "utf-8") : "";
1841
+ if (existing.split("\n").some((line) => line.trim() === ".recall/")) return;
1842
+ const prefix = existing && !existing.endsWith("\n") ? "\n" : "";
1843
+ writeFileSync(
1844
+ excludePath,
1845
+ `${existing}${prefix}# Recall generated context
1846
+ .recall/
1847
+ `
1848
+ );
1849
+ } catch {
1850
+ }
1851
+ }
1852
+
1853
+ // src/eval/harness.ts
1854
+ import { eq as eq8, sql, and as and3, gte, like } from "drizzle-orm";
1855
+ import { randomUUID as randomUUID4 } from "crypto";
1856
+ function startEvalSession(db, repo) {
1857
+ const id = randomUUID4();
1858
+ db.insert(evalSessions).values({
1859
+ id,
1860
+ repo,
1861
+ started_at: (/* @__PURE__ */ new Date()).toISOString()
1862
+ }).run();
1863
+ return id;
1864
+ }
1865
+ function endEvalSession(db, sessionId) {
1866
+ db.update(evalSessions).set({ ended_at: (/* @__PURE__ */ new Date()).toISOString() }).where(eq8(evalSessions.id, sessionId)).run();
1867
+ }
1868
+ function incrementEvalCounter(db, sessionId, field, amount = 1) {
1869
+ const col = evalSessions[field];
1870
+ db.update(evalSessions).set({ [field]: sql`${col} + ${amount}` }).where(eq8(evalSessions.id, sessionId)).run();
1871
+ }
1872
+ function computeMetrics(db, options = {}) {
1873
+ const conditions = [];
1874
+ if (options.repo) conditions.push(eq8(evalSessions.repo, options.repo));
1875
+ if (options.since) conditions.push(gte(evalSessions.started_at, options.since));
1876
+ const sessions = conditions.length > 0 ? db.select().from(evalSessions).where(and3(...conditions)).all() : db.select().from(evalSessions).all();
1877
+ if (sessions.length === 0) {
1878
+ const maintenance2 = computeMaintenanceMetrics(db);
1879
+ return {
1880
+ total_sessions: 0,
1881
+ injection_rate: 0,
1882
+ follow_rate: 0,
1883
+ override_rate: 0,
1884
+ correction_frequency: 0,
1885
+ avg_confidence_at_injection: 0,
1886
+ memory_effectiveness: 0,
1887
+ ...maintenance2 ? { maintenance: maintenance2 } : {}
1888
+ };
1889
+ }
1890
+ const totals = sessions.reduce(
1891
+ (acc, s) => ({
1892
+ injected: acc.injected + s.memories_injected,
1893
+ followed: acc.followed + s.memories_followed,
1894
+ overridden: acc.overridden + s.memories_overridden,
1895
+ corrections: acc.corrections + s.user_corrections,
1896
+ test_passes: acc.test_passes + s.test_passes,
1897
+ test_failures: acc.test_failures + s.test_failures
1898
+ }),
1899
+ {
1900
+ injected: 0,
1901
+ followed: 0,
1902
+ overridden: 0,
1903
+ corrections: 0,
1904
+ test_passes: 0,
1905
+ test_failures: 0
1906
+ }
1907
+ );
1908
+ const totalTests = totals.test_passes + totals.test_failures;
1909
+ const feedbackRows = db.select().from(feedbackEvents).all();
1910
+ const injectedFeedback = feedbackRows.filter((f) => f.injected);
1911
+ let avgConfidence = 0;
1912
+ if (injectedFeedback.length > 0) {
1913
+ const memIds = [...new Set(injectedFeedback.map((f) => f.memory_id))];
1914
+ let totalConf = 0;
1915
+ let count = 0;
1916
+ for (const memId of memIds) {
1917
+ const mem = db.select({ confidence: memories.confidence }).from(memories).where(eq8(memories.id, memId)).get();
1918
+ if (mem) {
1919
+ totalConf += mem.confidence;
1920
+ count++;
1921
+ }
1922
+ }
1923
+ avgConfidence = count > 0 ? totalConf / count : 0;
1924
+ }
1925
+ const effectiveness = totals.injected > 0 ? (totals.followed - totals.overridden) / totals.injected : 0;
1926
+ const maintenance = computeMaintenanceMetrics(db);
1927
+ return {
1928
+ total_sessions: sessions.length,
1929
+ injection_rate: totals.injected / Math.max(sessions.length, 1),
1930
+ follow_rate: totals.injected > 0 ? totals.followed / totals.injected : 0,
1931
+ override_rate: totals.injected > 0 ? totals.overridden / totals.injected : 0,
1932
+ correction_frequency: totals.corrections / Math.max(sessions.length, 1),
1933
+ avg_confidence_at_injection: avgConfidence,
1934
+ memory_effectiveness: effectiveness,
1935
+ ...maintenance ? { maintenance } : {}
1936
+ };
1937
+ }
1938
+ function computeMaintenanceMetrics(db) {
1939
+ const rows = db.select().from(memoryMaintenanceTasks).all();
1940
+ if (rows.length === 0) return void 0;
1941
+ let completed = 0;
1942
+ let abandoned = 0;
1943
+ const completed_by_kind = {};
1944
+ let completionDurations = [];
1945
+ let mergeCompleted = 0;
1946
+ for (const row of rows) {
1947
+ if (row.status === "completed") {
1948
+ completed += 1;
1949
+ completed_by_kind[row.kind] = (completed_by_kind[row.kind] ?? 0) + 1;
1950
+ if (row.kind === "merge_duplicates") mergeCompleted += 1;
1951
+ if (row.completed_at) {
1952
+ const delta = new Date(row.completed_at).getTime() - new Date(row.created_at).getTime();
1953
+ if (Number.isFinite(delta) && delta >= 0) completionDurations.push(delta);
1954
+ }
1955
+ } else if (row.status === "abandoned") {
1956
+ abandoned += 1;
1957
+ }
1958
+ }
1959
+ const mergeTouched = db.select().from(auditTrail).where(and3(
1960
+ like(auditTrail.reason, "merged_%"),
1961
+ like(auditTrail.actor, "maintenance:%")
1962
+ )).all();
1963
+ const touchedMemoryIds = new Set(mergeTouched.map((r) => r.memory_id));
1964
+ let mergeRollbacks = 0;
1965
+ if (touchedMemoryIds.size > 0) {
1966
+ const rollbacks = db.select().from(auditTrail).where(eq8(auditTrail.action, "rolled_back")).all();
1967
+ for (const r of rollbacks) {
1968
+ if (touchedMemoryIds.has(r.memory_id)) mergeRollbacks += 1;
1969
+ }
1970
+ }
1971
+ const merge_precision = mergeCompleted >= 5 && touchedMemoryIds.size > 0 ? Math.max(0, 1 - mergeRollbacks / touchedMemoryIds.size) : null;
1972
+ const mean_completion_ms = completionDurations.length ? completionDurations.reduce((a, b) => a + b, 0) / completionDurations.length : null;
1973
+ return {
1974
+ total_completed: completed,
1975
+ total_abandoned: abandoned,
1976
+ abandon_rate: completed + abandoned > 0 ? abandoned / (completed + abandoned) : 0,
1977
+ mean_completion_ms,
1978
+ completed_by_kind,
1979
+ merge_precision,
1980
+ merge_rollbacks: mergeRollbacks
1981
+ };
1982
+ }
1983
+ function formatMetricsReport(metrics) {
1984
+ const pct = (n) => `${(n * 100).toFixed(1)}%`;
1985
+ const lines = [
1986
+ `# Recall Evaluation Report`,
1987
+ ``,
1988
+ `Sessions: ${metrics.total_sessions}`,
1989
+ `Avg memories injected/session: ${metrics.injection_rate.toFixed(1)}`,
1990
+ ``,
1991
+ `## Trust`,
1992
+ `Follow rate: ${pct(metrics.follow_rate)}`,
1993
+ `Override rate: ${pct(metrics.override_rate)}`,
1994
+ `Effectiveness: ${pct(metrics.memory_effectiveness)}`,
1995
+ ``,
1996
+ `## Learning`,
1997
+ `Corrections/session: ${metrics.correction_frequency.toFixed(1)}`,
1998
+ `Avg confidence at injection: ${metrics.avg_confidence_at_injection.toFixed(2)}`
1999
+ ];
2000
+ if (metrics.maintenance) {
2001
+ const m = metrics.maintenance;
2002
+ lines.push(``, `## Maintenance (tier-2)`);
2003
+ lines.push(`Completed tasks: ${m.total_completed}`);
2004
+ lines.push(`Abandoned tasks: ${m.total_abandoned}`);
2005
+ lines.push(`Abandon rate: ${pct(m.abandon_rate)}`);
2006
+ if (m.mean_completion_ms != null) {
2007
+ lines.push(`Mean completion: ${(m.mean_completion_ms / 1e3).toFixed(1)}s`);
2008
+ }
2009
+ if (m.merge_precision != null) {
2010
+ lines.push(`Merge precision: ${pct(m.merge_precision)} (rollbacks: ${m.merge_rollbacks})`);
2011
+ }
2012
+ const kinds = Object.entries(m.completed_by_kind).sort((a, b) => b[1] - a[1]).map(([k, n]) => `${k}=${n}`).join(", ");
2013
+ if (kinds) lines.push(`By kind: ${kinds}`);
2014
+ }
2015
+ return lines.join("\n");
2016
+ }
2017
+
2018
+ // src/eval/retrieval.ts
2019
+ import { readFileSync as readFileSync3 } from "fs";
2020
+ function loadRetrievalEvalFile(path) {
2021
+ return RetrievalEvalFile.parse(JSON.parse(readFileSync3(path, "utf8")));
2022
+ }
2023
+ async function runRetrievalEval(db, input, options = {}) {
2024
+ const providers = options.providers?.length ? options.providers : ["current"];
2025
+ const providerReports = [];
2026
+ for (const provider of providers) {
2027
+ const cases = [];
2028
+ const embeddingConfig = provider === "current" ? loadEmbeddingConfigFromEnv() : embeddingConfigForProvider(provider);
2029
+ if (embeddingConfig) {
2030
+ await bootstrapEmbeddings(db, embeddingConfig);
2031
+ }
2032
+ for (const raw of input.cases) {
2033
+ const testCase = RetrievalEvalCase.parse(raw);
2034
+ const config = caseConfig(testCase);
2035
+ const baselineCompiled = compileContext(db, {
2036
+ repo: testCase.repo,
2037
+ path: testCase.path,
2038
+ config
2039
+ });
2040
+ const hybridCompiled = await compileContextHybrid(db, {
2041
+ repo: testCase.repo,
2042
+ path: testCase.path,
2043
+ query_text: testCase.query_text,
2044
+ config: {
2045
+ ...config,
2046
+ include_candidates: testCase.include_candidates
2047
+ },
2048
+ embedding_config: embeddingConfig
2049
+ });
2050
+ const baseline = evaluateCaseRun(db, testCase, baselineCompiled.memories_included, baselineCompiled.token_estimate);
2051
+ const hybrid = evaluateCaseRun(db, testCase, hybridCompiled.memories_included, hybridCompiled.token_estimate);
2052
+ cases.push({
2053
+ name: testCase.name,
2054
+ baseline,
2055
+ hybrid,
2056
+ improved: !baseline.passed && hybrid.passed,
2057
+ regressed: baseline.passed && !hybrid.passed
2058
+ });
2059
+ }
2060
+ const total = cases.length;
2061
+ const baselinePassed = cases.filter((item) => item.baseline.passed).length;
2062
+ const hybridPassed = cases.filter((item) => item.hybrid.passed).length;
2063
+ const improved = cases.filter((item) => item.improved).length;
2064
+ const regressed = cases.filter((item) => item.regressed).length;
2065
+ const baselineExpectedAnyHits = cases.filter((item) => item.baseline.expected_any_hit).length;
2066
+ const hybridExpectedAnyHits = cases.filter((item) => item.hybrid.expected_any_hit).length;
2067
+ const baselineForbiddenHits = cases.filter((item) => item.baseline.forbidden_hits.length > 0).length;
2068
+ const hybridForbiddenHits = cases.filter((item) => item.hybrid.forbidden_hits.length > 0).length;
2069
+ const reciprocalRanks = cases.map((item) => item.hybrid.first_expected_rank).filter((rank) => rank != null).map((rank) => 1 / rank);
2070
+ providerReports.push({
2071
+ provider,
2072
+ summary: {
2073
+ total_cases: total,
2074
+ baseline_passed: baselinePassed,
2075
+ hybrid_passed: hybridPassed,
2076
+ improved_cases: improved,
2077
+ regressed_cases: regressed,
2078
+ baseline_expected_any_hit_rate: ratio(baselineExpectedAnyHits, total),
2079
+ hybrid_expected_any_hit_rate: ratio(hybridExpectedAnyHits, total),
2080
+ baseline_forbidden_hit_rate: ratio(baselineForbiddenHits, total),
2081
+ hybrid_forbidden_hit_rate: ratio(hybridForbiddenHits, total)
2082
+ },
2083
+ metrics: {
2084
+ recall_at_k: ratio(hybridExpectedAnyHits, total),
2085
+ mrr: reciprocalRanks.length > 0 ? reciprocalRanks.reduce((sum, value) => sum + value, 0) / total : 0,
2086
+ override_rate: ratio(hybridForbiddenHits, total)
2087
+ },
2088
+ cases
2089
+ });
2090
+ }
2091
+ return {
2092
+ summary: providerReports[0].summary,
2093
+ cases: providerReports[0].cases,
2094
+ provider_reports: providerReports
2095
+ };
2096
+ }
2097
+ function formatRetrievalEvalReport(report) {
2098
+ const pct = (value) => `${(value * 100).toFixed(1)}%`;
2099
+ if (report.provider_reports.length > 1) {
2100
+ const lines = [
2101
+ "# Retrieval Eval",
2102
+ "",
2103
+ "## Provider Comparison"
2104
+ ];
2105
+ for (const provider of report.provider_reports) {
2106
+ lines.push(
2107
+ `- ${provider.provider}: passed=${provider.summary.hybrid_passed}/${provider.summary.total_cases} recall@k=${pct(provider.metrics.recall_at_k)} mrr=${provider.metrics.mrr.toFixed(3)} override=${pct(provider.metrics.override_rate)}`
2108
+ );
2109
+ }
2110
+ for (const provider of report.provider_reports) {
2111
+ lines.push("", `## ${provider.provider}`);
2112
+ lines.push(formatSingleProviderReport(provider.summary, provider.cases));
2113
+ }
2114
+ return lines.join("\n");
2115
+ }
2116
+ return formatSingleProviderReport(report.summary, report.cases);
2117
+ }
2118
+ function formatSingleProviderReport(summary, cases) {
2119
+ const pct = (value) => `${(value * 100).toFixed(1)}%`;
2120
+ const lines = [
2121
+ "# Retrieval Eval",
2122
+ "",
2123
+ `Cases: ${summary.total_cases}`,
2124
+ `Baseline passed: ${summary.baseline_passed}`,
2125
+ `Hybrid passed: ${summary.hybrid_passed}`,
2126
+ `Improved: ${summary.improved_cases}`,
2127
+ `Regressed: ${summary.regressed_cases}`,
2128
+ "",
2129
+ `Baseline expected-any hit rate: ${pct(summary.baseline_expected_any_hit_rate)}`,
2130
+ `Hybrid expected-any hit rate: ${pct(summary.hybrid_expected_any_hit_rate)}`,
2131
+ `Baseline forbidden hit rate: ${pct(summary.baseline_forbidden_hit_rate)}`,
2132
+ `Hybrid forbidden hit rate: ${pct(summary.hybrid_forbidden_hit_rate)}`
2133
+ ];
2134
+ const failedCases = cases.filter((item) => !item.hybrid.passed || item.regressed || item.improved);
2135
+ if (failedCases.length > 0) {
2136
+ lines.push("", "## Case Details");
2137
+ for (const item of failedCases) {
2138
+ lines.push(`- ${item.name}`);
2139
+ lines.push(` baseline: ${describeRun(item.baseline)}`);
2140
+ lines.push(` hybrid: ${describeRun(item.hybrid)}`);
2141
+ }
2142
+ }
2143
+ return lines.join("\n");
2144
+ }
2145
+ function caseConfig(testCase) {
2146
+ return {
2147
+ ...testCase.confidence_threshold != null ? { confidence_threshold: testCase.confidence_threshold } : {},
2148
+ ...testCase.max_lines != null ? { max_lines: testCase.max_lines } : {},
2149
+ ...testCase.max_commands != null ? { max_commands: testCase.max_commands } : {},
2150
+ ...testCase.max_gotchas != null ? { max_gotchas: testCase.max_gotchas } : {},
2151
+ ...testCase.token_budget != null ? { token_budget: testCase.token_budget } : {}
2152
+ };
2153
+ }
2154
+ function evaluateCaseRun(db, testCase, memoryIds, tokenEstimate) {
2155
+ const includedTexts = memoryIds.map((id) => getMemory(db, id)?.text).filter((text) => Boolean(text));
2156
+ const expectedAllMissing = testCase.expected_all_texts.filter((expected) => !includedTexts.includes(expected));
2157
+ const expectedAnyHit = testCase.expected_any_texts.length === 0 ? true : testCase.expected_any_texts.some((expected) => includedTexts.includes(expected));
2158
+ const forbiddenHits = testCase.forbidden_texts.filter((forbidden) => includedTexts.includes(forbidden));
2159
+ const relevantTexts = [
2160
+ ...testCase.expected_all_texts,
2161
+ ...testCase.expected_any_texts
2162
+ ];
2163
+ const firstExpectedRank = relevantTexts.length === 0 ? null : includedTexts.findIndex((text) => relevantTexts.includes(text)) + 1 || null;
2164
+ let countViolation;
2165
+ if (testCase.min_included != null && memoryIds.length < testCase.min_included) {
2166
+ countViolation = `included ${memoryIds.length} < min ${testCase.min_included}`;
2167
+ } else if (testCase.max_included != null && memoryIds.length > testCase.max_included) {
2168
+ countViolation = `included ${memoryIds.length} > max ${testCase.max_included}`;
2169
+ }
2170
+ const passed = expectedAllMissing.length === 0 && expectedAnyHit && forbiddenHits.length === 0 && !countViolation;
2171
+ return {
2172
+ included_ids: memoryIds,
2173
+ included_texts: includedTexts,
2174
+ token_estimate: tokenEstimate,
2175
+ passed,
2176
+ expected_all_missing: expectedAllMissing,
2177
+ expected_any_hit: expectedAnyHit,
2178
+ forbidden_hits: forbiddenHits,
2179
+ first_expected_rank: firstExpectedRank,
2180
+ count_violation: countViolation
2181
+ };
2182
+ }
2183
+ function describeRun(result) {
2184
+ const parts = [
2185
+ result.passed ? "pass" : "fail",
2186
+ `included=${result.included_ids.length}`
2187
+ ];
2188
+ if (result.expected_all_missing.length > 0) {
2189
+ parts.push(`missing_all=${result.expected_all_missing.join(" | ")}`);
2190
+ }
2191
+ if (!result.expected_any_hit) {
2192
+ parts.push("expected_any=miss");
2193
+ }
2194
+ if (result.forbidden_hits.length > 0) {
2195
+ parts.push(`forbidden=${result.forbidden_hits.join(" | ")}`);
2196
+ }
2197
+ if (result.count_violation) {
2198
+ parts.push(result.count_violation);
2199
+ }
2200
+ return parts.join(" ; ");
2201
+ }
2202
+ function ratio(value, total) {
2203
+ return total > 0 ? value / total : 0;
2204
+ }
2205
+ function embeddingConfigForProvider(provider) {
2206
+ const overrideDimensions = process.env.RECALL_EMBEDDING_DIMS ? parseInt(process.env.RECALL_EMBEDDING_DIMS, 10) : null;
2207
+ if (provider === "bge-small-en-v1.5") {
2208
+ return {
2209
+ provider,
2210
+ model: "Xenova/bge-small-en-v1.5",
2211
+ dimensions: overrideDimensions ?? 384,
2212
+ version: "eval",
2213
+ similarity_threshold: 0.8
2214
+ };
2215
+ }
2216
+ if (provider === "multilingual-e5") {
2217
+ return {
2218
+ provider,
2219
+ model: "Xenova/multilingual-e5-small",
2220
+ dimensions: overrideDimensions ?? 384,
2221
+ version: "eval",
2222
+ similarity_threshold: 0.8
2223
+ };
2224
+ }
2225
+ return {
2226
+ provider: "nomic",
2227
+ model: "nomic-ai/nomic-embed-text-v1.5",
2228
+ dimensions: overrideDimensions ?? 512,
2229
+ version: "eval",
2230
+ similarity_threshold: 0.8
2231
+ };
2232
+ }
2233
+
2234
+ // src/feedback/implicit.ts
2235
+ import { eq as eq9 } from "drizzle-orm";
2236
+ import { randomUUID as randomUUID5 } from "crypto";
2237
+ import { execSync as execSync2 } from "child_process";
2238
+ var SIGNAL_WEIGHTS = {
2239
+ test_pass: 0.03,
2240
+ test_fail: -0.15,
2241
+ file_unchanged: 0.02,
2242
+ file_rewritten: -0.1,
2243
+ task_accepted: 0.05,
2244
+ task_rejected: -0.2
2245
+ };
2246
+ function recordSignal(db, memoryId, sessionId, signalType, context) {
2247
+ const id = randomUUID5();
2248
+ db.insert(implicitSignals).values({
2249
+ id,
2250
+ memory_id: memoryId,
2251
+ session_id: sessionId,
2252
+ signal_type: signalType,
2253
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2254
+ context: context ?? null
2255
+ }).run();
2256
+ const weight = SIGNAL_WEIGHTS[signalType];
2257
+ if (weight > 0) {
2258
+ promoteMemory(db, memoryId, "passive_gain");
2259
+ } else if (weight < 0) {
2260
+ demoteMemory(db, memoryId, `implicit:${signalType}`);
2261
+ }
2262
+ return id;
2263
+ }
2264
+ function getSignals(db, memoryId) {
2265
+ return db.select().from(implicitSignals).where(eq9(implicitSignals.memory_id, memoryId)).all();
2266
+ }
2267
+ function runTests(repoPath, command) {
2268
+ try {
2269
+ const output = execSync2(command, {
2270
+ cwd: repoPath,
2271
+ encoding: "utf-8",
2272
+ timeout: 12e4,
2273
+ stdio: ["pipe", "pipe", "pipe"]
2274
+ });
2275
+ return { passed: true, output };
2276
+ } catch (err) {
2277
+ return {
2278
+ passed: false,
2279
+ output: err.stdout ?? err.stderr ?? err.message
2280
+ };
2281
+ }
2282
+ }
2283
+ function recordTestSignals(db, sessionId, injectedMemoryIds, testResult) {
2284
+ const signalType = testResult.passed ? "test_pass" : "test_fail";
2285
+ const ids = [];
2286
+ for (const memId of injectedMemoryIds) {
2287
+ const id = recordSignal(
2288
+ db,
2289
+ memId,
2290
+ sessionId,
2291
+ signalType,
2292
+ testResult.output?.slice(0, 500)
2293
+ );
2294
+ ids.push(id);
2295
+ }
2296
+ return ids;
2297
+ }
2298
+ function getSignalStats(db, memoryId) {
2299
+ const signals = getSignals(db, memoryId);
2300
+ const stats = {
2301
+ test_pass: 0,
2302
+ test_fail: 0,
2303
+ file_unchanged: 0,
2304
+ file_rewritten: 0,
2305
+ task_accepted: 0,
2306
+ task_rejected: 0
2307
+ };
2308
+ for (const s of signals) {
2309
+ stats[s.signal_type] = (stats[s.signal_type] ?? 0) + 1;
2310
+ }
2311
+ return stats;
2312
+ }
2313
+
2314
+ // src/policy/engine.ts
2315
+ import { eq as eq10, and as and4 } from "drizzle-orm";
2316
+ import { randomUUID as randomUUID6 } from "crypto";
2317
+ function createPolicy(db, orgId, ruleType, config) {
2318
+ const id = randomUUID6();
2319
+ const now2 = (/* @__PURE__ */ new Date()).toISOString();
2320
+ db.insert(policyRules).values({
2321
+ id,
2322
+ org_id: orgId,
2323
+ rule_type: ruleType,
2324
+ config,
2325
+ enabled: true,
2326
+ created_at: now2,
2327
+ updated_at: now2
2328
+ }).run();
2329
+ return id;
2330
+ }
2331
+ function listPolicies(db, orgId) {
2332
+ return db.select().from(policyRules).where(eq10(policyRules.org_id, orgId)).all().map(rowToPolicy);
2333
+ }
2334
+ function togglePolicy(db, policyId, enabled) {
2335
+ db.update(policyRules).set({ enabled, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).where(eq10(policyRules.id, policyId)).run();
2336
+ }
2337
+ function deletePolicy(db, policyId) {
2338
+ db.delete(policyRules).where(eq10(policyRules.id, policyId)).run();
2339
+ }
2340
+ function evaluatePolicy(db, orgId, memory) {
2341
+ const rules = listPolicies(db, orgId).filter((r) => r.enabled);
2342
+ const violations = [];
2343
+ for (const rule of rules) {
2344
+ const cfg = rule.config;
2345
+ switch (rule.rule_type) {
2346
+ case "min_confidence": {
2347
+ const min = cfg.min_confidence ?? 0.6;
2348
+ if (memory.confidence < min) {
2349
+ violations.push({
2350
+ rule_id: rule.id,
2351
+ rule_type: rule.rule_type,
2352
+ message: `Confidence ${memory.confidence.toFixed(2)} below minimum ${min}`,
2353
+ blocking: true
2354
+ });
2355
+ }
2356
+ break;
2357
+ }
2358
+ case "require_approval": {
2359
+ const forTypes = cfg.for_types;
2360
+ if (!forTypes || forTypes.includes(memory.type)) {
2361
+ violations.push({
2362
+ rule_id: rule.id,
2363
+ rule_type: rule.rule_type,
2364
+ message: `Memory type "${memory.type}" requires approval before activation`,
2365
+ blocking: true
2366
+ });
2367
+ }
2368
+ break;
2369
+ }
2370
+ case "allowed_sources": {
2371
+ const allowed = cfg.sources;
2372
+ if (allowed && !allowed.includes(memory.source)) {
2373
+ violations.push({
2374
+ rule_id: rule.id,
2375
+ rule_type: rule.rule_type,
2376
+ message: `Source "${memory.source}" not in allowed list: ${allowed.join(", ")}`,
2377
+ blocking: true
2378
+ });
2379
+ }
2380
+ break;
2381
+ }
2382
+ case "blocked_scopes": {
2383
+ const blocked = cfg.scopes;
2384
+ if (blocked && blocked.includes(memory.scope)) {
2385
+ violations.push({
2386
+ rule_id: rule.id,
2387
+ rule_type: rule.rule_type,
2388
+ message: `Scope "${memory.scope}" is blocked by policy`,
2389
+ blocking: true
2390
+ });
2391
+ }
2392
+ break;
2393
+ }
2394
+ case "max_active_per_repo": {
2395
+ const max = cfg.max ?? 50;
2396
+ if (memory.repo) {
2397
+ const active = queryMemories(db, { repo: memory.repo, status: "active" });
2398
+ if (active.length >= max) {
2399
+ violations.push({
2400
+ rule_id: rule.id,
2401
+ rule_type: rule.rule_type,
2402
+ message: `Repo "${memory.repo}" has ${active.length}/${max} active memories`,
2403
+ blocking: true
2404
+ });
2405
+ }
2406
+ }
2407
+ break;
2408
+ }
2409
+ case "require_evidence_count": {
2410
+ const minEvidence = cfg.min_evidence ?? 2;
2411
+ if (memory.evidence.length < minEvidence) {
2412
+ violations.push({
2413
+ rule_id: rule.id,
2414
+ rule_type: rule.rule_type,
2415
+ message: `Memory has ${memory.evidence.length} evidence entries, needs ${minEvidence}`,
2416
+ blocking: true
2417
+ });
2418
+ }
2419
+ break;
2420
+ }
2421
+ case "auto_approve_pattern": {
2422
+ break;
2423
+ }
2424
+ }
2425
+ }
2426
+ return violations;
2427
+ }
2428
+ function requestApproval(db, memoryId, orgId, requestedBy) {
2429
+ const id = randomUUID6();
2430
+ const now2 = (/* @__PURE__ */ new Date()).toISOString();
2431
+ db.insert(approvalRequests).values({
2432
+ id,
2433
+ memory_id: memoryId,
2434
+ org_id: orgId,
2435
+ requested_by: requestedBy,
2436
+ status: "pending",
2437
+ created_at: now2
2438
+ }).run();
2439
+ recordAudit(db, memoryId, "approval_requested", requestedBy, null);
2440
+ return id;
2441
+ }
2442
+ function resolveApproval(db, approvalId, status, reviewedBy, reason) {
2443
+ const row = db.select().from(approvalRequests).where(eq10(approvalRequests.id, approvalId)).get();
2444
+ if (!row) return false;
2445
+ const now2 = (/* @__PURE__ */ new Date()).toISOString();
2446
+ db.update(approvalRequests).set({
2447
+ status,
2448
+ reviewed_by: reviewedBy,
2449
+ reason: reason ?? null,
2450
+ resolved_at: now2
2451
+ }).where(eq10(approvalRequests.id, approvalId)).run();
2452
+ if (status === "approved") {
2453
+ confirmMemory(db, row.memory_id);
2454
+ } else {
2455
+ rejectMemory(db, row.memory_id);
2456
+ }
2457
+ recordAudit(db, row.memory_id, "approval_resolved", reviewedBy, reason ?? null);
2458
+ return true;
2459
+ }
2460
+ function listPendingApprovals(db, orgId) {
2461
+ return db.select().from(approvalRequests).where(
2462
+ and4(
2463
+ eq10(approvalRequests.org_id, orgId),
2464
+ eq10(approvalRequests.status, "pending")
2465
+ )
2466
+ ).all();
2467
+ }
2468
+ function rowToPolicy(row) {
2469
+ return {
2470
+ ...row,
2471
+ config: typeof row.config === "string" ? JSON.parse(row.config) : row.config ?? {},
2472
+ enabled: Boolean(row.enabled)
2473
+ };
2474
+ }
2475
+
2476
+ // src/pruning/pruner.ts
2477
+ import { eq as eq11 } from "drizzle-orm";
2478
+ var DEFAULT_CONFIG2 = {
2479
+ stale_days: 90,
2480
+ rejected_retention_days: 30,
2481
+ transient_retention_days: 7,
2482
+ min_health_score: 0.2,
2483
+ dry_run: false
2484
+ };
2485
+ function pruneMemories(db, config = {}) {
2486
+ const cfg = { ...DEFAULT_CONFIG2, ...config };
2487
+ const now2 = Date.now();
2488
+ const dayMs = 864e5;
2489
+ const result = {
2490
+ stale_rejected: [],
2491
+ rejected_pruned: [],
2492
+ transient_pruned: [],
2493
+ unhealthy_demoted: [],
2494
+ total: 0
2495
+ };
2496
+ const staleCutoff = new Date(now2 - cfg.stale_days * dayMs).toISOString();
2497
+ const staleCandidates = queryMemories(db, {
2498
+ repo: cfg.repo,
2499
+ limit: void 0
2500
+ }).filter((mem) => mem.status !== "rejected" && mem.status !== "transient");
2501
+ for (const mem of staleCandidates) {
2502
+ const lastActivity = mem.last_validated_at ?? mem.last_injected_at ?? mem.updated_at;
2503
+ if (lastActivity < staleCutoff) {
2504
+ if (!cfg.dry_run) {
2505
+ db.update(memories).set({ status: "rejected", dedupe_key: null, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).where(eq11(memories.id, mem.id)).run();
2506
+ queueMemoryEmbeddingSync(db, mem.id);
2507
+ recordAudit(db, mem.id, "rejected", "auto-pruner", `Stale: no activity since ${lastActivity}`);
2508
+ }
2509
+ result.stale_rejected.push(mem.id);
2510
+ }
2511
+ }
2512
+ const rejectedCutoff = new Date(
2513
+ now2 - cfg.rejected_retention_days * dayMs
2514
+ ).toISOString();
2515
+ const rejectedMemories = queryMemories(db, {
2516
+ repo: cfg.repo,
2517
+ status: "rejected"
2518
+ });
2519
+ for (const mem of rejectedMemories) {
2520
+ if (mem.updated_at < rejectedCutoff) {
2521
+ if (!cfg.dry_run) {
2522
+ db.delete(memories).where(eq11(memories.id, mem.id)).run();
2523
+ recordAudit(db, mem.id, "pruned", "auto-pruner", `Rejected memory past ${cfg.rejected_retention_days}d retention`);
2524
+ }
2525
+ result.rejected_pruned.push(mem.id);
2526
+ }
2527
+ }
2528
+ const transientCutoff = new Date(
2529
+ now2 - cfg.transient_retention_days * dayMs
2530
+ ).toISOString();
2531
+ const transientMemories = queryMemories(db, {
2532
+ repo: cfg.repo,
2533
+ status: "transient"
2534
+ });
2535
+ for (const mem of transientMemories) {
2536
+ if (mem.updated_at < transientCutoff) {
2537
+ if (!cfg.dry_run) {
2538
+ db.delete(memories).where(eq11(memories.id, mem.id)).run();
2539
+ recordAudit(db, mem.id, "pruned", "auto-pruner", `Transient memory past ${cfg.transient_retention_days}d retention`);
2540
+ }
2541
+ result.transient_pruned.push(mem.id);
2542
+ }
2543
+ }
2544
+ const activeMemories = queryMemories(db, {
2545
+ repo: cfg.repo,
2546
+ status: "active"
2547
+ });
2548
+ for (const mem of activeMemories) {
2549
+ const health = computeHealthScore(db, mem.id);
2550
+ if (health && health.score < cfg.min_health_score) {
2551
+ if (!cfg.dry_run) {
2552
+ db.update(memories).set({ status: "candidate", updated_at: (/* @__PURE__ */ new Date()).toISOString() }).where(eq11(memories.id, mem.id)).run();
2553
+ queueMemoryEmbeddingSync(db, mem.id);
2554
+ recordAudit(
2555
+ db,
2556
+ mem.id,
2557
+ "demoted",
2558
+ "auto-pruner",
2559
+ `Health score ${health.score.toFixed(2)} below threshold ${cfg.min_health_score}`
2560
+ );
2561
+ }
2562
+ result.unhealthy_demoted.push(mem.id);
2563
+ }
2564
+ }
2565
+ result.total = result.stale_rejected.length + result.rejected_pruned.length + result.transient_pruned.length + result.unhealthy_demoted.length;
2566
+ return result;
2567
+ }
2568
+ function formatPruneReport(result, dryRun) {
2569
+ const prefix = dryRun ? "[DRY RUN] " : "";
2570
+ const lines = [
2571
+ `${prefix}Prune Report`,
2572
+ ``,
2573
+ `Stale rejected: ${result.stale_rejected.length}`,
2574
+ `Rejected pruned: ${result.rejected_pruned.length}`,
2575
+ `Transient pruned: ${result.transient_pruned.length}`,
2576
+ `Unhealthy demoted: ${result.unhealthy_demoted.length}`,
2577
+ `Total affected: ${result.total}`
2578
+ ];
2579
+ if (result.stale_rejected.length > 0) {
2580
+ lines.push("", "Stale Rejected:");
2581
+ for (const id of result.stale_rejected.slice(0, 10)) {
2582
+ lines.push(` ${id.slice(0, 8)}`);
2583
+ }
2584
+ }
2585
+ if (result.unhealthy_demoted.length > 0) {
2586
+ lines.push("", "Unhealthy:");
2587
+ for (const id of result.unhealthy_demoted.slice(0, 10)) {
2588
+ lines.push(` ${id.slice(0, 8)}`);
2589
+ }
2590
+ }
2591
+ return lines.join("\n");
2592
+ }
2593
+
2594
+ // src/models/activity.ts
2595
+ import { and as and5, desc as desc2, eq as eq12, gte as gte2 } from "drizzle-orm";
2596
+ import { randomUUID as randomUUID7 } from "crypto";
2597
+ function createActivityEvent(db, input) {
2598
+ const dedupeKey = activityEventDedupeKey(input);
2599
+ const duplicateId = findDuplicateActivityEvent(db, input, dedupeKey);
2600
+ if (duplicateId) return duplicateId;
2601
+ const id = randomUUID7();
2602
+ db.insert(activityEvents).values({
2603
+ id,
2604
+ session_id: input.session_id ?? null,
2605
+ repo: input.repo ?? null,
2606
+ path: input.path ?? null,
2607
+ source: input.source,
2608
+ event_type: input.event_type,
2609
+ memory_ids: input.memory_ids ?? [],
2610
+ dedupe_key: dedupeKey,
2611
+ request: input.request ?? {},
2612
+ result: input.result ?? {},
2613
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
2614
+ }).run();
2615
+ return id;
2616
+ }
2617
+ function findDuplicateActivityEvent(db, input, dedupeKey) {
2618
+ if (!input.session_id) return null;
2619
+ if (dedupeKey) {
2620
+ const existing = db.select().from(activityEvents).where(eq12(activityEvents.dedupe_key, dedupeKey)).get();
2621
+ if (existing) return existing.id;
2622
+ }
2623
+ const since = new Date(Date.now() - 2e3).toISOString();
2624
+ const rows = db.select().from(activityEvents).where(and5(
2625
+ eq12(activityEvents.session_id, input.session_id),
2626
+ eq12(activityEvents.source, input.source),
2627
+ eq12(activityEvents.event_type, input.event_type),
2628
+ gte2(activityEvents.created_at, since)
2629
+ )).all();
2630
+ const requestKey = JSON.stringify(input.request ?? {});
2631
+ const resultKey = JSON.stringify(input.result ?? {});
2632
+ const repo = input.repo ?? null;
2633
+ const path = input.path ?? null;
2634
+ for (const row of rows) {
2635
+ if (row.repo !== repo || row.path !== path) continue;
2636
+ const request = typeof row.request === "string" ? JSON.parse(row.request) : row.request ?? {};
2637
+ const result = typeof row.result === "string" ? JSON.parse(row.result) : row.result ?? {};
2638
+ if (JSON.stringify(request) === requestKey && JSON.stringify(result) === resultKey) {
2639
+ return row.id;
2640
+ }
2641
+ }
2642
+ return null;
2643
+ }
2644
+ function listActivityEvents(db, query = {}) {
2645
+ const conditions = [];
2646
+ if (query.repo) conditions.push(eq12(activityEvents.repo, query.repo));
2647
+ if (query.session_id) conditions.push(eq12(activityEvents.session_id, query.session_id));
2648
+ if (query.source) conditions.push(eq12(activityEvents.source, query.source));
2649
+ if (query.event_type) conditions.push(eq12(activityEvents.event_type, query.event_type));
2650
+ if (query.since) conditions.push(gte2(activityEvents.created_at, query.since));
2651
+ const base = db.select().from(activityEvents);
2652
+ const rows = conditions.length > 0 ? base.where(and5(...conditions)).orderBy(desc2(activityEvents.created_at)).all() : base.orderBy(desc2(activityEvents.created_at)).all();
2653
+ const limited = query.limit ? rows.slice(0, query.limit) : rows;
2654
+ return limited.map(rowToActivityEvent);
2655
+ }
2656
+ function listActivitySessions(db, query = {}) {
2657
+ const events = listActivityEvents(db, query).filter((event) => event.session_id);
2658
+ const grouped = /* @__PURE__ */ new Map();
2659
+ for (const event of events) {
2660
+ const sessionId = event.session_id;
2661
+ const bucket = grouped.get(sessionId) ?? [];
2662
+ bucket.push(event);
2663
+ grouped.set(sessionId, bucket);
2664
+ }
2665
+ const sessions = [...grouped.entries()].map(([session_id, items]) => {
2666
+ const sorted = [...items].sort((a, b) => a.created_at.localeCompare(b.created_at));
2667
+ return {
2668
+ session_id,
2669
+ repo: sorted[0]?.repo ?? null,
2670
+ event_count: items.length,
2671
+ event_types: [...new Set(items.map((item) => item.event_type))],
2672
+ first_at: sorted[0].created_at,
2673
+ last_at: sorted[sorted.length - 1].created_at
2674
+ };
2675
+ });
2676
+ sessions.sort((a, b) => b.last_at.localeCompare(a.last_at));
2677
+ return query.limit ? sessions.slice(0, query.limit) : sessions;
2678
+ }
2679
+ function rowToActivityEvent(row) {
2680
+ const memory_ids = typeof row.memory_ids === "string" ? JSON.parse(row.memory_ids) : Array.isArray(row.memory_ids) ? row.memory_ids : [];
2681
+ const request = typeof row.request === "string" ? JSON.parse(row.request) : row.request ?? {};
2682
+ const result = typeof row.result === "string" ? JSON.parse(row.result) : row.result ?? {};
2683
+ return {
2684
+ id: row.id,
2685
+ session_id: row.session_id,
2686
+ repo: row.repo,
2687
+ path: row.path,
2688
+ source: row.source,
2689
+ event_type: row.event_type,
2690
+ memory_ids,
2691
+ request,
2692
+ result,
2693
+ created_at: row.created_at
2694
+ };
2695
+ }
2696
+
2697
+ // src/session/lifecycle.ts
2698
+ function resolveLifecycleSource(input) {
2699
+ if (input.source) return input.source;
2700
+ return input.client ? tagActivitySource("hook", input.client) : "daemon";
2701
+ }
2702
+ function startSessionLifecycle(db, input) {
2703
+ const repo = input.repo ?? inferRepoSlugFromPath(input.repo_path) ?? null;
2704
+ const bootstrap = ensureRepoBootstrapped(db, {
2705
+ repo,
2706
+ repoPathHint: input.repo_path
2707
+ });
2708
+ if (bootstrap.status === "bootstrapped" || bootstrap.status === "scanned_empty") {
2709
+ createActivityEvent(db, {
2710
+ session_id: input.session_id,
2711
+ repo: bootstrap.repo,
2712
+ path: input.path ?? null,
2713
+ source: resolveLifecycleSource(input),
2714
+ event_type: "scan",
2715
+ memory_ids: bootstrap.created_ids,
2716
+ request: {
2717
+ repo_path: bootstrap.repo_path,
2718
+ client: input.client ?? null,
2719
+ trigger: "session_start_bootstrap"
2720
+ },
2721
+ result: {
2722
+ created: bootstrap.created_ids.length,
2723
+ status: bootstrap.status
2724
+ }
2725
+ });
2726
+ }
2727
+ const artifact = writeRepoContextArtifact(db, {
2728
+ repo: bootstrap.repo,
2729
+ repo_path: bootstrap.repo_path ?? input.repo_path ?? null
2730
+ });
2731
+ createActivityEvent(db, {
2732
+ session_id: input.session_id,
2733
+ repo: bootstrap.repo,
2734
+ path: input.path ?? null,
2735
+ source: resolveLifecycleSource(input),
2736
+ event_type: "session_start",
2737
+ request: {
2738
+ client: input.client ?? null,
2739
+ repo_path: bootstrap.repo_path ?? input.repo_path ?? null,
2740
+ meta: input.meta ?? {}
2741
+ },
2742
+ result: {
2743
+ bootstrap_status: bootstrap.status,
2744
+ created: bootstrap.created_ids.length,
2745
+ artifact_path: artifact.output_path,
2746
+ artifact_written: artifact.written
2747
+ }
2748
+ });
2749
+ return {
2750
+ session_id: input.session_id,
2751
+ repo: bootstrap.repo,
2752
+ repo_path: bootstrap.repo_path ?? input.repo_path ?? null,
2753
+ bootstrap_status: bootstrap.status,
2754
+ created_ids: bootstrap.created_ids
2755
+ };
2756
+ }
2757
+ function recordSessionLifecycleEvent(db, input) {
2758
+ const repo = input.repo ?? inferRepoSlugFromPath(input.repo_path) ?? null;
2759
+ createActivityEvent(db, {
2760
+ session_id: input.session_id,
2761
+ repo,
2762
+ path: input.path ?? null,
2763
+ source: resolveLifecycleSource(input),
2764
+ event_type: "session_event",
2765
+ request: {
2766
+ client: input.client ?? null,
2767
+ name: input.name,
2768
+ repo_path: input.repo_path ?? null,
2769
+ meta: input.meta ?? {}
2770
+ },
2771
+ result: input.payload ?? {}
2772
+ });
2773
+ return {
2774
+ session_id: input.session_id,
2775
+ repo,
2776
+ repo_path: input.repo_path ?? null,
2777
+ bootstrap_status: "skipped",
2778
+ created_ids: []
2779
+ };
2780
+ }
2781
+ function endSessionLifecycle(db, input) {
2782
+ const repo = input.repo ?? inferRepoSlugFromPath(input.repo_path) ?? null;
2783
+ createActivityEvent(db, {
2784
+ session_id: input.session_id,
2785
+ repo,
2786
+ path: input.path ?? null,
2787
+ source: resolveLifecycleSource(input),
2788
+ event_type: "session_end",
2789
+ request: {
2790
+ client: input.client ?? null,
2791
+ repo_path: input.repo_path ?? null,
2792
+ meta: input.meta ?? {}
2793
+ },
2794
+ result: input.payload ?? {}
2795
+ });
2796
+ return {
2797
+ session_id: input.session_id,
2798
+ repo,
2799
+ repo_path: input.repo_path ?? null,
2800
+ bootstrap_status: "skipped",
2801
+ created_ids: []
2802
+ };
2803
+ }
2804
+
2805
+ // src/mcp/fallback.ts
2806
+ async function captureCorrectionFallback(db, input, source) {
2807
+ const sessionId = input.session_id ?? `${source}-capture`;
2808
+ const ids = await processCorrection(db, input.text, {
2809
+ sessionId,
2810
+ repo: input.repo,
2811
+ path: input.path,
2812
+ agent: input.agent,
2813
+ prev_assistant_turn: input.prev_assistant_turn,
2814
+ recent_tool_calls: input.recent_tool_calls
2815
+ });
2816
+ createActivityEvent(db, {
2817
+ session_id: sessionId,
2818
+ repo: input.repo ?? null,
2819
+ path: input.path ?? null,
2820
+ source,
2821
+ event_type: "correction",
2822
+ memory_ids: ids,
2823
+ request: {
2824
+ agent: input.agent ?? null,
2825
+ prev_assistant_turn: input.prev_assistant_turn ?? null,
2826
+ recent_tool_calls: normalizeRecentToolCalls(input.recent_tool_calls),
2827
+ text: input.text
2828
+ },
2829
+ result: {
2830
+ created: ids,
2831
+ created_count: ids.length
2832
+ }
2833
+ });
2834
+ return {
2835
+ ids,
2836
+ session_id: sessionId
2837
+ };
2838
+ }
2839
+ function signalOutcomeFallback(db, input, source) {
2840
+ const feedbackId = recordFeedback(
2841
+ db,
2842
+ input.memory_id,
2843
+ input.session_id,
2844
+ input.injected ?? true,
2845
+ input.outcome
2846
+ );
2847
+ resolveMemoryInjectionOutcome(db, input.memory_id, input.session_id, input.outcome);
2848
+ const memory = getMemory(db, input.memory_id);
2849
+ createActivityEvent(db, {
2850
+ session_id: input.session_id,
2851
+ repo: memory?.repo ?? null,
2852
+ path: memory?.path_scope ?? null,
2853
+ source,
2854
+ event_type: "feedback",
2855
+ memory_ids: [input.memory_id],
2856
+ request: {
2857
+ context: input.context ?? null,
2858
+ injected: input.injected ?? true,
2859
+ outcome: input.outcome
2860
+ },
2861
+ result: {
2862
+ feedback_id: feedbackId
2863
+ }
2864
+ });
2865
+ return {
2866
+ feedback_id: feedbackId
2867
+ };
2868
+ }
2869
+ function sessionEndFallback(db, input) {
2870
+ const result = endSessionLifecycle(db, {
2871
+ session_id: input.session_id,
2872
+ client: input.agent ?? "mcp",
2873
+ repo: input.repo ?? null,
2874
+ repo_path: input.repo_path ?? null,
2875
+ path: input.path ?? null,
2876
+ payload: {
2877
+ ended_at: (/* @__PURE__ */ new Date()).toISOString(),
2878
+ turn_count: input.turn_count ?? null
2879
+ }
2880
+ });
2881
+ return {
2882
+ session_id: result.session_id,
2883
+ repo: result.repo
2884
+ };
2885
+ }
2886
+ function normalizeRecentToolCalls(toolCalls) {
2887
+ if (!toolCalls) return [];
2888
+ return toolCalls.map((toolCall) => ({
2889
+ name: toolCall.name,
2890
+ path: toolCall.path,
2891
+ input_summary: toolCall.input_summary,
2892
+ exit_code: toolCall.exit_code
2893
+ }));
2894
+ }
2895
+
2896
+ export {
2897
+ RECALL_DB_USER_VERSION,
2898
+ getDbPath,
2899
+ initDb,
2900
+ getDbUserVersion,
2901
+ resetDb,
2902
+ evaluateScannedMemory,
2903
+ scanAndStore,
2904
+ listInjectedMemoryIdsForSession,
2905
+ listPendingMemoryInjections,
2906
+ pathMatchesMemory,
2907
+ toolCallTouchesMemory,
2908
+ listInjectedHistoryIdsForSession,
2909
+ createHistorySnippet,
2910
+ listHistorySnippets,
2911
+ findHistorySnippetBySession,
2912
+ findHistorySnippetByRepoKind,
2913
+ updateHistorySnippet,
2914
+ removeHistoryVecRow,
2915
+ removeHistoryFtsRow,
2916
+ syncHistoryFtsIndex,
2917
+ bootstrapHistoryEmbeddings,
2918
+ verifyHistoryEmbeddings,
2919
+ searchHistorySnippets,
2920
+ compileContext,
2921
+ compileContextHybrid,
2922
+ exportMarkdown,
2923
+ exportClaude,
2924
+ exportCodex,
2925
+ ensureRepoBootstrapped,
2926
+ inferRepoSlugFromPath,
2927
+ writeRepoContextArtifact,
2928
+ startEvalSession,
2929
+ endEvalSession,
2930
+ incrementEvalCounter,
2931
+ computeMetrics,
2932
+ formatMetricsReport,
2933
+ loadRetrievalEvalFile,
2934
+ runRetrievalEval,
2935
+ formatRetrievalEvalReport,
2936
+ recordSignal,
2937
+ runTests,
2938
+ recordTestSignals,
2939
+ getSignalStats,
2940
+ createPolicy,
2941
+ listPolicies,
2942
+ togglePolicy,
2943
+ deletePolicy,
2944
+ evaluatePolicy,
2945
+ requestApproval,
2946
+ resolveApproval,
2947
+ listPendingApprovals,
2948
+ pruneMemories,
2949
+ formatPruneReport,
2950
+ createActivityEvent,
2951
+ listActivityEvents,
2952
+ listActivitySessions,
2953
+ startSessionLifecycle,
2954
+ recordSessionLifecycleEvent,
2955
+ endSessionLifecycle,
2956
+ captureCorrectionFallback,
2957
+ signalOutcomeFallback,
2958
+ sessionEndFallback
2959
+ };
2960
+ //# sourceMappingURL=chunk-PC43MBX5.js.map