@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.
- package/LICENSE +21 -0
- package/README.md +409 -0
- package/dist/chunk-4CV4JOE5.js +27 -0
- package/dist/chunk-4CV4JOE5.js.map +1 -0
- package/dist/chunk-A5UIRZU6.js +469 -0
- package/dist/chunk-A5UIRZU6.js.map +1 -0
- package/dist/chunk-AYHFPCGY.js +964 -0
- package/dist/chunk-AYHFPCGY.js.map +1 -0
- package/dist/chunk-DNFKAHS6.js +204 -0
- package/dist/chunk-DNFKAHS6.js.map +1 -0
- package/dist/chunk-GC5XMBG4.js +551 -0
- package/dist/chunk-GC5XMBG4.js.map +1 -0
- package/dist/chunk-IILLSHLM.js +3021 -0
- package/dist/chunk-IILLSHLM.js.map +1 -0
- package/dist/chunk-LVQW6WHK.js +146 -0
- package/dist/chunk-LVQW6WHK.js.map +1 -0
- package/dist/chunk-LZ6PMQRX.js +955 -0
- package/dist/chunk-LZ6PMQRX.js.map +1 -0
- package/dist/chunk-PC43MBX5.js +2960 -0
- package/dist/chunk-PC43MBX5.js.map +1 -0
- package/dist/chunk-VEPXEHRZ.js +1763 -0
- package/dist/chunk-VEPXEHRZ.js.map +1 -0
- package/dist/cleanup-TVOX2S2S.js +28 -0
- package/dist/cleanup-TVOX2S2S.js.map +1 -0
- package/dist/cli.js +3425 -0
- package/dist/cli.js.map +1 -0
- package/dist/daemon.js +1298 -0
- package/dist/daemon.js.map +1 -0
- package/dist/dispatcher-UGMU6THT.js +15 -0
- package/dist/dispatcher-UGMU6THT.js.map +1 -0
- package/dist/keychain-5QG52ANO.js +22 -0
- package/dist/keychain-5QG52ANO.js.map +1 -0
- package/dist/mcp.js +21 -0
- package/dist/mcp.js.map +1 -0
- package/dist/quality-Z7LPMMBC.js +17 -0
- package/dist/quality-Z7LPMMBC.js.map +1 -0
- package/dist/sync-server.js +225 -0
- package/dist/sync-server.js.map +1 -0
- package/dist/tasks-UOLSPXJQ.js +61 -0
- package/dist/tasks-UOLSPXJQ.js.map +1 -0
- package/dist/usage-CY3V72YN.js +101 -0
- package/dist/usage-CY3V72YN.js.map +1 -0
- package/drizzle/0000_initial_create.sql +240 -0
- package/drizzle/0001_rich_liz_osborn.sql +21 -0
- package/drizzle/0002_unknown_spot.sql +18 -0
- package/drizzle/0003_red_wendigo.sql +19 -0
- package/drizzle/0004_early_carlie_cooper.sql +1 -0
- package/drizzle/0005_simple_emma_frost.sql +96 -0
- package/drizzle/0006_keen_mongoose.sql +2 -0
- package/drizzle/0007_flawless_maximus.sql +15 -0
- package/drizzle/meta/0000_snapshot.json +1630 -0
- package/drizzle/meta/0001_snapshot.json +1773 -0
- package/drizzle/meta/0002_snapshot.json +1891 -0
- package/drizzle/meta/0003_snapshot.json +2014 -0
- package/drizzle/meta/0004_snapshot.json +2022 -0
- package/drizzle/meta/0005_snapshot.json +2064 -0
- package/drizzle/meta/0006_snapshot.json +2078 -0
- package/drizzle/meta/0007_snapshot.json +2183 -0
- package/drizzle/meta/_journal.json +62 -0
- package/package.json +64 -0
- package/scripts/recall-claude +7 -0
- package/scripts/recall-codex +7 -0
- 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
|