@edihasaj/recall 0.5.7 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/{chunk-K5FZ47NN.js → chunk-7XCLKPJ3.js} +6 -8
  2. package/dist/{chunk-K5FZ47NN.js.map → chunk-7XCLKPJ3.js.map} +1 -1
  3. package/dist/{chunk-A5UIRZU6.js → chunk-A6XEULA4.js} +3 -2
  4. package/dist/chunk-A6XEULA4.js.map +1 -0
  5. package/dist/{chunk-F56Y3SHS.js → chunk-E4HJDGCW.js} +7 -9
  6. package/dist/{chunk-F56Y3SHS.js.map → chunk-E4HJDGCW.js.map} +1 -1
  7. package/dist/{chunk-IILLSHLM.js → chunk-KAGIAOD7.js} +2583 -84
  8. package/dist/chunk-KAGIAOD7.js.map +1 -0
  9. package/dist/{chunk-FHKV6ELT.js → chunk-MJ4GGBTL.js} +11 -13
  10. package/dist/{chunk-FHKV6ELT.js.map → chunk-MJ4GGBTL.js.map} +1 -1
  11. package/dist/{chunk-LVQW6WHK.js → chunk-XUM7JEJU.js} +2 -2
  12. package/dist/{cleanup-TVOX2S2S.js → cleanup-MYSQ44EP.js} +4 -4
  13. package/dist/cli.js +206 -33
  14. package/dist/cli.js.map +1 -1
  15. package/dist/daemon.js +60 -49
  16. package/dist/daemon.js.map +1 -1
  17. package/dist/dispatcher-SUUX5AX6.js +16 -0
  18. package/dist/mcp.js +5 -5
  19. package/dist/{quality-Z7LPMMBC.js → quality-YTQKAEY6.js} +3 -3
  20. package/dist/{tasks-UOLSPXJQ.js → tasks-GSQUHD4F.js} +6 -3
  21. package/dist/{usage-CY3V72YN.js → usage-DU4TKVJH.js} +2 -2
  22. package/package.json +1 -1
  23. package/dist/chunk-A5UIRZU6.js.map +0 -1
  24. package/dist/chunk-GC5XMBG4.js +0 -551
  25. package/dist/chunk-GC5XMBG4.js.map +0 -1
  26. package/dist/chunk-IILLSHLM.js.map +0 -1
  27. package/dist/chunk-VEPXEHRZ.js +0 -1763
  28. package/dist/chunk-VEPXEHRZ.js.map +0 -1
  29. package/dist/dispatcher-UGMU6THT.js +0 -15
  30. /package/dist/{chunk-LVQW6WHK.js.map → chunk-XUM7JEJU.js.map} +0 -0
  31. /package/dist/{cleanup-TVOX2S2S.js.map → cleanup-MYSQ44EP.js.map} +0 -0
  32. /package/dist/{dispatcher-UGMU6THT.js.map → dispatcher-SUUX5AX6.js.map} +0 -0
  33. /package/dist/{quality-Z7LPMMBC.js.map → quality-YTQKAEY6.js.map} +0 -0
  34. /package/dist/{tasks-UOLSPXJQ.js.map → tasks-GSQUHD4F.js.map} +0 -0
  35. /package/dist/{usage-CY3V72YN.js.map → usage-DU4TKVJH.js.map} +0 -0
@@ -1,16 +1,26 @@
1
1
  import {
2
2
  activityEvents,
3
3
  auditTrail,
4
+ contradictions,
4
5
  feedbackEvents,
5
6
  historySnippets,
7
+ implicitSignals,
8
+ llmUsage,
9
+ maintenanceCleanupLog,
6
10
  memories,
7
11
  memoryEmbeddings,
12
+ memoryInjections,
8
13
  memoryMaintenanceTasks
9
- } from "./chunk-A5UIRZU6.js";
14
+ } from "./chunk-A6XEULA4.js";
15
+ import {
16
+ getProviderConfig,
17
+ hasProviderConfigured,
18
+ init_keychain
19
+ } from "./chunk-DNFKAHS6.js";
10
20
 
11
21
  // src/maintenance/tasks.ts
12
- import { and as and2, desc as desc2, eq as eq7, gt, inArray as inArray2, lt, or, isNull, sql as sql2 } from "drizzle-orm";
13
- import { randomUUID as randomUUID4 } from "crypto";
22
+ import { and as and4, desc as desc2, eq as eq11, gt, inArray as inArray3, lt, or, isNull, sql as sql3 } from "drizzle-orm";
23
+ import { randomUUID as randomUUID8 } from "crypto";
14
24
  import { z as z2 } from "zod";
15
25
 
16
26
  // src/embeddings/embeddings.ts
@@ -128,7 +138,8 @@ var MaintenanceTaskKind = z.enum([
128
138
  "refine_candidate",
129
139
  "summarize_session",
130
140
  "synthesize_repo",
131
- "verify_capture"
141
+ "verify_capture",
142
+ "extract_rules_from_prompt"
132
143
  ]);
133
144
  var MaintenanceTaskStatus = z.enum([
134
145
  "pending",
@@ -1536,8 +1547,8 @@ function rowToMemory(row) {
1536
1547
  }
1537
1548
 
1538
1549
  // src/maintenance/appliers.ts
1539
- import { eq as eq6 } from "drizzle-orm";
1540
- import { randomUUID as randomUUID3 } from "crypto";
1550
+ import { eq as eq10 } from "drizzle-orm";
1551
+ import { randomUUID as randomUUID7 } from "crypto";
1541
1552
 
1542
1553
  // src/models/memory.ts
1543
1554
  import { eq as eq4, and, gte, inArray, like, sql } from "drizzle-orm";
@@ -2074,6 +2085,2362 @@ function rowToAudit(row) {
2074
2085
  };
2075
2086
  }
2076
2087
 
2088
+ // src/repo/quality.ts
2089
+ import { eq as eq7 } from "drizzle-orm";
2090
+
2091
+ // src/health/scoring.ts
2092
+ import { eq as eq6 } from "drizzle-orm";
2093
+ var WEIGHTS = {
2094
+ confidence: 0.4,
2095
+ freshness: 0.25,
2096
+ follow_rate: 0.2,
2097
+ signal_ratio: 0.15
2098
+ };
2099
+ function computeHealthScore(db, memoryId) {
2100
+ const mem = getMemory(db, memoryId);
2101
+ if (!mem) return null;
2102
+ const confidence = mem.confidence;
2103
+ const freshness = computeFreshness(mem);
2104
+ const followRate = computeFollowRate(db, memoryId);
2105
+ const signalRatio = computeSignalRatio(db, memoryId);
2106
+ const score = WEIGHTS.confidence * confidence + WEIGHTS.freshness * freshness + WEIGHTS.follow_rate * followRate + WEIGHTS.signal_ratio * signalRatio;
2107
+ return {
2108
+ memory_id: memoryId,
2109
+ score: clamp(score),
2110
+ confidence_component: confidence,
2111
+ freshness_component: freshness,
2112
+ follow_rate_component: followRate,
2113
+ signal_ratio_component: signalRatio,
2114
+ computed_at: (/* @__PURE__ */ new Date()).toISOString()
2115
+ };
2116
+ }
2117
+ function computeAllHealthScores(db, repo) {
2118
+ const mems = repo ? queryMemories(db, { repo }) : listMemories(db);
2119
+ const scores = [];
2120
+ for (const mem of mems) {
2121
+ if (mem.status === "rejected") continue;
2122
+ const score = computeHealthScore(db, mem.id);
2123
+ if (score) scores.push(score);
2124
+ }
2125
+ return scores.sort((a, b) => b.score - a.score);
2126
+ }
2127
+ function computeFreshness(mem) {
2128
+ const now = Date.now();
2129
+ const referenceDate = mem.last_validated_at ?? mem.last_injected_at ?? mem.updated_at;
2130
+ const age = now - new Date(referenceDate).getTime();
2131
+ const dayMs = 864e5;
2132
+ const halfLife = 30 * dayMs;
2133
+ const freshness = Math.pow(0.5, age / halfLife);
2134
+ return clamp(freshness);
2135
+ }
2136
+ function computeFollowRate(db, memoryId) {
2137
+ const feedback = db.select().from(feedbackEvents).where(eq6(feedbackEvents.memory_id, memoryId)).all();
2138
+ if (feedback.length === 0) return 0.5;
2139
+ const followed = feedback.filter((f) => f.outcome === "followed").length;
2140
+ return followed / feedback.length;
2141
+ }
2142
+ function computeSignalRatio(db, memoryId) {
2143
+ const signals = db.select().from(implicitSignals).where(eq6(implicitSignals.memory_id, memoryId)).all();
2144
+ if (signals.length === 0) return 0.5;
2145
+ const positive = signals.filter(
2146
+ (s) => ["test_pass", "file_unchanged", "task_accepted"].includes(s.signal_type)
2147
+ ).length;
2148
+ return positive / signals.length;
2149
+ }
2150
+ function formatHealthReport(scores) {
2151
+ if (scores.length === 0) return "No memories to score.";
2152
+ const lines = [
2153
+ "# Memory Health Report",
2154
+ "",
2155
+ `Total: ${scores.length} memories scored`,
2156
+ "",
2157
+ "| Score | Conf | Fresh | Follow | Signal | ID |",
2158
+ "|-------|------|-------|--------|--------|----------|"
2159
+ ];
2160
+ for (const s of scores.slice(0, 30)) {
2161
+ lines.push(
2162
+ `| ${pct(s.score)} | ${pct(s.confidence_component)} | ${pct(s.freshness_component)} | ${pct(s.follow_rate_component)} | ${pct(s.signal_ratio_component)} | ${s.memory_id.slice(0, 8)} |`
2163
+ );
2164
+ }
2165
+ const avg = scores.reduce((sum, s) => sum + s.score, 0) / scores.length;
2166
+ const unhealthy = scores.filter((s) => s.score < 0.3).length;
2167
+ const healthy = scores.filter((s) => s.score >= 0.6).length;
2168
+ lines.push("");
2169
+ lines.push(`Avg score: ${pct(avg)}`);
2170
+ lines.push(`Healthy (\u22650.6): ${healthy} | Unhealthy (<0.3): ${unhealthy}`);
2171
+ return lines.join("\n");
2172
+ }
2173
+ function clamp(n) {
2174
+ return Math.max(0, Math.min(1, n));
2175
+ }
2176
+ function pct(n) {
2177
+ return (n * 100).toFixed(0).padStart(3) + "%";
2178
+ }
2179
+
2180
+ // src/repo/quality.ts
2181
+ function getRepoQualityProfile(db, repo) {
2182
+ if (!repo) {
2183
+ return defaultProfile();
2184
+ }
2185
+ const memories4 = queryMemories(db, { repo }).filter((m) => m.status !== "rejected");
2186
+ const active = memories4.filter((m) => m.status === "active");
2187
+ const activeCount = active.length;
2188
+ const totalCount = memories4.length;
2189
+ const health = computeAllHealthScores(db, repo);
2190
+ const avgHealth = health.length > 0 ? health.reduce((sum, item) => sum + item.score, 0) / health.length : 0;
2191
+ const totalInjections = memories4.reduce((sum, m) => sum + m.injection_count, 0);
2192
+ const totalOverrides = memories4.reduce((sum, m) => sum + m.override_count, 0);
2193
+ const overrideRate = totalInjections > 0 ? totalOverrides / totalInjections : 0;
2194
+ const ids = new Set(memories4.map((m) => m.id));
2195
+ const unresolved = db.select().from(contradictions).where(eq7(contradictions.resolved, false)).all().filter((item) => ids.has(item.memory_a_id) || ids.has(item.memory_b_id));
2196
+ const contradictionRate = activeCount > 0 ? unresolved.length / activeCount : 0;
2197
+ const stage = classifyStage(activeCount);
2198
+ const pressure = clamp2(activeCount / 50);
2199
+ const score = clamp2(
2200
+ avgHealth * 0.5 + (1 - clamp2(overrideRate)) * 0.25 + (1 - clamp2(contradictionRate)) * 0.15 + (1 - pressure) * 0.1
2201
+ );
2202
+ let repeatSessionsRequired = stage === "cold" ? 2 : stage === "growing" ? 3 : 4;
2203
+ if (score >= 0.75 && repeatSessionsRequired > 2) {
2204
+ repeatSessionsRequired -= 1;
2205
+ } else if (score < 0.45) {
2206
+ repeatSessionsRequired += 1;
2207
+ }
2208
+ let compileConfidenceThreshold = stage === "cold" ? CONFIDENCE.ACTIVE_MIN : stage === "growing" ? 0.68 : 0.72;
2209
+ if (score < 0.45) {
2210
+ compileConfidenceThreshold += 0.05;
2211
+ } else if (score > 0.8) {
2212
+ compileConfidenceThreshold -= 0.03;
2213
+ }
2214
+ let dedupSimilarityThreshold = stage === "cold" ? 0.85 : stage === "growing" ? 0.8 : 0.75;
2215
+ if (score < 0.45) {
2216
+ dedupSimilarityThreshold -= 0.05;
2217
+ }
2218
+ return {
2219
+ repo,
2220
+ stage,
2221
+ score,
2222
+ total_count: totalCount,
2223
+ active_count: activeCount,
2224
+ avg_health: avgHealth,
2225
+ override_rate: clamp2(overrideRate),
2226
+ contradiction_rate: clamp2(contradictionRate),
2227
+ repeat_sessions_required: repeatSessionsRequired,
2228
+ compile_confidence_threshold: clamp2(
2229
+ compileConfidenceThreshold,
2230
+ CONFIDENCE.ACTIVE_MIN,
2231
+ 0.82
2232
+ ),
2233
+ dedup_similarity_threshold: clamp2(dedupSimilarityThreshold, 0.65, 0.9)
2234
+ };
2235
+ }
2236
+ function seedCandidateConfidence(baseConfidence, profile) {
2237
+ const maturityPenalty = profile.stage === "cold" ? 0 : profile.stage === "growing" ? 0.03 : 0.05;
2238
+ const qualityPenalty = profile.score < 0.45 ? 0.03 : 0;
2239
+ return clamp2(
2240
+ baseConfidence - maturityPenalty - qualityPenalty,
2241
+ CONFIDENCE.TRANSIENT_MAX + 0.05,
2242
+ CONFIDENCE.ACTIVE_MIN - 0.01
2243
+ );
2244
+ }
2245
+ function seedScannedConfidence(baseConfidence, profile) {
2246
+ const maturityPenalty = profile.stage === "cold" ? 0 : profile.stage === "growing" ? 0.02 : 0.05;
2247
+ const qualityPenalty = profile.score < 0.45 ? 0.03 : 0;
2248
+ return clamp2(
2249
+ baseConfidence - maturityPenalty - qualityPenalty,
2250
+ 0.5,
2251
+ 0.85
2252
+ );
2253
+ }
2254
+ function classifyStage(activeCount) {
2255
+ if (activeCount < 10) return "cold";
2256
+ if (activeCount < 50) return "growing";
2257
+ return "mature";
2258
+ }
2259
+ function defaultProfile() {
2260
+ return {
2261
+ stage: "cold",
2262
+ score: 0.35,
2263
+ total_count: 0,
2264
+ active_count: 0,
2265
+ avg_health: 0,
2266
+ override_rate: 0,
2267
+ contradiction_rate: 0,
2268
+ repeat_sessions_required: 2,
2269
+ compile_confidence_threshold: CONFIDENCE.ACTIVE_MIN,
2270
+ dedup_similarity_threshold: 0.85
2271
+ };
2272
+ }
2273
+ function clamp2(n, min = 0, max = 1) {
2274
+ return Math.max(min, Math.min(max, n));
2275
+ }
2276
+
2277
+ // src/llm/client.ts
2278
+ init_keychain();
2279
+ import { randomUUID as randomUUID3 } from "crypto";
2280
+ var LlmCredentialError = class extends Error {
2281
+ };
2282
+ var LlmRequestError = class extends Error {
2283
+ };
2284
+ var DEFAULT_MODELS = {
2285
+ openai: "gpt-4o-mini",
2286
+ anthropic: "claude-haiku-4-5-20251001",
2287
+ // For Azure the "model" is the deployment name, set by the user when they
2288
+ // provisioned the deployment. We leave it empty so we always fall through
2289
+ // to the deployment from AzureOpenAiConfig.
2290
+ "azure-openai": ""
2291
+ };
2292
+ var COST_PER_M_TOKENS = {
2293
+ "gpt-4o-mini": { input: 0.15, output: 0.6 },
2294
+ "gpt-4o": { input: 2.5, output: 10 },
2295
+ "claude-haiku-4-5-20251001": { input: 1, output: 5 },
2296
+ "claude-sonnet-4-6": { input: 3, output: 15 },
2297
+ "claude-opus-4-7": { input: 15, output: 75 }
2298
+ };
2299
+ async function callLlm(db, input) {
2300
+ const provider = input.provider;
2301
+ const config = getProviderConfig(provider);
2302
+ if (!config) {
2303
+ throw new LlmCredentialError(missingCredentialMessage(provider));
2304
+ }
2305
+ const model = input.model ?? (provider === "azure-openai" ? config.deployment : DEFAULT_MODELS[provider]);
2306
+ const started = Date.now();
2307
+ let result = null;
2308
+ let errorMessage;
2309
+ try {
2310
+ if (provider === "openai") {
2311
+ result = await callOpenAi(config.key, model, input);
2312
+ } else if (provider === "anthropic") {
2313
+ result = await callAnthropic(config.key, model, input);
2314
+ } else {
2315
+ result = await callAzureOpenAi(config, model, input);
2316
+ }
2317
+ return result;
2318
+ } catch (err) {
2319
+ errorMessage = err instanceof Error ? err.message : String(err);
2320
+ throw err;
2321
+ } finally {
2322
+ try {
2323
+ await recordUsage(db, {
2324
+ provider,
2325
+ model,
2326
+ task_kind: input.task_kind,
2327
+ task_id: input.task_id ?? null,
2328
+ repo: input.repo ?? null,
2329
+ usage: result?.usage ?? { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0, cost_usd: null },
2330
+ duration_ms: Date.now() - started,
2331
+ ok: Boolean(result),
2332
+ error: errorMessage
2333
+ });
2334
+ } catch {
2335
+ }
2336
+ }
2337
+ }
2338
+ async function callOpenAi(apiKey, model, input) {
2339
+ const started = Date.now();
2340
+ const response = await fetch("https://api.openai.com/v1/chat/completions", {
2341
+ method: "POST",
2342
+ headers: {
2343
+ "Content-Type": "application/json",
2344
+ Authorization: `Bearer ${apiKey}`
2345
+ },
2346
+ body: JSON.stringify({
2347
+ model,
2348
+ messages: [
2349
+ { role: "system", content: input.system },
2350
+ { role: "user", content: input.user }
2351
+ ],
2352
+ max_completion_tokens: input.max_output_tokens ?? 2048,
2353
+ temperature: input.temperature ?? 0
2354
+ })
2355
+ });
2356
+ if (!response.ok) {
2357
+ const body = await safeText(response);
2358
+ throw new LlmRequestError(`OpenAI ${response.status}: ${body.slice(0, 400)}`);
2359
+ }
2360
+ const payload = await response.json();
2361
+ const text = payload.choices?.[0]?.message?.content?.trim() ?? "";
2362
+ const prompt_tokens = payload.usage?.prompt_tokens ?? 0;
2363
+ const completion_tokens = payload.usage?.completion_tokens ?? 0;
2364
+ const total_tokens = payload.usage?.total_tokens ?? prompt_tokens + completion_tokens;
2365
+ return {
2366
+ text,
2367
+ model,
2368
+ provider: "openai",
2369
+ duration_ms: Date.now() - started,
2370
+ usage: {
2371
+ prompt_tokens,
2372
+ completion_tokens,
2373
+ total_tokens,
2374
+ cost_usd: computeCost(model, prompt_tokens, completion_tokens)
2375
+ }
2376
+ };
2377
+ }
2378
+ async function callAzureOpenAi(config, deployment, input) {
2379
+ const started = Date.now();
2380
+ const url = `${config.endpoint}/openai/deployments/${encodeURIComponent(deployment)}/chat/completions?api-version=${encodeURIComponent(config.api_version)}`;
2381
+ const response = await fetch(url, {
2382
+ method: "POST",
2383
+ headers: {
2384
+ "Content-Type": "application/json",
2385
+ "api-key": config.key
2386
+ },
2387
+ body: JSON.stringify({
2388
+ messages: [
2389
+ { role: "system", content: input.system },
2390
+ { role: "user", content: input.user }
2391
+ ],
2392
+ max_completion_tokens: input.max_output_tokens ?? 2048,
2393
+ temperature: input.temperature ?? 0
2394
+ })
2395
+ });
2396
+ if (!response.ok) {
2397
+ const body = await safeText(response);
2398
+ throw new LlmRequestError(`Azure OpenAI ${response.status}: ${body.slice(0, 400)}`);
2399
+ }
2400
+ const payload = await response.json();
2401
+ const text = payload.choices?.[0]?.message?.content?.trim() ?? "";
2402
+ const prompt_tokens = payload.usage?.prompt_tokens ?? 0;
2403
+ const completion_tokens = payload.usage?.completion_tokens ?? 0;
2404
+ const total_tokens = payload.usage?.total_tokens ?? prompt_tokens + completion_tokens;
2405
+ return {
2406
+ text,
2407
+ model: deployment,
2408
+ provider: "azure-openai",
2409
+ duration_ms: Date.now() - started,
2410
+ usage: {
2411
+ prompt_tokens,
2412
+ completion_tokens,
2413
+ total_tokens,
2414
+ cost_usd: computeCost(deployment, prompt_tokens, completion_tokens)
2415
+ }
2416
+ };
2417
+ }
2418
+ async function callAnthropic(apiKey, model, input) {
2419
+ const started = Date.now();
2420
+ const response = await fetch("https://api.anthropic.com/v1/messages", {
2421
+ method: "POST",
2422
+ headers: {
2423
+ "Content-Type": "application/json",
2424
+ "x-api-key": apiKey,
2425
+ "anthropic-version": "2023-06-01"
2426
+ },
2427
+ body: JSON.stringify({
2428
+ model,
2429
+ system: input.system,
2430
+ messages: [{ role: "user", content: input.user }],
2431
+ max_tokens: input.max_output_tokens ?? 2048,
2432
+ temperature: input.temperature ?? 0
2433
+ })
2434
+ });
2435
+ if (!response.ok) {
2436
+ const body = await safeText(response);
2437
+ throw new LlmRequestError(`Anthropic ${response.status}: ${body.slice(0, 400)}`);
2438
+ }
2439
+ const payload = await response.json();
2440
+ const text = (payload.content ?? []).filter((block) => block.type === "text" && typeof block.text === "string").map((block) => block.text).join("").trim();
2441
+ const prompt_tokens = payload.usage?.input_tokens ?? 0;
2442
+ const completion_tokens = payload.usage?.output_tokens ?? 0;
2443
+ return {
2444
+ text,
2445
+ model,
2446
+ provider: "anthropic",
2447
+ duration_ms: Date.now() - started,
2448
+ usage: {
2449
+ prompt_tokens,
2450
+ completion_tokens,
2451
+ total_tokens: prompt_tokens + completion_tokens,
2452
+ cost_usd: computeCost(model, prompt_tokens, completion_tokens)
2453
+ }
2454
+ };
2455
+ }
2456
+ function computeCost(model, inputTokens, outputTokens) {
2457
+ const rates = COST_PER_M_TOKENS[model];
2458
+ if (!rates) return null;
2459
+ return inputTokens / 1e6 * rates.input + outputTokens / 1e6 * rates.output;
2460
+ }
2461
+ async function recordUsage(db, row) {
2462
+ await db.insert(llmUsage).values({
2463
+ id: randomUUID3(),
2464
+ provider: row.provider,
2465
+ model: row.model,
2466
+ task_kind: row.task_kind,
2467
+ task_id: row.task_id,
2468
+ repo: row.repo,
2469
+ prompt_tokens: row.usage.prompt_tokens,
2470
+ completion_tokens: row.usage.completion_tokens,
2471
+ total_tokens: row.usage.total_tokens,
2472
+ cost_usd: row.usage.cost_usd ?? null,
2473
+ duration_ms: row.duration_ms,
2474
+ ok: row.ok,
2475
+ error: row.error ?? null,
2476
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
2477
+ });
2478
+ }
2479
+ function missingCredentialMessage(provider) {
2480
+ switch (provider) {
2481
+ case "openai":
2482
+ return `No API key for provider "openai". Set it via \`recall maintenance credentials set openai <key>\` or the OPENAI_API_KEY env var.`;
2483
+ case "anthropic":
2484
+ return `No API key for provider "anthropic". Set it via \`recall maintenance credentials set anthropic <key>\` or the ANTHROPIC_API_KEY env var.`;
2485
+ case "azure-openai":
2486
+ return `Azure OpenAI is not fully configured. Run \`recall maintenance credentials set azure --endpoint <url> --deployment <name> --api-version <version> <key>\` or set AZURE_OPENAI_{ENDPOINT,DEPLOYMENT,API_VERSION,API_KEY}.`;
2487
+ }
2488
+ }
2489
+ async function safeText(response) {
2490
+ try {
2491
+ return await response.text();
2492
+ } catch {
2493
+ return "";
2494
+ }
2495
+ }
2496
+
2497
+ // src/maintenance/dispatcher.ts
2498
+ init_keychain();
2499
+ var DISPATCH_AGENT = "recall:dispatcher";
2500
+ var DEFAULT_LEASE_SECONDS = 120;
2501
+ async function dispatchPendingTasks(db, options = {}) {
2502
+ const provider = resolveProvider2(options.provider);
2503
+ const report = {
2504
+ provider,
2505
+ model: null,
2506
+ dry_run: Boolean(options.dryRun),
2507
+ attempted: 0,
2508
+ applied: 0,
2509
+ rejected: 0,
2510
+ released: 0,
2511
+ outcomes: []
2512
+ };
2513
+ if (!provider) return report;
2514
+ const pending = listTasks(db, {
2515
+ status: "pending",
2516
+ kinds: options.kinds,
2517
+ repo: options.repo,
2518
+ limit: options.maxTasks ?? 5
2519
+ });
2520
+ for (const task of pending) {
2521
+ if (options.dryRun) {
2522
+ report.outcomes.push({
2523
+ task_id: task.id,
2524
+ kind: task.kind,
2525
+ repo: task.repo,
2526
+ status: "skipped",
2527
+ reason: "dry-run"
2528
+ });
2529
+ continue;
2530
+ }
2531
+ report.attempted += 1;
2532
+ const outcome = await runSingle(db, task, provider, options.model);
2533
+ report.outcomes.push(outcome);
2534
+ if (outcome.status === "applied") report.applied += 1;
2535
+ else if (outcome.status === "rejected") report.rejected += 1;
2536
+ else if (outcome.status === "released") report.released += 1;
2537
+ if (outcome.prompt_tokens != null && !report.model) {
2538
+ const last = report.outcomes[report.outcomes.length - 1];
2539
+ report.model = last.task_id ? options.model ?? null : null;
2540
+ }
2541
+ }
2542
+ return report;
2543
+ }
2544
+ async function runSingle(db, task, provider, model) {
2545
+ let claimed;
2546
+ try {
2547
+ const claim = claimTask(db, task.id, DISPATCH_AGENT, DEFAULT_LEASE_SECONDS);
2548
+ claimed = claim.task;
2549
+ } catch (err) {
2550
+ if (err instanceof TaskClaimConflictError) {
2551
+ return {
2552
+ task_id: task.id,
2553
+ kind: task.kind,
2554
+ repo: task.repo,
2555
+ status: "skipped",
2556
+ reason: err.reason
2557
+ };
2558
+ }
2559
+ throw err;
2560
+ }
2561
+ const prompt = buildPrompt(claimed);
2562
+ if (!prompt) {
2563
+ releaseTask(db, claimed.id, DISPATCH_AGENT);
2564
+ return {
2565
+ task_id: claimed.id,
2566
+ kind: claimed.kind,
2567
+ repo: claimed.repo,
2568
+ status: "released",
2569
+ reason: "no prompt builder"
2570
+ };
2571
+ }
2572
+ try {
2573
+ const llmResult = await callLlm(db, {
2574
+ provider,
2575
+ model,
2576
+ system: prompt.system,
2577
+ user: prompt.user,
2578
+ max_output_tokens: prompt.max_output_tokens,
2579
+ task_kind: claimed.kind,
2580
+ task_id: claimed.id,
2581
+ repo: claimed.repo
2582
+ });
2583
+ const parsed = parseJson(llmResult.text);
2584
+ if (!parsed) {
2585
+ releaseTask(db, claimed.id, DISPATCH_AGENT);
2586
+ return {
2587
+ task_id: claimed.id,
2588
+ kind: claimed.kind,
2589
+ repo: claimed.repo,
2590
+ status: "released",
2591
+ reason: "llm did not return valid JSON",
2592
+ prompt_tokens: llmResult.usage.prompt_tokens,
2593
+ completion_tokens: llmResult.usage.completion_tokens,
2594
+ cost_usd: llmResult.usage.cost_usd,
2595
+ duration_ms: llmResult.duration_ms
2596
+ };
2597
+ }
2598
+ const submit = submitTask(db, claimed.id, DISPATCH_AGENT, parsed);
2599
+ if (submit.status === "applied") {
2600
+ return {
2601
+ task_id: claimed.id,
2602
+ kind: claimed.kind,
2603
+ repo: claimed.repo,
2604
+ status: "applied",
2605
+ target_id: submit.target_id,
2606
+ changed_fields: submit.changed_fields,
2607
+ prompt_tokens: llmResult.usage.prompt_tokens,
2608
+ completion_tokens: llmResult.usage.completion_tokens,
2609
+ cost_usd: llmResult.usage.cost_usd,
2610
+ duration_ms: llmResult.duration_ms
2611
+ };
2612
+ }
2613
+ return {
2614
+ task_id: claimed.id,
2615
+ kind: claimed.kind,
2616
+ repo: claimed.repo,
2617
+ status: "rejected",
2618
+ reason: submit.reason,
2619
+ prompt_tokens: llmResult.usage.prompt_tokens,
2620
+ completion_tokens: llmResult.usage.completion_tokens,
2621
+ cost_usd: llmResult.usage.cost_usd,
2622
+ duration_ms: llmResult.duration_ms
2623
+ };
2624
+ } catch (err) {
2625
+ releaseTask(db, claimed.id, DISPATCH_AGENT);
2626
+ const reason = err instanceof LlmCredentialError ? err.message : err instanceof Error ? err.message : String(err);
2627
+ return {
2628
+ task_id: claimed.id,
2629
+ kind: claimed.kind,
2630
+ repo: claimed.repo,
2631
+ status: "released",
2632
+ reason
2633
+ };
2634
+ }
2635
+ }
2636
+ function hasAnyLlmProvider() {
2637
+ return resolveProvider2() != null;
2638
+ }
2639
+ function resolveProvider2(preferred) {
2640
+ const candidates = preferred ? [preferred] : ["anthropic", "azure-openai", "openai"];
2641
+ for (const provider of candidates) {
2642
+ if (hasProviderConfigured(provider)) return provider;
2643
+ }
2644
+ return null;
2645
+ }
2646
+ function parseJson(text) {
2647
+ const trimmed = text.trim();
2648
+ if (trimmed.length === 0) return null;
2649
+ const stripped = trimmed.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "").trim();
2650
+ try {
2651
+ return JSON.parse(stripped);
2652
+ } catch {
2653
+ const first = stripped.indexOf("{");
2654
+ const last = stripped.lastIndexOf("}");
2655
+ if (first >= 0 && last > first) {
2656
+ try {
2657
+ return JSON.parse(stripped.slice(first, last + 1));
2658
+ } catch {
2659
+ return null;
2660
+ }
2661
+ }
2662
+ return null;
2663
+ }
2664
+ }
2665
+ function buildPrompt(task) {
2666
+ switch (task.kind) {
2667
+ case "verify_capture":
2668
+ return buildVerifyCapturePrompt(task);
2669
+ case "refine_candidate":
2670
+ return buildRefineCandidatePrompt(task);
2671
+ case "summarize_history":
2672
+ return buildSummarizeHistoryPrompt(task);
2673
+ case "merge_duplicates":
2674
+ return buildMergeDuplicatesPrompt(task);
2675
+ case "summarize_session":
2676
+ return buildSummarizeSessionPrompt(task);
2677
+ case "synthesize_repo":
2678
+ return buildSynthesizeRepoPrompt(task);
2679
+ case "extract_rules_from_prompt":
2680
+ return buildExtractRulesFromPromptPrompt(task);
2681
+ default:
2682
+ return null;
2683
+ }
2684
+ }
2685
+ function buildExtractRulesFromPromptPrompt(task) {
2686
+ const payload = task.payload;
2687
+ const system = [
2688
+ "You are the capture judge for a coding-agent memory store.",
2689
+ "Read the USER PROMPT below and extract zero or more DURABLE RULES the user is stating about how the agent should behave on this repo (or globally).",
2690
+ "A rule must be:",
2691
+ " (a) Imperative \u2014 telling the agent what to always/never do, or what to prefer.",
2692
+ " (b) Durable \u2014 the user expects it to apply across future sessions, not just this one task.",
2693
+ " (c) Specific \u2014 concrete enough that an agent can follow it without further clarification.",
2694
+ "Recognize rules in ANY natural language (English, Spanish, French, German, Italian, Portuguese, Russian, Chinese, Japanese, Albanian, Turkish, Arabic, \u2026). When the source is non-English, return the cleaned rule TEXT in English so memories are searchable across sessions.",
2695
+ "REJECT (return empty list):",
2696
+ " \u2022 Questions ('should we use X?'), one-off task requests ('please fix this bug now'), narration ('I never use X' as description of past behavior), code paste, error logs, transcripts.",
2697
+ " \u2022 Trigger-template rules ('when user says X, do Y') and destructive-risky rules (delete/wipe/drop + settings/secrets/branches/files) \u2014 return them but flag is_destructive_risky=true so they require explicit user confirm before going active.",
2698
+ " \u2022 Anything where the intent is ambiguous without surrounding context.",
2699
+ "Output a single CANONICAL sentence per rule, in imperative mood. Strip filler words (uh, um, like, you know).",
2700
+ "Set scope as tight as the evidence supports: 'path' if a specific file/dir is referenced, 'repo' for repo-wide, 'global' only if the user explicitly says 'across all my projects' / 'globally' / 'everywhere'.",
2701
+ "Confidence: 0.9+ for unambiguous explicit rules, 0.5-0.8 for inferred/soft preferences, below 0.5 means you should probably not return it at all.",
2702
+ "Be STRICT \u2014 false positives produce wrong agent behavior. When unsure, prefer empty list over a low-confidence guess.",
2703
+ JSON_ONLY
2704
+ ].join(" ");
2705
+ const recentToolsSummary = summarizeRecentToolCalls(payload.recent_tool_calls);
2706
+ const user = [
2707
+ `Repo: ${JSON.stringify(payload.repo ?? null)}`,
2708
+ `Path: ${JSON.stringify(payload.path ?? null)}`,
2709
+ `Agent: ${JSON.stringify(payload.agent ?? null)}`,
2710
+ payload.prev_assistant_turn ? `Previous assistant turn (for context only \u2014 do not extract rules from it): ${JSON.stringify(payload.prev_assistant_turn.slice(0, 800))}` : "Previous assistant turn: null",
2711
+ recentToolsSummary ? `Recent tool calls: ${recentToolsSummary}` : "Recent tool calls: none",
2712
+ "",
2713
+ `USER PROMPT:`,
2714
+ JSON.stringify(payload.raw_prompt ?? ""),
2715
+ "",
2716
+ 'Return JSON: {"rules": [{"text": string, "type": "rule"|"decision"|"review_pattern"|"command"|"gotcha", "scope": "session"|"path"|"repo"|"team"|"global", "path_scope": string|null, "confidence": number, "is_destructive_risky": boolean, "rationale": string}], "dropped_reason": string?}',
2717
+ 'When the prompt contains no durable rule, return {"rules": []} with a brief dropped_reason.'
2718
+ ].join("\n");
2719
+ return { system, user, max_output_tokens: 800 };
2720
+ }
2721
+ function summarizeRecentToolCalls(value) {
2722
+ if (!Array.isArray(value) || value.length === 0) return null;
2723
+ return value.slice(-5).map((entry) => {
2724
+ if (!entry || typeof entry !== "object") return null;
2725
+ const name = typeof entry.name === "string" ? entry.name : "tool";
2726
+ const path = typeof entry.path === "string" ? entry.path : null;
2727
+ return path ? `${name}(${path})` : name;
2728
+ }).filter(Boolean).join(", ");
2729
+ }
2730
+ function buildVerifyCapturePrompt(task) {
2731
+ const payload = task.payload;
2732
+ const system = [
2733
+ "You verify a captured candidate rule for a coding-agent memory store.",
2734
+ "Decide if it is a durable rule worth saving, salvageable but needs rewriting, or noise/narration.",
2735
+ "Be strict \u2014 false positives produce wrong agent behavior. When unsure, prefer reject over save.",
2736
+ "Reject voice transcripts, descriptive clauses about what the user does ('things I never use'), one-shot task chatter, and any text whose intent is unclear without surrounding context.",
2737
+ "When rewriting, output a single canonical sentence in imperative mood. Keep scope as tight as the evidence supports.",
2738
+ 'Flag is_destructive_risky=true when the rule pairs a destructive verb (remove/delete/drop/wipe) with high-risk targets (settings/config/files/secrets/branches), OR when it is shaped as a literal-trigger rule ("when user says X, do Y") \u2014 both require explicit user confirm regardless.',
2739
+ JSON_ONLY
2740
+ ].join(" ");
2741
+ const user = [
2742
+ `Candidate text: ${JSON.stringify(payload.text ?? "")}`,
2743
+ `Inferred scope: ${payload.inferred_scope ?? "repo"}`,
2744
+ `Inferred path_scope: ${JSON.stringify(payload.inferred_path_scope ?? null)}`,
2745
+ `Repo: ${JSON.stringify(payload.repo ?? null)}`,
2746
+ `Capture context: ${JSON.stringify(payload.capture_context ?? null)}`,
2747
+ "",
2748
+ 'Return JSON: {"verdict": "save"|"rewrite"|"reject", "cleaned_text"?: string, "scope"?: "session"|"path"|"repo"|"team"|"global", "path_scope"?: string|null, "is_destructive_risky"?: boolean, "reason"?: string}'
2749
+ ].join("\n");
2750
+ return { system, user };
2751
+ }
2752
+ var JSON_ONLY = "Respond with a single JSON object matching the required schema, no prose, no markdown fences.";
2753
+ function buildRefineCandidatePrompt(task) {
2754
+ const payload = task.payload;
2755
+ const system = [
2756
+ "You refine candidate memories in a coding-agent memory store.",
2757
+ "Keep only durable rules/commands/gotchas. Clamp scope tighter when the evidence is path-specific.",
2758
+ JSON_ONLY
2759
+ ].join(" ");
2760
+ const user = [
2761
+ `Current memory text: ${JSON.stringify(payload.text ?? "")}`,
2762
+ `Current scope: ${payload.current_scope ?? "repo"}`,
2763
+ `Current path_scope: ${JSON.stringify(payload.current_path_scope ?? null)}`,
2764
+ `Repo: ${JSON.stringify(payload.repo ?? null)}`,
2765
+ `Repetition count: ${payload.repetition_count ?? 0}`,
2766
+ "",
2767
+ 'Return JSON: {"refined_text": string, "scope": "session"|"path"|"repo"|"team"|"global", "path_scope": string|null, "rationale": string, "verdict"?: "rewrite"|"reject"}'
2768
+ ].join("\n");
2769
+ return { system, user };
2770
+ }
2771
+ function buildSummarizeHistoryPrompt(task) {
2772
+ const payload = task.payload;
2773
+ const system = [
2774
+ "You compress activity snippets in a coding-agent memory store.",
2775
+ "Keep the essential facts; drop filler. <= 3 short sentences.",
2776
+ JSON_ONLY
2777
+ ].join(" ");
2778
+ const user = [
2779
+ `Kind: ${payload.kind ?? "unknown"}`,
2780
+ `Repo: ${JSON.stringify(payload.repo ?? null)}`,
2781
+ `Current text: ${JSON.stringify(payload.current_text ?? "")}`,
2782
+ "",
2783
+ 'Return JSON: {"summary_text": string, "tags": [string, ...]}'
2784
+ ].join("\n");
2785
+ return { system, user };
2786
+ }
2787
+ function buildMergeDuplicatesPrompt(task) {
2788
+ const payload = task.payload;
2789
+ const system = [
2790
+ "You pick the best memory among near-duplicates in a coding-agent memory store.",
2791
+ "Choose the single winning id. You may also rewrite the winner's text for clarity, and tighten its scope if evidence supports it.",
2792
+ JSON_ONLY
2793
+ ].join(" ");
2794
+ const user = [
2795
+ `Repo: ${JSON.stringify(payload.repo ?? null)}`,
2796
+ `Cluster:`,
2797
+ JSON.stringify(payload.cluster ?? [], null, 2),
2798
+ "",
2799
+ 'Return JSON: {"winner_id": uuid, "winner_text"?: string, "winner_scope"?: "session"|"path"|"repo"|"team", "winner_path_scope"?: string|null, "rationale"?: string}'
2800
+ ].join("\n");
2801
+ return { system, user };
2802
+ }
2803
+ function buildSummarizeSessionPrompt(task) {
2804
+ const payload = task.payload;
2805
+ const system = [
2806
+ "You condense a coding-agent session into a brief durable summary.",
2807
+ "<= 5 short bullet points; no filler.",
2808
+ JSON_ONLY
2809
+ ].join(" ");
2810
+ const user = [
2811
+ `Session: ${payload.session_id ?? "unknown"}`,
2812
+ `Repo: ${JSON.stringify(payload.repo ?? null)}`,
2813
+ `Events: ${JSON.stringify(payload.events ?? [], null, 2).slice(0, 12e3)}`,
2814
+ "",
2815
+ 'Return JSON: {"summary_text": string}'
2816
+ ].join("\n");
2817
+ return { system, user };
2818
+ }
2819
+ function buildSynthesizeRepoPrompt(task) {
2820
+ const payload = task.payload;
2821
+ const system = [
2822
+ "You synthesize a concise repo-level summary from the stable memory set.",
2823
+ "Focus on commands, rules, gotchas, and decisions that repeat across sessions.",
2824
+ JSON_ONLY
2825
+ ].join(" ");
2826
+ const user = [
2827
+ `Repo: ${JSON.stringify(payload.repo ?? null)}`,
2828
+ `Memory set: ${JSON.stringify(payload.memories ?? [], null, 2).slice(0, 12e3)}`,
2829
+ "",
2830
+ 'Return JSON: {"summary_text": string}'
2831
+ ].join("\n");
2832
+ return { system, user };
2833
+ }
2834
+ function formatDispatchReport(report) {
2835
+ const lines = [
2836
+ "# Recall Maintenance Dispatch",
2837
+ `Provider: ${report.provider ?? "(none \u2014 no API key)"}`,
2838
+ `Dry run: ${report.dry_run ? "yes" : "no"}`,
2839
+ `Attempted: ${report.attempted}`,
2840
+ `Applied: ${report.applied}`,
2841
+ `Rejected: ${report.rejected}`,
2842
+ `Released: ${report.released}`
2843
+ ];
2844
+ if (report.outcomes.length > 0) {
2845
+ lines.push("", "## Outcomes");
2846
+ for (const o of report.outcomes) {
2847
+ const cost = o.cost_usd != null ? ` $${o.cost_usd.toFixed(4)}` : "";
2848
+ const tokens = o.prompt_tokens != null ? ` tokens=${(o.prompt_tokens ?? 0) + (o.completion_tokens ?? 0)}` : "";
2849
+ const reason = o.reason ? ` \u2014 ${o.reason}` : "";
2850
+ lines.push(` ${o.task_id.slice(0, 8)} ${o.kind.padEnd(20)} ${o.status.padEnd(10)}${tokens}${cost}${reason}`);
2851
+ }
2852
+ }
2853
+ return lines.join("\n");
2854
+ }
2855
+
2856
+ // src/capture/correction.ts
2857
+ import { randomUUID as randomUUID6 } from "crypto";
2858
+
2859
+ // src/capture/scope.ts
2860
+ import { execSync } from "child_process";
2861
+ import { dirname, extname, basename } from "path";
2862
+ var SCOPE_MARKERS = [
2863
+ {
2864
+ pattern: /\b(in this file|this file only|just this file)\b/i,
2865
+ scope: "path",
2866
+ reason: "explicit file scope marker"
2867
+ },
2868
+ {
2869
+ pattern: /\b(in this directory|in this folder|this dir)\b/i,
2870
+ scope: "path",
2871
+ reason: "explicit directory scope marker"
2872
+ },
2873
+ {
2874
+ pattern: /\b(in this repo|for this repo|repo-wide|across the repo|this project)\b/i,
2875
+ scope: "repo",
2876
+ reason: "explicit repo scope marker"
2877
+ },
2878
+ {
2879
+ pattern: /\b(team-wide|company-wide|org-wide|for the team|for the org|across the team)\b/i,
2880
+ scope: "team",
2881
+ reason: "explicit team/org scope marker"
2882
+ },
2883
+ {
2884
+ pattern: /\b(for me always|always for me|agent-wide|regardless of project|across all my repos|in all my work|for all (?:my )?projects|everywhere|all repos)\b/i,
2885
+ scope: "global",
2886
+ reason: "explicit global/cross-repo scope marker"
2887
+ },
2888
+ {
2889
+ pattern: /\b(just for now|this time|for this task|temporarily)\b/i,
2890
+ scope: "session",
2891
+ reason: "explicit session scope marker"
2892
+ }
2893
+ ];
2894
+ var FRAMEWORK_INDICATORS = [
2895
+ /\b(typescript|javascript|python|rust|go|java|swift|ruby)\b/i,
2896
+ /\b(react|vue|angular|svelte|next\.?js|express|fastify|django|flask|rails)\b/i,
2897
+ /\b(eslint|prettier|biome|ruff|clippy|rubocop)\b/i,
2898
+ /\b(jest|vitest|pytest|cargo test|go test)\b/i,
2899
+ /\b(npm|yarn|pnpm|bun|pip|uv|poetry|cargo|go mod)\b/i
2900
+ ];
2901
+ var FILE_TYPE_SCOPES = {
2902
+ // Config files → repo scope
2903
+ ".json": "repo",
2904
+ ".yaml": "repo",
2905
+ ".yml": "repo",
2906
+ ".toml": "repo",
2907
+ ".ini": "repo",
2908
+ // Source files → path scope
2909
+ ".ts": "path",
2910
+ ".tsx": "path",
2911
+ ".js": "path",
2912
+ ".jsx": "path",
2913
+ ".py": "path",
2914
+ ".rs": "path",
2915
+ ".go": "path",
2916
+ ".swift": "path",
2917
+ ".java": "path",
2918
+ ".rb": "path",
2919
+ // Test files → path scope
2920
+ ".test.ts": "path",
2921
+ ".spec.ts": "path",
2922
+ ".test.js": "path",
2923
+ ".spec.js": "path"
2924
+ };
2925
+ function inferScope(correctionText, contextPath, repoPath, context = {}) {
2926
+ const markerHaystack = `${context.original_text ?? ""} ${correctionText}`;
2927
+ for (const marker of SCOPE_MARKERS) {
2928
+ if (marker.pattern.test(markerHaystack)) {
2929
+ return {
2930
+ scope: marker.scope,
2931
+ path_scope: marker.scope === "path" && contextPath ? inferPathScope(contextPath) : null,
2932
+ confidence_modifier: 0.1,
2933
+ // boost for explicit markers
2934
+ reason: marker.reason
2935
+ };
2936
+ }
2937
+ }
2938
+ for (const indicator of FRAMEWORK_INDICATORS) {
2939
+ if (indicator.test(correctionText)) {
2940
+ return {
2941
+ scope: "repo",
2942
+ path_scope: null,
2943
+ confidence_modifier: 0.05,
2944
+ reason: "language/framework reference implies repo scope"
2945
+ };
2946
+ }
2947
+ }
2948
+ if (contextPath) {
2949
+ const ext = extname(contextPath);
2950
+ const base = basename(contextPath);
2951
+ if (base.includes(".test.") || base.includes(".spec.") || contextPath.includes("__tests__") || contextPath.includes("/test/")) {
2952
+ return {
2953
+ scope: "path",
2954
+ path_scope: inferPathScope(contextPath),
2955
+ confidence_modifier: 0,
2956
+ reason: "test file context \u2192 path scope"
2957
+ };
2958
+ }
2959
+ if (FILE_TYPE_SCOPES[ext] === "repo" || base === "package.json" || base === "tsconfig.json" || base === "Makefile") {
2960
+ return {
2961
+ scope: "repo",
2962
+ path_scope: null,
2963
+ confidence_modifier: 0.05,
2964
+ reason: "config file context \u2192 repo scope"
2965
+ };
2966
+ }
2967
+ if (FILE_TYPE_SCOPES[ext] === "path") {
2968
+ return {
2969
+ scope: "path",
2970
+ path_scope: inferPathScope(contextPath),
2971
+ confidence_modifier: 0,
2972
+ reason: "source file context \u2192 directory scope"
2973
+ };
2974
+ }
2975
+ }
2976
+ const toolScope = inferFromRecentTools(context.recent_tool_calls);
2977
+ if (toolScope) {
2978
+ return toolScope;
2979
+ }
2980
+ const assistantScope = inferFromAssistantTurn(context.prev_assistant_turn);
2981
+ if (assistantScope) {
2982
+ return assistantScope;
2983
+ }
2984
+ if (hasSpecificFileReference(correctionText)) {
2985
+ return {
2986
+ scope: "path",
2987
+ path_scope: extractPathFromText(correctionText),
2988
+ confidence_modifier: 0,
2989
+ reason: "specific file/path reference in correction"
2990
+ };
2991
+ }
2992
+ if (contextPath && repoPath) {
2993
+ const ownerScope = inferFromGitOwnership(contextPath, repoPath);
2994
+ if (ownerScope) return ownerScope;
2995
+ }
2996
+ return {
2997
+ scope: "repo",
2998
+ path_scope: null,
2999
+ confidence_modifier: 0,
3000
+ reason: "default: no specific scope signals detected"
3001
+ };
3002
+ }
3003
+ function inferPathScope(filePath) {
3004
+ const dir = dirname(filePath);
3005
+ return `${dir}/**`;
3006
+ }
3007
+ function hasSpecificFileReference(text) {
3008
+ return /\b[\w-]+\.(ts|js|py|rs|go|swift|java|rb|tsx|jsx|vue|svelte)\b/.test(text) || /\b(src|lib|app|components|utils|test|spec)\//.test(text);
3009
+ }
3010
+ function extractPathFromText(text) {
3011
+ const pathMatch = text.match(
3012
+ /\b((?:src|lib|app|components|utils|test|spec)\/[\w/.-]+)/
3013
+ );
3014
+ if (pathMatch) return `${pathMatch[1]}**`;
3015
+ const dirMatch = text.match(
3016
+ /\b((?:src|lib|app|components|utils|test|spec)\/[\w/-]*)/
3017
+ );
3018
+ if (dirMatch) return `${dirMatch[1]}/**`;
3019
+ return null;
3020
+ }
3021
+ var SOURCE_AWARE_TOOLS = /* @__PURE__ */ new Set([
3022
+ "Read",
3023
+ "Edit",
3024
+ "Write",
3025
+ "MultiEdit",
3026
+ "NotebookEdit",
3027
+ "NotebookRead",
3028
+ "Grep",
3029
+ "Glob"
3030
+ ]);
3031
+ function isPathInsideRepo(path) {
3032
+ if (path.startsWith("/")) return false;
3033
+ if (/^(?:Applications|app|usr|opt|System|Library|private|var|tmp)\//.test(path)) return false;
3034
+ return true;
3035
+ }
3036
+ function inferFromRecentTools(toolCalls) {
3037
+ if (!toolCalls || toolCalls.length === 0) return null;
3038
+ for (const toolCall of toolCalls) {
3039
+ if (toolCall.path && SOURCE_AWARE_TOOLS.has(toolCall.name) && isPathInsideRepo(toolCall.path)) {
3040
+ return {
3041
+ scope: "path",
3042
+ path_scope: inferPathScope(toolCall.path),
3043
+ confidence_modifier: 0.05,
3044
+ reason: `recent tool path context: ${toolCall.path}`
3045
+ };
3046
+ }
3047
+ const inferredPath = extractPathFromText(toolCall.input_summary ?? "");
3048
+ if (inferredPath) {
3049
+ return {
3050
+ scope: "path",
3051
+ path_scope: inferredPath,
3052
+ confidence_modifier: 0.05,
3053
+ reason: "recent tool summary implies path scope"
3054
+ };
3055
+ }
3056
+ const summary = toolCall.input_summary ?? "";
3057
+ if (/\b(pytest|vitest|jest|cargo test|go test)\b/i.test(summary)) {
3058
+ return {
3059
+ scope: "repo",
3060
+ path_scope: null,
3061
+ confidence_modifier: 0.04,
3062
+ reason: "recent test/tool pattern implies repo scope"
3063
+ };
3064
+ }
3065
+ }
3066
+ return null;
3067
+ }
3068
+ function inferFromAssistantTurn(assistantTurn) {
3069
+ if (!assistantTurn) return null;
3070
+ const inferredPath = extractPathFromText(assistantTurn);
3071
+ if (inferredPath) {
3072
+ return {
3073
+ scope: "path",
3074
+ path_scope: inferredPath,
3075
+ confidence_modifier: 0.05,
3076
+ reason: "previous assistant turn referenced a path"
3077
+ };
3078
+ }
3079
+ for (const indicator of FRAMEWORK_INDICATORS) {
3080
+ if (indicator.test(assistantTurn)) {
3081
+ return {
3082
+ scope: "repo",
3083
+ path_scope: null,
3084
+ confidence_modifier: 0.03,
3085
+ reason: "previous assistant turn referenced repo-level tooling/framework"
3086
+ };
3087
+ }
3088
+ }
3089
+ return null;
3090
+ }
3091
+ function inferFromGitOwnership(filePath, repoPath) {
3092
+ try {
3093
+ const codeowners = execSync(
3094
+ `cat .github/CODEOWNERS 2>/dev/null || cat CODEOWNERS 2>/dev/null || echo ""`,
3095
+ { cwd: repoPath, encoding: "utf-8" }
3096
+ ).trim();
3097
+ if (codeowners) {
3098
+ const dir = dirname(filePath);
3099
+ for (const line of codeowners.split("\n")) {
3100
+ if (line.startsWith("#") || !line.trim()) continue;
3101
+ const parts = line.trim().split(/\s+/);
3102
+ if (parts.length >= 2 && filePath.includes(parts[0].replace("*", ""))) {
3103
+ return {
3104
+ scope: "path",
3105
+ path_scope: `${parts[0]}`,
3106
+ confidence_modifier: 0.05,
3107
+ reason: `CODEOWNERS match: ${parts[0]} \u2192 ${parts.slice(1).join(", ")}`
3108
+ };
3109
+ }
3110
+ }
3111
+ }
3112
+ } catch {
3113
+ }
3114
+ return null;
3115
+ }
3116
+
3117
+ // src/maintenance/cleanup.ts
3118
+ import { and as and3, eq as eq9, gte as gte2, inArray as inArray2, sql as sql2 } from "drizzle-orm";
3119
+ import { randomUUID as randomUUID5 } from "crypto";
3120
+
3121
+ // src/contradictions/detector.ts
3122
+ import { eq as eq8, and as and2 } from "drizzle-orm";
3123
+ import { randomUUID as randomUUID4 } from "crypto";
3124
+ function detectContradictions(db, repo) {
3125
+ const mems = queryMemories(db, { repo }).filter(
3126
+ (m) => m.status === "active" || m.status === "candidate"
3127
+ );
3128
+ const found = [];
3129
+ const seen = /* @__PURE__ */ new Set();
3130
+ for (let i = 0; i < mems.length; i++) {
3131
+ for (let j = i + 1; j < mems.length; j++) {
3132
+ const a = mems[i];
3133
+ const b = mems[j];
3134
+ const pairKey = [a.id, b.id].sort().join(":");
3135
+ if (seen.has(pairKey)) continue;
3136
+ const contradiction = checkContradiction(a, b);
3137
+ if (contradiction) {
3138
+ seen.add(pairKey);
3139
+ const existing = db.select().from(contradictions).where(
3140
+ and2(
3141
+ eq8(contradictions.memory_a_id, a.id),
3142
+ eq8(contradictions.memory_b_id, b.id)
3143
+ )
3144
+ ).get();
3145
+ if (!existing) {
3146
+ const id = randomUUID4();
3147
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3148
+ db.insert(contradictions).values({
3149
+ id,
3150
+ memory_a_id: a.id,
3151
+ memory_b_id: b.id,
3152
+ contradiction_type: contradiction.type,
3153
+ severity: contradiction.severity,
3154
+ description: contradiction.description,
3155
+ resolved: false,
3156
+ detected_at: now
3157
+ }).run();
3158
+ recordAudit(db, a.id, "contradiction_detected", "system", contradiction.description);
3159
+ recordAudit(db, b.id, "contradiction_detected", "system", contradiction.description);
3160
+ found.push({
3161
+ id,
3162
+ memory_a_id: a.id,
3163
+ memory_b_id: b.id,
3164
+ contradiction_type: contradiction.type,
3165
+ severity: contradiction.severity,
3166
+ description: contradiction.description,
3167
+ resolved: false,
3168
+ resolution: null,
3169
+ detected_at: now,
3170
+ resolved_at: null
3171
+ });
3172
+ }
3173
+ }
3174
+ }
3175
+ }
3176
+ return found;
3177
+ }
3178
+ function checkContradiction(a, b) {
3179
+ if (!scopesOverlap(a, b)) return null;
3180
+ const negation = checkDirectNegation(a, b);
3181
+ if (negation) return negation;
3182
+ const conflict = checkConflictingRules(a, b);
3183
+ if (conflict) return conflict;
3184
+ if (a.supersedes === b.id || b.supersedes === a.id) {
3185
+ return {
3186
+ type: "superseded",
3187
+ severity: "medium",
3188
+ description: `One memory supersedes the other`
3189
+ };
3190
+ }
3191
+ return null;
3192
+ }
3193
+ var NEGATION_PAIRS = [
3194
+ [/\balways\b/i, /\bnever\b/i],
3195
+ [/\bdo\b/i, /\bdo not\b|don't\b/i],
3196
+ [/\buse\b/i, /\bdo not use\b|don't use\b|never use\b/i],
3197
+ [/\brequired\b/i, /\bforbidden\b|prohibited\b/i],
3198
+ [/\benable\b/i, /\bdisable\b/i]
3199
+ ];
3200
+ function checkDirectNegation(a, b) {
3201
+ for (const [pos, neg] of NEGATION_PAIRS) {
3202
+ const aPos = pos.test(a.text) && !neg.test(a.text);
3203
+ const aNeg = neg.test(a.text) && !pos.test(a.text);
3204
+ const bPos = pos.test(b.text) && !neg.test(b.text);
3205
+ const bNeg = neg.test(b.text) && !pos.test(b.text);
3206
+ if (aPos && bNeg || aNeg && bPos) {
3207
+ const subjectA = extractSubject(a.text);
3208
+ const subjectB = extractSubject(b.text);
3209
+ if (subjectA && subjectB && wordOverlap(subjectA, subjectB) > 0.5) {
3210
+ return {
3211
+ type: "direct_negation",
3212
+ severity: "high",
3213
+ description: `"${a.text}" contradicts "${b.text}"`
3214
+ };
3215
+ }
3216
+ }
3217
+ }
3218
+ return null;
3219
+ }
3220
+ function checkConflictingRules(a, b) {
3221
+ if (a.type !== b.type) return null;
3222
+ if (a.type !== "rule" && a.type !== "command") return null;
3223
+ const useA = a.text.match(/\buse\s+(\S+)/i);
3224
+ const useB = b.text.match(/\buse\s+(\S+)/i);
3225
+ if (useA && useB) {
3226
+ const toolA = useA[1].toLowerCase().replace(/[,.:;]/g, "");
3227
+ const toolB = useB[1].toLowerCase().replace(/[,.:;]/g, "");
3228
+ if (toolA !== toolB) {
3229
+ const contextA = extractContext(a.text);
3230
+ const contextB = extractContext(b.text);
3231
+ if (contextA && contextB && wordOverlap(contextA, contextB) > 0.3) {
3232
+ return {
3233
+ type: "conflicting_rules",
3234
+ severity: "medium",
3235
+ description: `"use ${toolA}" vs "use ${toolB}" in similar context`
3236
+ };
3237
+ }
3238
+ }
3239
+ }
3240
+ const sim = wordOverlap(a.text, b.text);
3241
+ if (sim > 0.6 && sim < 0.95 && a.text !== b.text) {
3242
+ return {
3243
+ type: "scope_overlap",
3244
+ severity: "low",
3245
+ description: `Very similar memories (${(sim * 100).toFixed(0)}% overlap): "${a.text.slice(0, 50)}" vs "${b.text.slice(0, 50)}"`
3246
+ };
3247
+ }
3248
+ return null;
3249
+ }
3250
+ function resolveContradiction(db, contradictionId, keepMemoryId, actor, resolution) {
3251
+ const row = db.select().from(contradictions).where(eq8(contradictions.id, contradictionId)).get();
3252
+ if (!row) return false;
3253
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3254
+ const demoteId = row.memory_a_id === keepMemoryId ? row.memory_b_id : row.memory_a_id;
3255
+ demoteMemory(db, demoteId, `contradiction resolved: keep ${keepMemoryId.slice(0, 8)}`);
3256
+ db.update(contradictions).set({
3257
+ resolved: true,
3258
+ resolution: resolution ?? `Kept ${keepMemoryId.slice(0, 8)}, demoted ${demoteId.slice(0, 8)}`,
3259
+ resolved_at: now
3260
+ }).where(eq8(contradictions.id, contradictionId)).run();
3261
+ recordAudit(db, keepMemoryId, "contradiction_resolved", actor, resolution ?? null);
3262
+ recordAudit(db, demoteId, "contradiction_resolved", actor, `demoted in favor of ${keepMemoryId.slice(0, 8)}`);
3263
+ return true;
3264
+ }
3265
+ function autoResolveContradictions(db, repo) {
3266
+ const unresolved = db.select().from(contradictions).where(eq8(contradictions.resolved, false)).all();
3267
+ let resolved = 0;
3268
+ for (const c of unresolved) {
3269
+ const a = getMemory(db, c.memory_a_id);
3270
+ const b = getMemory(db, c.memory_b_id);
3271
+ if (!a || !b) continue;
3272
+ if (repo && a.repo !== repo && b.repo !== repo) continue;
3273
+ if (c.severity === "low") continue;
3274
+ if (Math.abs(a.confidence - b.confidence) < 0.15) continue;
3275
+ const keepId = a.confidence >= b.confidence ? a.id : b.id;
3276
+ resolveContradiction(db, c.id, keepId, "auto-resolver", "Auto-resolved: higher confidence wins");
3277
+ resolved++;
3278
+ }
3279
+ return resolved;
3280
+ }
3281
+ function listContradictions(db, options = {}) {
3282
+ if (options.resolved !== void 0) {
3283
+ return db.select().from(contradictions).where(eq8(contradictions.resolved, options.resolved)).all();
3284
+ }
3285
+ return db.select().from(contradictions).all();
3286
+ }
3287
+ function scopesOverlap(a, b) {
3288
+ if (a.scope === "global" || b.scope === "global") return true;
3289
+ if (a.scope === "team" || b.scope === "team") return true;
3290
+ if (a.scope === "repo" && b.scope === "repo") {
3291
+ return !a.repo || !b.repo || a.repo === b.repo;
3292
+ }
3293
+ if (a.scope === "path" && b.scope === "path") {
3294
+ if (!a.path_scope || !b.path_scope) return true;
3295
+ return a.path_scope.startsWith(b.path_scope.replace("/**", "")) || b.path_scope.startsWith(a.path_scope.replace("/**", ""));
3296
+ }
3297
+ if (a.scope === "repo" && b.scope === "path" || a.scope === "path" && b.scope === "repo") {
3298
+ return !a.repo || !b.repo || a.repo === b.repo;
3299
+ }
3300
+ return false;
3301
+ }
3302
+ function extractSubject(text) {
3303
+ return text.replace(/\b(always|never|must|do not|don't|use|do|run|call|import)\b/gi, "").trim().toLowerCase();
3304
+ }
3305
+ function extractContext(text) {
3306
+ return text.replace(/\buse\s+\S+/i, "").trim().toLowerCase();
3307
+ }
3308
+ function wordOverlap(a, b) {
3309
+ const wordsA = new Set(a.toLowerCase().split(/\s+/).filter((w) => w.length > 2));
3310
+ const wordsB = new Set(b.toLowerCase().split(/\s+/).filter((w) => w.length > 2));
3311
+ if (wordsA.size === 0 || wordsB.size === 0) return 0;
3312
+ const intersection = [...wordsA].filter((w) => wordsB.has(w));
3313
+ const union = /* @__PURE__ */ new Set([...wordsA, ...wordsB]);
3314
+ return intersection.length / union.size;
3315
+ }
3316
+
3317
+ // src/maintenance/cleanup.ts
3318
+ var SUPPRESS_INJECTION_FLOOR = 50;
3319
+ var GLOBALIZE_REPO_FLOOR = 3;
3320
+ var DEFAULT_ACTOR = "maintenance:cleanup";
3321
+ function runDeterministicCleanup(db, opts = { dryRun: true }) {
3322
+ const runId = randomUUID5();
3323
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
3324
+ const plan = [];
3325
+ if (!opts.only || opts.only === "dedupe_exact_merge") {
3326
+ plan.push(...planDedupeExact(db));
3327
+ }
3328
+ if (!opts.only || opts.only === "reject_fragment_candidate") {
3329
+ plan.push(...planRejectFragments(db));
3330
+ }
3331
+ if (!opts.only || opts.only === "promote_repeat_correction") {
3332
+ plan.push(...planPromoteRepeats(db));
3333
+ }
3334
+ if (!opts.only || opts.only === "suppress_unproductive_command") {
3335
+ plan.push(...planSuppressCommands(db));
3336
+ }
3337
+ if (!opts.only || opts.only === "globalize_cross_repo") {
3338
+ plan.push(...planGlobalizeCrossRepo(db));
3339
+ }
3340
+ const counts = summarize(plan);
3341
+ if (!opts.dryRun) {
3342
+ for (const item of plan) {
3343
+ switch (item.kind) {
3344
+ case "dedupe_exact_merge":
3345
+ applyDedupeExact(db, runId, item);
3346
+ break;
3347
+ case "reject_fragment_candidate":
3348
+ applyRejectFragment(db, runId, item);
3349
+ break;
3350
+ case "promote_repeat_correction":
3351
+ applyPromoteRepeat(db, runId, item);
3352
+ break;
3353
+ case "suppress_unproductive_command":
3354
+ applySuppressCommand(db, runId, item);
3355
+ break;
3356
+ case "globalize_cross_repo":
3357
+ applyGlobalizeCrossRepo(db, runId, item);
3358
+ break;
3359
+ }
3360
+ }
3361
+ }
3362
+ return {
3363
+ run_id: runId,
3364
+ dry_run: opts.dryRun,
3365
+ started_at: startedAt,
3366
+ finished_at: (/* @__PURE__ */ new Date()).toISOString(),
3367
+ counts,
3368
+ plan
3369
+ };
3370
+ }
3371
+ function summarize(plan) {
3372
+ let dedupeClusters = 0;
3373
+ let dedupeLosers = 0;
3374
+ let fragmentRejections = 0;
3375
+ let repeatPromotions = 0;
3376
+ let commandSuppressions = 0;
3377
+ let globalizations = 0;
3378
+ let globalizeLosers = 0;
3379
+ for (const p of plan) {
3380
+ if (p.kind === "dedupe_exact_merge") {
3381
+ dedupeClusters += 1;
3382
+ dedupeLosers += p.loser_ids.length;
3383
+ } else if (p.kind === "reject_fragment_candidate") {
3384
+ fragmentRejections += 1;
3385
+ } else if (p.kind === "promote_repeat_correction") {
3386
+ repeatPromotions += 1;
3387
+ } else if (p.kind === "suppress_unproductive_command") {
3388
+ commandSuppressions += 1;
3389
+ } else {
3390
+ globalizations += 1;
3391
+ globalizeLosers += p.loser_ids.length;
3392
+ }
3393
+ }
3394
+ return {
3395
+ dedupe_clusters: dedupeClusters,
3396
+ dedupe_losers: dedupeLosers,
3397
+ fragment_rejections: fragmentRejections,
3398
+ repeat_promotions: repeatPromotions,
3399
+ command_suppressions: commandSuppressions,
3400
+ globalizations,
3401
+ globalize_losers: globalizeLosers
3402
+ };
3403
+ }
3404
+ function normalizeText(text) {
3405
+ return text.toLowerCase().replace(/\s+/g, " ").replace(/[\s.;:,!?`]+$/g, "").trim();
3406
+ }
3407
+ function scopeKey(row) {
3408
+ return [row.type, row.scope, row.repo ?? "", row.path_scope ?? "", row.norm].join("\0");
3409
+ }
3410
+ function planDedupeExact(db) {
3411
+ const rows = db.select({
3412
+ id: memories.id,
3413
+ type: memories.type,
3414
+ text: memories.text,
3415
+ scope: memories.scope,
3416
+ repo: memories.repo,
3417
+ path_scope: memories.path_scope,
3418
+ status: memories.status,
3419
+ injection_count: memories.injection_count,
3420
+ confidence: memories.confidence,
3421
+ created_at: memories.created_at
3422
+ }).from(memories).where(inArray2(memories.status, ["active", "candidate"])).all();
3423
+ const groups = /* @__PURE__ */ new Map();
3424
+ for (const row of rows) {
3425
+ const norm = normalizeText(row.text);
3426
+ if (!norm) continue;
3427
+ const key = scopeKey({ ...row, norm });
3428
+ const list = groups.get(key) ?? [];
3429
+ list.push(row);
3430
+ groups.set(key, list);
3431
+ }
3432
+ const plans = [];
3433
+ for (const [key, list] of groups) {
3434
+ if (list.length < 2) continue;
3435
+ const sorted = [...list].sort((a, b) => {
3436
+ const statusRank = (s) => s === "active" ? 0 : 1;
3437
+ const dStatus = statusRank(a.status) - statusRank(b.status);
3438
+ if (dStatus !== 0) return dStatus;
3439
+ if (a.injection_count !== b.injection_count) return b.injection_count - a.injection_count;
3440
+ if (a.confidence !== b.confidence) return b.confidence - a.confidence;
3441
+ return a.created_at.localeCompare(b.created_at);
3442
+ });
3443
+ const winner = sorted[0];
3444
+ const losers = sorted.slice(1);
3445
+ const total = list.reduce((acc, r) => acc + r.injection_count, 0);
3446
+ plans.push({
3447
+ kind: "dedupe_exact_merge",
3448
+ winner_id: winner.id,
3449
+ winner_text: winner.text,
3450
+ loser_ids: losers.map((l) => l.id),
3451
+ scope_key: key,
3452
+ total_injection_count: total
3453
+ });
3454
+ }
3455
+ return plans;
3456
+ }
3457
+ function applyDedupeExact(db, runId, plan) {
3458
+ const winner = getMemory(db, plan.winner_id);
3459
+ if (!winner) return;
3460
+ const losers = plan.loser_ids.map((id) => getMemory(db, id)).filter((m) => m != null);
3461
+ if (losers.length === 0) return;
3462
+ const sumCounts = losers.reduce(
3463
+ (acc, l) => ({
3464
+ injection: acc.injection + l.injection_count,
3465
+ override: acc.override + l.override_count,
3466
+ repetition: acc.repetition + l.repetition_count
3467
+ }),
3468
+ { injection: 0, override: 0, repetition: 0 }
3469
+ );
3470
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3471
+ for (const loser of losers) {
3472
+ db.update(feedbackEvents).set({ memory_id: winner.id }).where(eq9(feedbackEvents.memory_id, loser.id)).run();
3473
+ const loserInj = db.select().from(memoryInjections).where(eq9(memoryInjections.memory_id, loser.id)).all();
3474
+ for (const inj of loserInj) {
3475
+ const collision = db.select({ id: memoryInjections.id }).from(memoryInjections).where(and3(eq9(memoryInjections.memory_id, winner.id), eq9(memoryInjections.session_id, inj.session_id))).get();
3476
+ if (collision) {
3477
+ db.delete(memoryInjections).where(eq9(memoryInjections.id, inj.id)).run();
3478
+ } else {
3479
+ db.update(memoryInjections).set({ memory_id: winner.id }).where(eq9(memoryInjections.id, inj.id)).run();
3480
+ }
3481
+ }
3482
+ }
3483
+ db.update(memories).set({
3484
+ injection_count: winner.injection_count + sumCounts.injection,
3485
+ override_count: winner.override_count + sumCounts.override,
3486
+ repetition_count: winner.repetition_count + sumCounts.repetition,
3487
+ updated_at: now
3488
+ }).where(eq9(memories.id, winner.id)).run();
3489
+ for (const loser of losers) {
3490
+ db.update(memories).set({ status: "rejected", supersedes: winner.id, dedupe_key: null, updated_at: now }).where(eq9(memories.id, loser.id)).run();
3491
+ const after = getMemory(db, loser.id);
3492
+ recordAuditWithSnapshot(
3493
+ db,
3494
+ loser.id,
3495
+ "rejected",
3496
+ DEFAULT_ACTOR,
3497
+ `dedupe_exact:merged_into:${winner.id}:run:${runId}`,
3498
+ loser,
3499
+ after ?? null
3500
+ );
3501
+ db.insert(maintenanceCleanupLog).values({
3502
+ id: randomUUID5(),
3503
+ run_id: runId,
3504
+ action: "dedupe_exact_merge",
3505
+ memory_id: loser.id,
3506
+ related_memory_id: winner.id,
3507
+ before_snapshot: loser,
3508
+ after_snapshot: after,
3509
+ details: { scope_key: plan.scope_key, transferred_injection_count: loser.injection_count },
3510
+ reverted: false,
3511
+ reverted_at: null,
3512
+ created_at: now
3513
+ }).run();
3514
+ }
3515
+ }
3516
+ var VERB_HINTS = [
3517
+ "use",
3518
+ "uses",
3519
+ "used",
3520
+ "run",
3521
+ "runs",
3522
+ "ran",
3523
+ "avoid",
3524
+ "prefer",
3525
+ "keep",
3526
+ "set",
3527
+ "add",
3528
+ "remove",
3529
+ "skip",
3530
+ "replace",
3531
+ "fix",
3532
+ "ensure",
3533
+ "require",
3534
+ "make",
3535
+ "build",
3536
+ "test",
3537
+ "deploy",
3538
+ "install",
3539
+ "import",
3540
+ "export",
3541
+ "commit",
3542
+ "push",
3543
+ "call",
3544
+ "wrap",
3545
+ "split",
3546
+ "merge",
3547
+ "store",
3548
+ "load",
3549
+ "save",
3550
+ "ignore",
3551
+ "accept",
3552
+ "reject",
3553
+ "update",
3554
+ "create",
3555
+ "delete",
3556
+ "rename",
3557
+ "move",
3558
+ "copy",
3559
+ "validate",
3560
+ "verify",
3561
+ "check",
3562
+ "follow",
3563
+ "read",
3564
+ "write",
3565
+ "open",
3566
+ "close",
3567
+ "send",
3568
+ "receive",
3569
+ "configure",
3570
+ "enable",
3571
+ "disable"
3572
+ ];
3573
+ var BARE_MODAL_RE = /^\s*(must|never|always|do not|don't|required|prefer|should)\b[^\w]*(stay|do|stop|go|reply|reply\?)?\s*$/i;
3574
+ var TRAILING_QUESTION_RE = /\?\s*$/;
3575
+ var DANGLING_CONNECTOR_RE = /\b(?:and|or|but|with|without|to|from|for|of|as|because|instead|over|the|a|an|on|in|at|by)\s*$/i;
3576
+ var TRAILING_DOUBLE_DOT_RE = /\.{2,}\s*$/;
3577
+ var TRAILING_DASH_RE = /[—–-]\s*$/;
3578
+ var EMBEDDED_QUESTION_RE = /^\s*(?:always|never|must|don't|do not|required|prefer|should)\s+(?:can|could|would|will|do|does|did|is|are|was|were|have|has|should|may|might)\s+(?:you|we|i|they|it|he|she)\b/i;
3579
+ var RULE_FILLER_PREFIX_RE = /^\s*(?:always|never|must|don't|do not|prefer|required)\s+(?:just|now|uh|um|so|like|maybe|kinda|sort\s+of)\b/i;
3580
+ var MAX_RULE_LENGTH = 300;
3581
+ var MIN_RULE_LENGTH = 20;
3582
+ function planRejectFragments(db) {
3583
+ const rows = db.select({
3584
+ id: memories.id,
3585
+ text: memories.text,
3586
+ source: memories.source,
3587
+ status: memories.status
3588
+ }).from(memories).where(and3(eq9(memories.status, "candidate"), eq9(memories.source, "user_correction"))).all();
3589
+ const out = [];
3590
+ for (const row of rows) {
3591
+ const reasons = qualityReasons(row.text);
3592
+ if (reasons.length > 0) {
3593
+ out.push({
3594
+ kind: "reject_fragment_candidate",
3595
+ memory_id: row.id,
3596
+ text: row.text,
3597
+ reasons
3598
+ });
3599
+ }
3600
+ }
3601
+ return out;
3602
+ }
3603
+ function qualityReasons(rawText) {
3604
+ const text = rawText.trim();
3605
+ const reasons = [];
3606
+ if (text.length < MIN_RULE_LENGTH) reasons.push("too_short");
3607
+ if (text.length > MAX_RULE_LENGTH) reasons.push("too_long");
3608
+ if (TRAILING_QUESTION_RE.test(text)) reasons.push("trailing_question");
3609
+ if (BARE_MODAL_RE.test(text)) reasons.push("bare_modal");
3610
+ if (TRAILING_DOUBLE_DOT_RE.test(text)) reasons.push("trailing_double_dot");
3611
+ if (TRAILING_DASH_RE.test(text)) reasons.push("trailing_dash");
3612
+ if (DANGLING_CONNECTOR_RE.test(text)) reasons.push("dangling_connector");
3613
+ if (RULE_FILLER_PREFIX_RE.test(text)) reasons.push("filler_prefix");
3614
+ if (EMBEDDED_QUESTION_RE.test(text)) reasons.push("embedded_question");
3615
+ const words = text.toLowerCase().replace(/[^\w' ]+/g, " ").split(/\s+/).filter(Boolean);
3616
+ const hasVerb = words.some((w) => VERB_HINTS.includes(w));
3617
+ if (!hasVerb) reasons.push("no_verb");
3618
+ return reasons;
3619
+ }
3620
+ function applyRejectFragment(db, runId, plan) {
3621
+ const before = getMemory(db, plan.memory_id);
3622
+ if (!before || before.status !== "candidate") return;
3623
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3624
+ db.update(memories).set({ status: "rejected", dedupe_key: null, updated_at: now }).where(eq9(memories.id, plan.memory_id)).run();
3625
+ const after = getMemory(db, plan.memory_id);
3626
+ recordAuditWithSnapshot(
3627
+ db,
3628
+ plan.memory_id,
3629
+ "rejected",
3630
+ DEFAULT_ACTOR,
3631
+ `cleanup_fragment:${plan.reasons.join(",")}:run:${runId}`,
3632
+ before,
3633
+ after ?? null
3634
+ );
3635
+ db.insert(maintenanceCleanupLog).values({
3636
+ id: randomUUID5(),
3637
+ run_id: runId,
3638
+ action: "reject_fragment_candidate",
3639
+ memory_id: plan.memory_id,
3640
+ related_memory_id: null,
3641
+ before_snapshot: before,
3642
+ after_snapshot: after,
3643
+ details: { reasons: plan.reasons },
3644
+ reverted: false,
3645
+ reverted_at: null,
3646
+ created_at: now
3647
+ }).run();
3648
+ }
3649
+ function planPromoteRepeats(db) {
3650
+ const rows = db.select({
3651
+ id: memories.id,
3652
+ text: memories.text,
3653
+ repetition_count: memories.repetition_count,
3654
+ source: memories.source,
3655
+ status: memories.status
3656
+ }).from(memories).where(and3(eq9(memories.status, "candidate"), eq9(memories.source, "user_correction"))).all();
3657
+ const out = [];
3658
+ for (const row of rows) {
3659
+ const text = row.text.trim();
3660
+ if (qualityReasons(text).length > 0) continue;
3661
+ if (isDestructiveRisky(text)) continue;
3662
+ if (row.repetition_count >= 2) {
3663
+ out.push({ kind: "promote_repeat_correction", memory_id: row.id, text, matched_pattern: "repetition" });
3664
+ }
3665
+ }
3666
+ return out;
3667
+ }
3668
+ function applyPromoteRepeat(db, runId, plan) {
3669
+ const before = getMemory(db, plan.memory_id);
3670
+ if (!before || before.status !== "candidate") return;
3671
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3672
+ db.update(memories).set({ status: "active", confidence: Math.max(before.confidence, 0.7), updated_at: now, last_validated_at: now }).where(eq9(memories.id, plan.memory_id)).run();
3673
+ const after = getMemory(db, plan.memory_id);
3674
+ recordAuditWithSnapshot(
3675
+ db,
3676
+ plan.memory_id,
3677
+ "promoted",
3678
+ DEFAULT_ACTOR,
3679
+ `cleanup_promote:${plan.matched_pattern}:run:${runId}`,
3680
+ before,
3681
+ after ?? null
3682
+ );
3683
+ db.insert(maintenanceCleanupLog).values({
3684
+ id: randomUUID5(),
3685
+ run_id: runId,
3686
+ action: "promote_repeat_correction",
3687
+ memory_id: plan.memory_id,
3688
+ related_memory_id: null,
3689
+ before_snapshot: before,
3690
+ after_snapshot: after,
3691
+ details: { matched_pattern: plan.matched_pattern },
3692
+ reverted: false,
3693
+ reverted_at: null,
3694
+ created_at: now
3695
+ }).run();
3696
+ }
3697
+ function planSuppressCommands(db) {
3698
+ const candidates = db.select({
3699
+ id: memories.id,
3700
+ text: memories.text,
3701
+ injection_count: memories.injection_count
3702
+ }).from(memories).where(and3(
3703
+ eq9(memories.status, "active"),
3704
+ eq9(memories.type, "command"),
3705
+ eq9(memories.auto_inject, true),
3706
+ gte2(memories.injection_count, SUPPRESS_INJECTION_FLOOR)
3707
+ )).all();
3708
+ const out = [];
3709
+ for (const row of candidates) {
3710
+ const followedRow = db.select({ n: sql2`count(*)` }).from(feedbackEvents).where(and3(
3711
+ eq9(feedbackEvents.memory_id, row.id),
3712
+ eq9(feedbackEvents.outcome, "followed")
3713
+ )).get();
3714
+ const followed = followedRow?.n ?? 0;
3715
+ if (followed > 0) continue;
3716
+ out.push({
3717
+ kind: "suppress_unproductive_command",
3718
+ memory_id: row.id,
3719
+ text: row.text,
3720
+ injection_count: row.injection_count,
3721
+ followed_count: followed
3722
+ });
3723
+ }
3724
+ return out;
3725
+ }
3726
+ function applySuppressCommand(db, runId, plan) {
3727
+ const before = getMemory(db, plan.memory_id);
3728
+ if (!before || !before.auto_inject) return;
3729
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3730
+ db.update(memories).set({ auto_inject: false, updated_at: now }).where(eq9(memories.id, plan.memory_id)).run();
3731
+ const after = getMemory(db, plan.memory_id);
3732
+ recordAuditWithSnapshot(
3733
+ db,
3734
+ plan.memory_id,
3735
+ "demoted",
3736
+ DEFAULT_ACTOR,
3737
+ `cleanup_suppress_command:inj=${plan.injection_count},followed=0:run:${runId}`,
3738
+ before,
3739
+ after ?? null
3740
+ );
3741
+ db.insert(maintenanceCleanupLog).values({
3742
+ id: randomUUID5(),
3743
+ run_id: runId,
3744
+ action: "suppress_unproductive_command",
3745
+ memory_id: plan.memory_id,
3746
+ related_memory_id: null,
3747
+ before_snapshot: before,
3748
+ after_snapshot: after,
3749
+ details: {
3750
+ injection_count: plan.injection_count,
3751
+ followed_count: plan.followed_count
3752
+ },
3753
+ reverted: false,
3754
+ reverted_at: null,
3755
+ created_at: now
3756
+ }).run();
3757
+ }
3758
+ function planGlobalizeCrossRepo(db) {
3759
+ const rows = db.select({
3760
+ id: memories.id,
3761
+ type: memories.type,
3762
+ text: memories.text,
3763
+ scope: memories.scope,
3764
+ repo: memories.repo,
3765
+ injection_count: memories.injection_count,
3766
+ confidence: memories.confidence,
3767
+ created_at: memories.created_at
3768
+ }).from(memories).where(and3(eq9(memories.status, "active"), inArray2(memories.type, ["command", "rule"]))).all();
3769
+ const groups = /* @__PURE__ */ new Map();
3770
+ for (const row of rows) {
3771
+ if (row.scope === "global") continue;
3772
+ if (!row.repo) continue;
3773
+ const norm = normalizeText(row.text);
3774
+ if (!norm) continue;
3775
+ const key = `${row.type}::${norm}`;
3776
+ const list = groups.get(key) ?? [];
3777
+ list.push(row);
3778
+ groups.set(key, list);
3779
+ }
3780
+ const allActive = queryMemories(db, { status: "active" }).filter((m) => m.type === "rule" || m.type === "command");
3781
+ const plans = [];
3782
+ for (const list of groups.values()) {
3783
+ const repos = new Set(list.map((r) => r.repo));
3784
+ if (repos.size < GLOBALIZE_REPO_FLOOR) continue;
3785
+ const sorted = [...list].sort((a, b) => {
3786
+ if (a.injection_count !== b.injection_count) return b.injection_count - a.injection_count;
3787
+ if (a.confidence !== b.confidence) return b.confidence - a.confidence;
3788
+ return a.created_at.localeCompare(b.created_at);
3789
+ });
3790
+ const winner = sorted[0];
3791
+ const winnerMemory = getMemory(db, winner.id);
3792
+ if (!winnerMemory) continue;
3793
+ const clusterIds = new Set(list.map((r) => r.id));
3794
+ const conflict = allActive.find((other) => {
3795
+ if (clusterIds.has(other.id)) return false;
3796
+ if (!other.repo || repos.has(other.repo)) return false;
3797
+ const winnerAsGlobal = { ...winnerMemory, scope: "global" };
3798
+ return checkContradiction(winnerAsGlobal, other) != null;
3799
+ });
3800
+ if (conflict) continue;
3801
+ const losers = sorted.slice(1);
3802
+ const total = list.reduce((acc, r) => acc + r.injection_count, 0);
3803
+ plans.push({
3804
+ kind: "globalize_cross_repo",
3805
+ winner_id: winner.id,
3806
+ winner_text: winner.text,
3807
+ loser_ids: losers.map((l) => l.id),
3808
+ repos: [...repos],
3809
+ total_injection_count: total
3810
+ });
3811
+ }
3812
+ return plans;
3813
+ }
3814
+ function applyGlobalizeCrossRepo(db, runId, plan) {
3815
+ const winner = getMemory(db, plan.winner_id);
3816
+ if (!winner) return;
3817
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3818
+ const globalDedupeKey = memoryDedupeKey({ ...winner, scope: "global", repo: null });
3819
+ const globalDedupeCollision = db.select({ id: memories.id }).from(memories).where(eq9(memories.dedupe_key, globalDedupeKey)).get();
3820
+ db.update(memories).set({
3821
+ scope: "global",
3822
+ repo: null,
3823
+ dedupe_key: globalDedupeCollision && globalDedupeCollision.id !== winner.id ? null : globalDedupeKey,
3824
+ updated_at: now
3825
+ }).where(eq9(memories.id, winner.id)).run();
3826
+ const after = getMemory(db, winner.id);
3827
+ recordAuditWithSnapshot(
3828
+ db,
3829
+ winner.id,
3830
+ "edited",
3831
+ DEFAULT_ACTOR,
3832
+ `cleanup_globalize:winner:run:${runId}`,
3833
+ winner,
3834
+ after ?? null
3835
+ );
3836
+ db.insert(maintenanceCleanupLog).values({
3837
+ id: randomUUID5(),
3838
+ run_id: runId,
3839
+ action: "globalize_cross_repo",
3840
+ memory_id: winner.id,
3841
+ related_memory_id: null,
3842
+ before_snapshot: winner,
3843
+ after_snapshot: after,
3844
+ details: { role: "winner", repos: plan.repos },
3845
+ reverted: false,
3846
+ reverted_at: null,
3847
+ created_at: now
3848
+ }).run();
3849
+ for (const loserId of plan.loser_ids) {
3850
+ const loser = getMemory(db, loserId);
3851
+ if (!loser || loser.status === "rejected") continue;
3852
+ db.update(memories).set({ status: "rejected", supersedes: winner.id, dedupe_key: null, updated_at: now }).where(eq9(memories.id, loserId)).run();
3853
+ const afterLoser = getMemory(db, loserId);
3854
+ recordAuditWithSnapshot(
3855
+ db,
3856
+ loserId,
3857
+ "rejected",
3858
+ DEFAULT_ACTOR,
3859
+ `cleanup_globalize:loser:winner=${winner.id}:run:${runId}`,
3860
+ loser,
3861
+ afterLoser ?? null
3862
+ );
3863
+ db.insert(maintenanceCleanupLog).values({
3864
+ id: randomUUID5(),
3865
+ run_id: runId,
3866
+ action: "globalize_cross_repo",
3867
+ memory_id: loserId,
3868
+ related_memory_id: winner.id,
3869
+ before_snapshot: loser,
3870
+ after_snapshot: afterLoser,
3871
+ details: { role: "loser", repo: loser.repo },
3872
+ reverted: false,
3873
+ reverted_at: null,
3874
+ created_at: now
3875
+ }).run();
3876
+ }
3877
+ }
3878
+ function revertCleanupRun(db, runId) {
3879
+ const rows = db.select().from(maintenanceCleanupLog).where(eq9(maintenanceCleanupLog.run_id, runId)).all();
3880
+ if (rows.length === 0) {
3881
+ return { run_id: runId, reverted: 0, skipped: 0, reasons: { not_found: 1 } };
3882
+ }
3883
+ const reasons = {};
3884
+ let reverted = 0;
3885
+ let skipped = 0;
3886
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3887
+ for (const row of rows) {
3888
+ if (row.reverted) {
3889
+ skipped += 1;
3890
+ reasons.already_reverted = (reasons.already_reverted ?? 0) + 1;
3891
+ continue;
3892
+ }
3893
+ const before = row.before_snapshot;
3894
+ if (!before || !before.status) {
3895
+ skipped += 1;
3896
+ reasons.no_snapshot = (reasons.no_snapshot ?? 0) + 1;
3897
+ continue;
3898
+ }
3899
+ const current = getMemory(db, row.memory_id);
3900
+ if (!current) {
3901
+ skipped += 1;
3902
+ reasons.memory_missing = (reasons.memory_missing ?? 0) + 1;
3903
+ continue;
3904
+ }
3905
+ db.update(memories).set({
3906
+ status: before.status,
3907
+ text: before.text ?? current.text,
3908
+ scope: before.scope ?? current.scope,
3909
+ path_scope: before.path_scope ?? current.path_scope,
3910
+ repo: before.repo !== void 0 ? before.repo : current.repo,
3911
+ confidence: before.confidence ?? current.confidence,
3912
+ injection_count: before.injection_count ?? current.injection_count,
3913
+ override_count: before.override_count ?? current.override_count,
3914
+ repetition_count: before.repetition_count ?? current.repetition_count,
3915
+ supersedes: before.supersedes ?? null,
3916
+ auto_inject: before.auto_inject ?? current.auto_inject,
3917
+ updated_at: now
3918
+ }).where(eq9(memories.id, row.memory_id)).run();
3919
+ const after = getMemory(db, row.memory_id);
3920
+ recordAuditWithSnapshot(
3921
+ db,
3922
+ row.memory_id,
3923
+ "rolled_back",
3924
+ DEFAULT_ACTOR,
3925
+ `cleanup_revert:run:${runId}:log:${row.id}`,
3926
+ current,
3927
+ after ?? null
3928
+ );
3929
+ db.update(maintenanceCleanupLog).set({ reverted: true, reverted_at: now }).where(eq9(maintenanceCleanupLog.id, row.id)).run();
3930
+ reverted += 1;
3931
+ }
3932
+ return { run_id: runId, reverted, skipped, reasons };
3933
+ }
3934
+ function listCleanupRuns(db, limit = 10) {
3935
+ const rows = db.select().from(maintenanceCleanupLog).all();
3936
+ const byRun = /* @__PURE__ */ new Map();
3937
+ for (const row of rows) {
3938
+ let entry = byRun.get(row.run_id);
3939
+ if (!entry) {
3940
+ entry = {
3941
+ run_id: row.run_id,
3942
+ started_at: row.created_at,
3943
+ finished_at: row.created_at,
3944
+ total: 0,
3945
+ by_action: {},
3946
+ reverted: 0
3947
+ };
3948
+ byRun.set(row.run_id, entry);
3949
+ }
3950
+ entry.total += 1;
3951
+ entry.by_action[row.action] = (entry.by_action[row.action] ?? 0) + 1;
3952
+ if (row.reverted) entry.reverted += 1;
3953
+ if (row.created_at < entry.started_at) entry.started_at = row.created_at;
3954
+ if (row.created_at > entry.finished_at) entry.finished_at = row.created_at;
3955
+ }
3956
+ return [...byRun.values()].sort((a, b) => b.finished_at.localeCompare(a.finished_at)).slice(0, limit);
3957
+ }
3958
+ function formatCleanupReport(report) {
3959
+ const lines = [];
3960
+ lines.push(`Cleanup ${report.dry_run ? "DRY-RUN" : "APPLY"} run=${report.run_id.slice(0, 8)}`);
3961
+ lines.push(` dedupe_clusters: ${report.counts.dedupe_clusters}`);
3962
+ lines.push(` dedupe_losers: ${report.counts.dedupe_losers}`);
3963
+ lines.push(` fragment_rejections: ${report.counts.fragment_rejections}`);
3964
+ lines.push(` repeat_promotions: ${report.counts.repeat_promotions}`);
3965
+ lines.push(` command_suppressions: ${report.counts.command_suppressions}`);
3966
+ lines.push(` globalizations: ${report.counts.globalizations} (losers=${report.counts.globalize_losers})`);
3967
+ if (report.plan.length === 0) {
3968
+ lines.push(" (no actions)");
3969
+ return lines.join("\n");
3970
+ }
3971
+ lines.push("");
3972
+ for (const item of report.plan) {
3973
+ if (item.kind === "dedupe_exact_merge") {
3974
+ lines.push(` merge: keep ${item.winner_id.slice(0, 8)} drop ${item.loser_ids.length} inj=${item.total_injection_count}`);
3975
+ lines.push(` "${truncate(item.winner_text, 80)}"`);
3976
+ } else if (item.kind === "reject_fragment_candidate") {
3977
+ lines.push(` reject: ${item.memory_id.slice(0, 8)} reasons=${item.reasons.join(",")}`);
3978
+ lines.push(` "${truncate(item.text, 80)}"`);
3979
+ } else if (item.kind === "promote_repeat_correction") {
3980
+ lines.push(` promote: ${item.memory_id.slice(0, 8)} via=${item.matched_pattern}`);
3981
+ lines.push(` "${truncate(item.text, 80)}"`);
3982
+ } else if (item.kind === "suppress_unproductive_command") {
3983
+ lines.push(` suppress: ${item.memory_id.slice(0, 8)} inj=${item.injection_count} followed=${item.followed_count}`);
3984
+ lines.push(` "${truncate(item.text, 80)}"`);
3985
+ } else {
3986
+ lines.push(` globalize: keep ${item.winner_id.slice(0, 8)} drop ${item.loser_ids.length} repos=[${item.repos.join(",")}]`);
3987
+ lines.push(` "${truncate(item.winner_text, 80)}"`);
3988
+ }
3989
+ }
3990
+ return lines.join("\n");
3991
+ }
3992
+ function truncate(s, n) {
3993
+ if (s.length <= n) return s;
3994
+ return s.slice(0, n - 1) + "\u2026";
3995
+ }
3996
+
3997
+ // src/capture/correction.ts
3998
+ var NEGATION_REPLACEMENT = /\b(?:not|don't|do not|never|stop)\s+(?:use|do|run|call|import)\s+(.+?)[\s,;.]+(?:use|do|run|call|import|instead)\s+(.+)/i;
3999
+ var EXPLICIT_RULE = /\b(always|never|must|required|forbidden|don't ever)\b\s+(.+)/i;
4000
+ var WHEN_DO_RULE = /\b(?:whenever|each time|every time|when(?:ever)?)\s+(?:i|you|we)\s+(say|use|ask|mention|do|run)\s+(.+?)[,.]?\s+(?:we|you|i|please|always|just)?\s*(do|run|use|please|add|make|update|commit|push|backup|back up|sync|verify|check|ensure)\s+(.+)/i;
4001
+ var REVIEW_FEEDBACK = /\b(?:review|reviewer|PR feedback|code review)\s+(?:said|says|asked|wants|requires|flagged)\s+(.+)/i;
4002
+ var SOFT_PREFERENCE = /\b(?:we|I|the team|this repo)\s+(?:prefer|usually use|tend to use|lean on|default to|use)\s+(.+?)(?:\s+(?:instead of|not|over)\s+(.+))?$/i;
4003
+ var SOFT_DECISION = /\b(?:let's|lets|let us|we should|we'll|we will|we can|use)\s+(?:use|keep|follow|stick with|go with)\s+(.+?)(?:\s+(?:instead of|over)\s+(.+))?(?:[.!]|$)/i;
4004
+ var CONFIG_BACKED_DECISION = /\b(?:editorconfig|prettier|eslint|tsconfig|package\.json|ci|workflow|this repo)\b.*\b(?:says|uses|wants|defaults to|is configured for)\s+(.+)/i;
4005
+ var QUESTION_ONLY = /^\s*(?:should|could|would|can|do)\b.*\?\s*$/i;
4006
+ var DESCRIPTIVE_MODAL_RE = /\b(?:i|you|we|they|those|that|which|who)(?:\s+\w+){0,2}\s+(?:always|never|must|don't|do not|prefer|required|forbidden)\b/i;
4007
+ var DESTRUCTIVE_VERB_RE = /\b(?:remove|delete|drop|wipe|clear|purge|erase|nuke|truncate|reset|destroy)\b/i;
4008
+ var HIGH_RISK_TARGET_RE = /\b(?:plugin|plugins|setting|settings|config|configs|configuration|file|files|folder|folders|directory|directories|memor(?:y|ies)|database|db|repo|repos|repository|branch|branches|commit|commits|history|backup|backups|secret|secrets|credential|credentials|key|keys|token|tokens)\b/i;
4009
+ function isDestructiveRisky(text) {
4010
+ return DESTRUCTIVE_VERB_RE.test(text) && HIGH_RISK_TARGET_RE.test(text);
4011
+ }
4012
+ var TRIGGER_TEMPLATE_RE = /^\s*when(?:ever)?\s+(?:the\s+)?user\s+(?:says|asks|writes|types|mentions|uses|requests)\b/i;
4013
+ function isTriggerTemplateRule(text) {
4014
+ return TRIGGER_TEMPLATE_RE.test(text);
4015
+ }
4016
+ function isHighRiskRule(text) {
4017
+ return isDestructiveRisky(text) || isTriggerTemplateRule(text);
4018
+ }
4019
+ var NON_LETTER = "(?<![\\p{L}])(?:";
4020
+ var NON_LETTER_END = ")(?![\\p{L}])";
4021
+ var PROMPT_SCREEN_RE = new RegExp(
4022
+ [
4023
+ // English imperatives + save verbs (ASCII — `\b` works)
4024
+ "\\b(?:always|never|don't|do\\s*not|must|should|prefer|avoid|remember|memorize|note|save\\s+this|keep\\s+in\\s+mind|by\\s+default|use\\s+only|forbid|please\\s+(?:always|never))\\b",
4025
+ // Romance languages (handle diacritics with Unicode boundaries)
4026
+ `${NON_LETTER}siempre|nunca|jam\xE1s|no\\s+uses|prefiere|recuerda${NON_LETTER_END}`,
4027
+ `${NON_LETTER}toujours|jamais|n'utilise(?:z)?\\s+pas|pr\xE9f\xE8re|rappel${NON_LETTER_END}`,
4028
+ `${NON_LETTER}immer|nie(?:mals)?|nicht\\s+verwenden|bevorzuge|merk\\s*dir${NON_LETTER_END}`,
4029
+ `${NON_LETTER}sempre|mai|non\\s+usare|preferisci|ricorda${NON_LETTER_END}`,
4030
+ `${NON_LETTER}n\xE3o\\s+use|prefira|lembre${NON_LETTER_END}`,
4031
+ // Russian (Cyrillic)
4032
+ `${NON_LETTER}\u0432\u0441\u0435\u0433\u0434\u0430|\u043D\u0438\u043A\u043E\u0433\u0434\u0430|\u043D\u0435\\s+\u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0439|\u043F\u0440\u0435\u0434\u043F\u043E\u0447\u0442\u0438|\u0437\u0430\u043F\u043E\u043C\u043D\u0438${NON_LETTER_END}`,
4033
+ // CJK — no word boundaries needed
4034
+ "(?:\u603B\u662F|\u4ECE\u4E0D|\u4E0D\u8981\u4F7F\u7528|\u504F\u597D|\u8BB0\u4F4F|\u5E38\u306B|\u6C7A\u3057\u3066|\u4F7F\u308F\u306A\u3044|\u899A\u3048\u3066)",
4035
+ // Albanian (Edi's native) — has diacritics
4036
+ `${NON_LETTER}gjithmon\xEB|asnj\xEBher\xEB|kurr\xEB|mos\\s+p\xEBrdor|mbaj\\s+mend${NON_LETTER_END}`,
4037
+ // Turkish
4038
+ `${NON_LETTER}her\\s*zaman|asla|kullanma|tercih\\s+et|hat\u0131rla${NON_LETTER_END}`
4039
+ ].join("|"),
4040
+ "iu"
4041
+ );
4042
+ function wakeDispatcherBestEffort() {
4043
+ const port = parseInt(process.env.RECALL_DAEMON_PORT ?? "47649", 10);
4044
+ fetch(`http://127.0.0.1:${port}/dispatch/wake`, {
4045
+ method: "POST",
4046
+ headers: { "Content-Type": "application/json" },
4047
+ body: "{}",
4048
+ signal: AbortSignal.timeout(250)
4049
+ }).catch(() => {
4050
+ });
4051
+ }
4052
+ function isPromptWorthLLM(text) {
4053
+ const trimmed = text.trim();
4054
+ if (trimmed.length < 8) return false;
4055
+ if (/^```/.test(trimmed)) return false;
4056
+ if (trimmed.length > 800) return true;
4057
+ return PROMPT_SCREEN_RE.test(trimmed);
4058
+ }
4059
+ function detectCorrections(text) {
4060
+ const normalizedText = text.trim();
4061
+ if (QUESTION_ONLY.test(normalizedText)) return [];
4062
+ if (looksLikePastedTranscript(normalizedText)) return [];
4063
+ const matches = [];
4064
+ const segments = correctionCandidateSegments(normalizedText);
4065
+ for (const segment of segments) {
4066
+ const descriptive = DESCRIPTIVE_MODAL_RE.exec(segment);
4067
+ if (descriptive && descriptive.index > 0) continue;
4068
+ const whenDo = segment.match(WHEN_DO_RULE);
4069
+ if (whenDo) {
4070
+ const trigger = stripTrailingPunctuation(whenDo[2]);
4071
+ const action = stripTrailingPunctuation(`${whenDo[3]} ${whenDo[4]}`);
4072
+ matches.push({
4073
+ type: "rule",
4074
+ text: `When user ${whenDo[1].toLowerCase()}s "${trigger}", ${action}.`,
4075
+ confidence: 0.5,
4076
+ original: segment
4077
+ });
4078
+ continue;
4079
+ }
4080
+ const negMatch = segment.match(NEGATION_REPLACEMENT);
4081
+ if (negMatch) {
4082
+ matches.push({
4083
+ type: "rule",
4084
+ text: `Do not use ${negMatch[1].trim()}. Use ${negMatch[2].trim()} instead.`,
4085
+ confidence: 0.45,
4086
+ original: segment
4087
+ });
4088
+ continue;
4089
+ }
4090
+ const reviewMatch = segment.match(REVIEW_FEEDBACK);
4091
+ if (reviewMatch) {
4092
+ matches.push({
4093
+ type: "review_pattern",
4094
+ text: reviewMatch[1].trim(),
4095
+ confidence: 0.55
4096
+ // stronger — review feedback
4097
+ });
4098
+ continue;
4099
+ }
4100
+ const ruleMatch = segment.match(EXPLICIT_RULE);
4101
+ if (ruleMatch) {
4102
+ matches.push({
4103
+ type: "rule",
4104
+ text: `${ruleMatch[1]} ${ruleMatch[2].trim()}`,
4105
+ confidence: 0.5
4106
+ });
4107
+ continue;
4108
+ }
4109
+ const decisionMatch = segment.match(SOFT_DECISION);
4110
+ if (decisionMatch && isDurableDecision(segment, decisionMatch[1], decisionMatch[2])) {
4111
+ const decision = decisionMatch[2] ? `Prefer ${decisionMatch[1].trim()} over ${decisionMatch[2].trim()}` : `Use ${stripTrailingPunctuation(decisionMatch[1])}`;
4112
+ matches.push({
4113
+ type: "decision",
4114
+ text: ensureSentence(decision),
4115
+ confidence: 0.38
4116
+ });
4117
+ continue;
4118
+ }
4119
+ const prefMatch = segment.match(SOFT_PREFERENCE);
4120
+ if (prefMatch && isDurableDecision(segment, prefMatch[1], prefMatch[2])) {
4121
+ const pref = prefMatch[2] ? `Prefer ${prefMatch[1].trim()} over ${prefMatch[2].trim()}` : `Prefer ${stripTrailingPunctuation(prefMatch[1])}`;
4122
+ matches.push({
4123
+ type: "decision",
4124
+ text: ensureSentence(pref),
4125
+ confidence: 0.36
4126
+ });
4127
+ continue;
4128
+ }
4129
+ const configMatch = segment.match(CONFIG_BACKED_DECISION);
4130
+ if (configMatch) {
4131
+ matches.push({
4132
+ type: "decision",
4133
+ text: ensureSentence(`Follow configured repo convention: ${stripTrailingPunctuation(configMatch[1])}`),
4134
+ confidence: 0.42
4135
+ });
4136
+ }
4137
+ }
4138
+ return matches;
4139
+ }
4140
+ function stripTrailingPunctuation(text) {
4141
+ return text.trim().replace(/[.?!,:;]+$/, "");
4142
+ }
4143
+ function ensureSentence(text) {
4144
+ const cleaned = stripTrailingPunctuation(text);
4145
+ return cleaned.endsWith(".") ? cleaned : `${cleaned}.`;
4146
+ }
4147
+ var TRANSCRIPT_MARKERS = [
4148
+ "\u203B recap:",
4149
+ "\u273B",
4150
+ "\u23FA",
4151
+ "\u23BF",
4152
+ "\u276F",
4153
+ "Bash(",
4154
+ "Hook activity",
4155
+ "Top reused memories",
4156
+ "RECENT INJECTIONS",
4157
+ "BREAKDOWN BY TYPE",
4158
+ "sqlite3"
4159
+ ];
4160
+ var DURABLE_DECISION_HINT = /\b(repo|repository|project|default|defaults|convention(?:al|s)?|configured|config|editorconfig|prettier|eslint|tsconfig|package\.json|ci|workflow|style|pattern|architecture|runtime|database|sqlite|pnpm|yarn|npm|uv|pytest|vitest)\b/i;
4161
+ var TRANSCRIPT_LINE_RE = /^(?:[⏺⎿❯✻※]|(?:Bash|Edit|Write|Read|Grep|Glob|Task|TodoWrite)\(|\s*(?:│|├|┌|└|─)|\s*…|\s*={3,})/u;
4162
+ function looksLikePastedTranscript(text) {
4163
+ if (text.length < 1200) return false;
4164
+ const markerCount = TRANSCRIPT_MARKERS.reduce(
4165
+ (total, marker) => total + (text.includes(marker) ? 1 : 0),
4166
+ 0
4167
+ );
4168
+ return markerCount >= 2;
4169
+ }
4170
+ function correctionCandidateSegments(text) {
4171
+ const lines = text.split(/\r?\n/);
4172
+ const singleLine = lines.length === 1;
4173
+ const segments = singleLine ? [text] : lines.map((line) => line.trim()).filter(Boolean);
4174
+ return segments.map(stripListPrefix).filter((line) => line.length >= 8 && line.length <= 500).filter((line) => !TRANSCRIPT_LINE_RE.test(line)).filter((line) => !line.startsWith("```"));
4175
+ }
4176
+ function stripListPrefix(text) {
4177
+ return text.replace(/^\s*(?:[-*]|\d+[.)])\s+/, "").trim();
4178
+ }
4179
+ function isDurableDecision(segment, first, second) {
4180
+ const haystack = `${segment} ${first} ${second ?? ""}`;
4181
+ if (/\b(?:instead of|over)\b/i.test(haystack)) return true;
4182
+ return DURABLE_DECISION_HINT.test(haystack);
4183
+ }
4184
+ async function processCorrection(db, text, ctx) {
4185
+ if (process.env.RECALL_LLM_CAPTURE_DISABLED !== "true" && hasAnyLlmProvider() && isPromptWorthLLM(text)) {
4186
+ const promptId = `prompt:${ctx.sessionId}:${Date.now()}:${randomUUID6().slice(0, 8)}`;
4187
+ const taskId = enqueueExtractRulesFromPrompt(db, {
4188
+ prompt_id: promptId,
4189
+ raw_prompt: text,
4190
+ repo: ctx.repo ?? null,
4191
+ path: ctx.path ?? null,
4192
+ agent: ctx.agent ?? null,
4193
+ session_id: ctx.sessionId,
4194
+ prev_assistant_turn: ctx.prev_assistant_turn ?? null,
4195
+ recent_tool_calls: ctx.recent_tool_calls ?? null
4196
+ });
4197
+ wakeDispatcherBestEffort();
4198
+ return taskId ? [] : [];
4199
+ }
4200
+ const corrections = detectCorrections(text);
4201
+ if (corrections.length === 0) return [];
4202
+ const profile = getRepoQualityProfile(db, ctx.repo);
4203
+ const ids = [];
4204
+ const captureContext = buildCaptureContext(ctx);
4205
+ for (const correction of corrections) {
4206
+ if (correction.type !== "review_pattern") {
4207
+ const reasons = qualityReasons(correction.text);
4208
+ if (reasons.length > 0) continue;
4209
+ }
4210
+ if (await isSimilarToRejectedFragmentSemantic(db, correction.text)) continue;
4211
+ const evidence = correction.type === "review_pattern" ? {
4212
+ type: "review_feedback",
4213
+ reported_by_user: true,
4214
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4215
+ context: text
4216
+ } : {
4217
+ type: "session_correction",
4218
+ session: ctx.sessionId,
4219
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4220
+ context: text
4221
+ };
4222
+ const duplicate = await findDuplicateMemory(
4223
+ db,
4224
+ ctx.repo,
4225
+ correction.type,
4226
+ correction.text,
4227
+ profile.dedup_similarity_threshold
4228
+ );
4229
+ if (duplicate) {
4230
+ const before = getMemory(db, duplicate.id);
4231
+ appendEvidence(db, duplicate.id, evidence);
4232
+ if (captureContext) {
4233
+ updateMemoryCaptureContext(db, duplicate.id, captureContext);
4234
+ }
4235
+ if (before && !before.evidence.some((entry) => entry.type === "session_correction" && entry.session === ctx.sessionId)) {
4236
+ incrementMemoryRepetition(db, duplicate.id);
4237
+ }
4238
+ const updated = getMemory(db, duplicate.id);
4239
+ if (updated && updated.status !== "active" && !isHighRiskRule(updated.text) && countDistinctCorrectionSessions(updated) >= profile.repeat_sessions_required) {
4240
+ promoteMemory(db, duplicate.id, "repeat_correction");
4241
+ const after = getMemory(db, duplicate.id);
4242
+ recordAuditWithSnapshot(
4243
+ db,
4244
+ duplicate.id,
4245
+ "promoted",
4246
+ "system",
4247
+ `repetition:${after?.repetition_count ?? updated.repetition_count}`,
4248
+ before ?? null,
4249
+ after ?? null
4250
+ );
4251
+ }
4252
+ ids.push(duplicate.id);
4253
+ continue;
4254
+ }
4255
+ const inferredScope = inferScope(
4256
+ correction.text,
4257
+ ctx.path,
4258
+ void 0,
4259
+ {
4260
+ prev_assistant_turn: ctx.prev_assistant_turn,
4261
+ recent_tool_calls: ctx.recent_tool_calls,
4262
+ original_text: text
4263
+ }
4264
+ );
4265
+ const input = {
4266
+ type: correction.type,
4267
+ text: correction.text,
4268
+ scope: inferredScope.scope,
4269
+ path_scope: inferredScope.path_scope,
4270
+ repo: ctx.repo ?? null,
4271
+ source: correction.type === "review_pattern" ? "user_reported_review" : "user_correction",
4272
+ confidence: seedCandidateConfidence(
4273
+ Math.min(1, correction.confidence + inferredScope.confidence_modifier),
4274
+ profile
4275
+ ),
4276
+ evidence: [evidence],
4277
+ capture_context: captureContext
4278
+ };
4279
+ const id = createMemory(db, input);
4280
+ maybePromoteGroupCandidate(db, id);
4281
+ enqueueVerifyCapture(db, {
4282
+ id,
4283
+ text: input.text,
4284
+ scope: input.scope,
4285
+ path_scope: input.path_scope ?? null,
4286
+ repo: input.repo ?? null,
4287
+ capture_context: captureContext ?? null
4288
+ });
4289
+ ids.push(id);
4290
+ }
4291
+ return ids;
4292
+ }
4293
+ function maybePromoteGroupCandidate(db, candidateId) {
4294
+ const candidate = getMemory(db, candidateId);
4295
+ if (!candidate || candidate.status !== "candidate") return;
4296
+ if (isHighRiskRule(candidate.text)) return;
4297
+ const followedCount = queryMemories(db, {
4298
+ repo: candidate.repo ?? void 0,
4299
+ type: candidate.type,
4300
+ scope: candidate.scope
4301
+ }).filter((memory) => memory.id !== candidate.id).reduce((total, memory) => total + getMemoryFeedback(db, memory.id).filter((entry) => entry.outcome === "followed").length, 0);
4302
+ if (followedCount < 3) return;
4303
+ const before = candidate;
4304
+ promoteMemory(db, candidate.id, "repeat_correction");
4305
+ const after = getMemory(db, candidate.id);
4306
+ recordAuditWithSnapshot(
4307
+ db,
4308
+ candidate.id,
4309
+ "promoted",
4310
+ "system",
4311
+ `repetition:group_followed:${followedCount}`,
4312
+ before,
4313
+ after ?? null
4314
+ );
4315
+ }
4316
+ function buildCaptureContext(ctx) {
4317
+ const recentToolCalls = (ctx.recent_tool_calls ?? []).slice(-5).map((toolCall) => ({
4318
+ name: toolCall.name,
4319
+ path: toolCall.path ?? extractContextPath(toolCall.input_summary),
4320
+ exit_code: toolCall.exit_code
4321
+ }));
4322
+ const hasContext = Boolean(ctx.prev_assistant_turn) || recentToolCalls.length > 0 || Boolean(ctx.repo) || Boolean(ctx.path) || Boolean(ctx.agent);
4323
+ if (!hasContext) return null;
4324
+ return {
4325
+ prev_assistant_text: ctx.prev_assistant_turn,
4326
+ recent_tool_calls: recentToolCalls,
4327
+ repo: ctx.repo ?? null,
4328
+ path: ctx.path ?? null,
4329
+ agent: ctx.agent
4330
+ };
4331
+ }
4332
+ function extractContextPath(text) {
4333
+ if (!text) return void 0;
4334
+ const match = text.match(
4335
+ /\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/
4336
+ );
4337
+ return match?.[1];
4338
+ }
4339
+ async function processReviewFeedback(db, feedback, ctx) {
4340
+ const profile = getRepoQualityProfile(db, ctx.repo);
4341
+ const evidence = {
4342
+ type: "review_feedback",
4343
+ reported_by_user: true,
4344
+ reviewer: ctx.reviewer,
4345
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4346
+ context: feedback
4347
+ };
4348
+ const corrections = detectCorrections(feedback);
4349
+ if (corrections.length > 0) {
4350
+ const ids = [];
4351
+ for (const correction of corrections) {
4352
+ const duplicate = await findDuplicateMemory(
4353
+ db,
4354
+ ctx.repo,
4355
+ correction.type,
4356
+ correction.text,
4357
+ profile.dedup_similarity_threshold
4358
+ );
4359
+ if (duplicate) {
4360
+ appendEvidence(db, duplicate.id, evidence);
4361
+ const updated = getMemory(db, duplicate.id);
4362
+ if (updated && updated.status !== "active" && countDistinctCorrectionSessions(updated) >= Math.max(1, profile.repeat_sessions_required - 1)) {
4363
+ promoteMemory(db, duplicate.id, "review_feedback");
4364
+ }
4365
+ ids.push(duplicate.id);
4366
+ continue;
4367
+ }
4368
+ const id2 = createMemory(db, {
4369
+ type: correction.type,
4370
+ text: correction.text,
4371
+ scope: ctx.path ? "path" : "repo",
4372
+ path_scope: ctx.path ?? null,
4373
+ repo: ctx.repo ?? null,
4374
+ source: "user_reported_review",
4375
+ confidence: seedCandidateConfidence(correction.confidence + 0.1, profile),
4376
+ evidence: [evidence]
4377
+ });
4378
+ ids.push(id2);
4379
+ }
4380
+ return ids;
4381
+ }
4382
+ const id = createMemory(db, {
4383
+ type: "review_pattern",
4384
+ text: feedback,
4385
+ scope: ctx.path ? "path" : "repo",
4386
+ path_scope: ctx.path ?? null,
4387
+ repo: ctx.repo ?? null,
4388
+ source: "user_reported_review",
4389
+ confidence: seedCandidateConfidence(0.4, profile),
4390
+ evidence: [evidence]
4391
+ });
4392
+ return [id];
4393
+ }
4394
+ function textSimilarity(a, b) {
4395
+ const wordsA = new Set(a.toLowerCase().split(/\s+/));
4396
+ const wordsB = new Set(b.toLowerCase().split(/\s+/));
4397
+ const intersection = [...wordsA].filter((w) => wordsB.has(w));
4398
+ const union = /* @__PURE__ */ new Set([...wordsA, ...wordsB]);
4399
+ return intersection.length / union.size;
4400
+ }
4401
+ var REJECTED_EXEMPLAR_THRESHOLD = 0.7;
4402
+ var REJECTED_EXEMPLAR_SEMANTIC_THRESHOLD = 0.85;
4403
+ function isSimilarToRejectedFragment(db, text, threshold = REJECTED_EXEMPLAR_THRESHOLD) {
4404
+ const rejected = queryMemories(db, { status: "rejected" }).filter((m) => m.source === "user_correction" || m.source === "user_reported_review");
4405
+ for (const exemplar of rejected) {
4406
+ if (textSimilarity(text, exemplar.text) >= threshold) return true;
4407
+ }
4408
+ return false;
4409
+ }
4410
+ async function isSimilarToRejectedFragmentSemantic(db, text, options = {}) {
4411
+ const lexicalT = options.lexicalThreshold ?? REJECTED_EXEMPLAR_THRESHOLD;
4412
+ const semanticT = options.semanticThreshold ?? REJECTED_EXEMPLAR_SEMANTIC_THRESHOLD;
4413
+ if (isSimilarToRejectedFragment(db, text, lexicalT)) return true;
4414
+ const config = loadEmbeddingConfigFromEnv();
4415
+ if (!config) return false;
4416
+ const match = await findSimilarRejectedExemplar(db, text, config, semanticT);
4417
+ return match != null;
4418
+ }
4419
+ async function findDuplicateMemory(db, repo, type, text, threshold) {
4420
+ if (!repo) return void 0;
4421
+ const existing = queryMemories(db, { repo }).filter((m) => m.status !== "rejected" && m.type === type);
4422
+ let best;
4423
+ let bestScore = 0;
4424
+ for (const memory of existing) {
4425
+ const score = textSimilarity(memory.text, text);
4426
+ if (score >= threshold && score > bestScore) {
4427
+ best = memory;
4428
+ bestScore = score;
4429
+ }
4430
+ }
4431
+ if (best) return best;
4432
+ const config = loadEmbeddingConfigFromEnv();
4433
+ if (!config) return void 0;
4434
+ const semantic = await findSemanticDuplicates(
4435
+ db,
4436
+ text,
4437
+ config,
4438
+ threshold,
4439
+ { repo, type, limit: 1 }
4440
+ );
4441
+ return semantic[0] ? getMemory(db, semantic[0].id) : void 0;
4442
+ }
4443
+
2077
4444
  // src/maintenance/appliers.ts
2078
4445
  var ApplyError = class extends Error {
2079
4446
  constructor(message, code) {
@@ -2107,7 +4474,7 @@ function applyRefineCandidate(db, task, result) {
2107
4474
  path_scope: newPathScope,
2108
4475
  updated_at: now,
2109
4476
  last_validated_at: now
2110
- }).where(eq6(memories.id, memoryId)).run();
4477
+ }).where(eq10(memories.id, memoryId)).run();
2111
4478
  queueMemoryEmbeddingSync(db, memoryId);
2112
4479
  const after = getMemory(db, memoryId);
2113
4480
  const reason = result.rationale ? `refined:${task.id}:${result.rationale.slice(0, 200)}` : `refined:${task.id}`;
@@ -2151,7 +4518,7 @@ function applyVerifyCapture(db, task, result) {
2151
4518
  path_scope: newPathScope,
2152
4519
  updated_at: now,
2153
4520
  last_validated_at: now
2154
- }).where(eq6(memories.id, memoryId)).run();
4521
+ }).where(eq10(memories.id, memoryId)).run();
2155
4522
  queueMemoryEmbeddingSync(db, memoryId);
2156
4523
  const after = getMemory(db, memoryId);
2157
4524
  const reason = result.reason ? `verify:rewrite:${task.id}:${result.reason.slice(0, 200)}` : `verify:rewrite:${task.id}`;
@@ -2171,7 +4538,7 @@ function rejectMemoryFromTask(db, task, memoryId, before, actor, reasonText) {
2171
4538
  return { audit_entry_id: null, target_id: memoryId, changed_fields: [] };
2172
4539
  }
2173
4540
  const now = (/* @__PURE__ */ new Date()).toISOString();
2174
- db.update(memories).set({ status: "rejected", confidence: 0, dedupe_key: null, updated_at: now }).where(eq6(memories.id, memoryId)).run();
4541
+ db.update(memories).set({ status: "rejected", confidence: 0, dedupe_key: null, updated_at: now }).where(eq10(memories.id, memoryId)).run();
2175
4542
  queueMemoryEmbeddingSync(db, memoryId);
2176
4543
  const after = getMemory(db, memoryId);
2177
4544
  const reason = reasonText ? `${task.kind}:reject:${task.id}:${reasonText.slice(0, 200)}` : `${task.kind}:reject:${task.id}`;
@@ -2189,7 +4556,7 @@ function rejectMemoryFromTask(db, task, memoryId, before, actor, reasonText) {
2189
4556
  function applySummarizeHistory(db, task, result) {
2190
4557
  const snippetId = task.payload.snippet_id;
2191
4558
  if (!snippetId) throw new ApplyError("payload missing snippet_id", "invalid-state");
2192
- const existing = db.select().from(historySnippets).where(eq6(historySnippets.id, snippetId)).get();
4559
+ const existing = db.select().from(historySnippets).where(eq10(historySnippets.id, snippetId)).get();
2193
4560
  if (!existing) throw new ApplyError(`history snippet ${snippetId} not found`, "target-missing");
2194
4561
  const changed = [];
2195
4562
  if (existing.text !== result.summary_text) changed.push("text");
@@ -2199,7 +4566,7 @@ function applySummarizeHistory(db, task, result) {
2199
4566
  db.update(historySnippets).set({
2200
4567
  text: result.summary_text,
2201
4568
  updated_at: (/* @__PURE__ */ new Date()).toISOString()
2202
- }).where(eq6(historySnippets.id, snippetId)).run();
4569
+ }).where(eq10(historySnippets.id, snippetId)).run();
2203
4570
  return { audit_entry_id: null, target_id: snippetId, changed_fields: changed };
2204
4571
  }
2205
4572
  function applyMergeDuplicates(db, task, result) {
@@ -2228,7 +4595,7 @@ function applyMergeDuplicates(db, task, result) {
2228
4595
  path_scope: nextPathScope,
2229
4596
  updated_at: now,
2230
4597
  last_validated_at: now
2231
- }).where(eq6(memories.id, winnerId)).run();
4598
+ }).where(eq10(memories.id, winnerId)).run();
2232
4599
  queueMemoryEmbeddingSync(db, winnerId);
2233
4600
  const after = getMemory(db, winnerId);
2234
4601
  recordAuditWithSnapshot(
@@ -2252,7 +4619,7 @@ function applyMergeDuplicates(db, task, result) {
2252
4619
  supersedes: winnerId,
2253
4620
  dedupe_key: null,
2254
4621
  updated_at: now
2255
- }).where(eq6(memories.id, cand.id)).run();
4622
+ }).where(eq10(memories.id, cand.id)).run();
2256
4623
  queueMemoryEmbeddingSync(db, cand.id);
2257
4624
  const afterLoser = getMemory(db, cand.id);
2258
4625
  recordAuditWithSnapshot(
@@ -2276,7 +4643,7 @@ function applySummarizeSession(db, task, result) {
2276
4643
  const payload = task.payload;
2277
4644
  const sessionId = payload.session_id;
2278
4645
  if (!sessionId) throw new ApplyError("payload missing session_id", "invalid-state");
2279
- const id = randomUUID3();
4646
+ const id = randomUUID7();
2280
4647
  const now = (/* @__PURE__ */ new Date()).toISOString();
2281
4648
  db.insert(historySnippets).values({
2282
4649
  id,
@@ -2294,7 +4661,7 @@ function applySynthesizeRepo(db, task, result) {
2294
4661
  const payload = task.payload;
2295
4662
  const repo = payload.repo;
2296
4663
  if (!repo) throw new ApplyError("payload missing repo", "invalid-state");
2297
- const id = randomUUID3();
4664
+ const id = randomUUID7();
2298
4665
  const now = (/* @__PURE__ */ new Date()).toISOString();
2299
4666
  db.insert(historySnippets).values({
2300
4667
  id,
@@ -2308,6 +4675,78 @@ function applySynthesizeRepo(db, task, result) {
2308
4675
  }).run();
2309
4676
  return { audit_entry_id: null, target_id: id, changed_fields: ["text"] };
2310
4677
  }
4678
+ function applyExtractRulesFromPrompt(db, task, result) {
4679
+ const payload = task.payload;
4680
+ const repo = payload.repo ?? null;
4681
+ const profile = getRepoQualityProfile(db, repo ?? void 0);
4682
+ if (!result.rules || result.rules.length === 0) {
4683
+ return { audit_entry_id: null, target_id: task.id, changed_fields: [] };
4684
+ }
4685
+ const captureContext = {
4686
+ prev_assistant_text: void 0,
4687
+ recent_tool_calls: [],
4688
+ repo,
4689
+ path: payload.path ?? null,
4690
+ agent: payload.agent ?? void 0
4691
+ };
4692
+ const createdIds = [];
4693
+ for (const rule of result.rules) {
4694
+ if (existsSimilar(db, repo, rule)) continue;
4695
+ const memoryType = rule.type ?? "rule";
4696
+ const id = createMemory(db, {
4697
+ type: memoryType,
4698
+ text: rule.text,
4699
+ scope: rule.scope,
4700
+ path_scope: rule.path_scope ?? null,
4701
+ repo,
4702
+ source: "user_correction",
4703
+ confidence: seedCandidateConfidence(rule.confidence, profile),
4704
+ evidence: [
4705
+ {
4706
+ type: "session_correction",
4707
+ session: payload.session_id ?? "unknown",
4708
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4709
+ context: payload.raw_prompt ?? ""
4710
+ }
4711
+ ],
4712
+ capture_context: captureContext
4713
+ });
4714
+ createdIds.push(id);
4715
+ const after = getMemory(db, id);
4716
+ recordAuditWithSnapshot(
4717
+ db,
4718
+ id,
4719
+ "created",
4720
+ `maintenance:${task.claimed_by ?? "llm"}`,
4721
+ `extract_rules_from_prompt:${task.id}${rule.rationale ? `:${rule.rationale.slice(0, 200)}` : ""}`,
4722
+ null,
4723
+ after ?? null
4724
+ );
4725
+ if (rule.is_destructive_risky || isHighRiskRule(rule.text)) {
4726
+ }
4727
+ }
4728
+ return {
4729
+ audit_entry_id: null,
4730
+ target_id: createdIds[0] ?? task.id,
4731
+ changed_fields: createdIds.length > 0 ? ["created_memories"] : []
4732
+ };
4733
+ }
4734
+ function existsSimilar(db, repo, rule) {
4735
+ if (!repo) return false;
4736
+ const candidates = queryMemories(db, { repo: repo ?? void 0, type: rule.type }).filter((memory) => memory.status !== "rejected");
4737
+ const normalized = rule.text.toLowerCase().trim();
4738
+ return candidates.some((memory) => {
4739
+ if (memory.text.toLowerCase().trim() === normalized) return true;
4740
+ return jaccard(memory.text, rule.text) >= 0.85;
4741
+ });
4742
+ }
4743
+ function jaccard(a, b) {
4744
+ const wordsA = new Set(a.toLowerCase().split(/\s+/));
4745
+ const wordsB = new Set(b.toLowerCase().split(/\s+/));
4746
+ const intersection = [...wordsA].filter((w) => wordsB.has(w));
4747
+ const union = /* @__PURE__ */ new Set([...wordsA, ...wordsB]);
4748
+ return union.size === 0 ? 0 : intersection.length / union.size;
4749
+ }
2311
4750
  function applyTaskResult(db, task, result) {
2312
4751
  switch (task.kind) {
2313
4752
  case "verify_capture":
@@ -2322,6 +4761,8 @@ function applyTaskResult(db, task, result) {
2322
4761
  return applySummarizeSession(db, task, result);
2323
4762
  case "synthesize_repo":
2324
4763
  return applySynthesizeRepo(db, task, result);
4764
+ case "extract_rules_from_prompt":
4765
+ return applyExtractRulesFromPrompt(db, task, result);
2325
4766
  default: {
2326
4767
  const never = task.kind;
2327
4768
  throw new ApplyError(`unknown kind ${never}`, "unsupported-kind");
@@ -2332,8 +4773,13 @@ function applyTaskResult(db, task, result) {
2332
4773
  // src/maintenance/tasks.ts
2333
4774
  var OPEN_STATUSES = ["pending", "claimed", "submitted"];
2334
4775
  var ACTIVE_STATUSES = [...OPEN_STATUSES, "completed"];
2335
- var DEFAULT_LEASE_SECONDS = 600;
4776
+ var DEFAULT_LEASE_SECONDS2 = 600;
2336
4777
  var DEFAULT_PRIORITIES = {
4778
+ // extract_rules_from_prompt runs at higher priority than verify_capture
4779
+ // because it is the primary capture path when an LLM provider is
4780
+ // configured — its output IS the candidate creation. Without it, real
4781
+ // rules never enter the queue at all.
4782
+ extract_rules_from_prompt: 14,
2337
4783
  verify_capture: 12,
2338
4784
  refine_candidate: 10,
2339
4785
  merge_duplicates: 8,
@@ -2376,16 +4822,16 @@ function targetKey(kind, target) {
2376
4822
  return `${kind}:${target}`;
2377
4823
  }
2378
4824
  function hasActiveTaskForTarget(db, kind, target) {
2379
- const row = db.select({ id: memoryMaintenanceTasks.id }).from(memoryMaintenanceTasks).where(and2(
2380
- eq7(memoryMaintenanceTasks.kind, kind),
2381
- eq7(memoryMaintenanceTasks.target_key, targetKey(kind, target)),
2382
- inArray2(memoryMaintenanceTasks.status, ACTIVE_STATUSES)
4825
+ const row = db.select({ id: memoryMaintenanceTasks.id }).from(memoryMaintenanceTasks).where(and4(
4826
+ eq11(memoryMaintenanceTasks.kind, kind),
4827
+ eq11(memoryMaintenanceTasks.target_key, targetKey(kind, target)),
4828
+ inArray3(memoryMaintenanceTasks.status, ACTIVE_STATUSES)
2383
4829
  )).limit(1).get();
2384
4830
  return Boolean(row);
2385
4831
  }
2386
4832
  function insertTaskIdempotent(db, input) {
2387
4833
  if (hasActiveTaskForTarget(db, input.kind, input.target)) return null;
2388
- const id = randomUUID4();
4834
+ const id = randomUUID8();
2389
4835
  db.insert(memoryMaintenanceTasks).values({
2390
4836
  id,
2391
4837
  kind: input.kind,
@@ -2408,13 +4854,13 @@ function insertTaskIdempotent(db, input) {
2408
4854
  return id;
2409
4855
  }
2410
4856
  function deleteTask(db, id) {
2411
- const result = db.delete(memoryMaintenanceTasks).where(eq7(memoryMaintenanceTasks.id, id)).run();
4857
+ const result = db.delete(memoryMaintenanceTasks).where(eq11(memoryMaintenanceTasks.id, id)).run();
2412
4858
  return result.changes > 0;
2413
4859
  }
2414
4860
  function getTaskStats(db) {
2415
4861
  const rows = db.select().from(memoryMaintenanceTasks).all();
2416
4862
  const by_status = { pending: 0, claimed: 0, submitted: 0, completed: 0, abandoned: 0 };
2417
- const by_kind = { verify_capture: 0, refine_candidate: 0, merge_duplicates: 0, summarize_history: 0, summarize_session: 0, synthesize_repo: 0 };
4863
+ const by_kind = { verify_capture: 0, refine_candidate: 0, merge_duplicates: 0, summarize_history: 0, summarize_session: 0, synthesize_repo: 0, extract_rules_from_prompt: 0 };
2418
4864
  const by_kind_status = {};
2419
4865
  const dayAgo = new Date(Date.now() - 864e5).toISOString();
2420
4866
  let completed_last_24h = 0;
@@ -2451,24 +4897,24 @@ function getTaskStats(db) {
2451
4897
  };
2452
4898
  }
2453
4899
  function getTask(db, id) {
2454
- const row = db.select().from(memoryMaintenanceTasks).where(eq7(memoryMaintenanceTasks.id, id)).get();
4900
+ const row = db.select().from(memoryMaintenanceTasks).where(eq11(memoryMaintenanceTasks.id, id)).get();
2455
4901
  return row ? rowToTask(row) : void 0;
2456
4902
  }
2457
4903
  function listTasks(db, query = {}) {
2458
4904
  const conditions = [];
2459
4905
  if (query.status) {
2460
4906
  const statuses = Array.isArray(query.status) ? query.status : [query.status];
2461
- conditions.push(inArray2(memoryMaintenanceTasks.status, statuses));
4907
+ conditions.push(inArray3(memoryMaintenanceTasks.status, statuses));
2462
4908
  }
2463
4909
  if (query.kinds?.length) {
2464
- conditions.push(inArray2(memoryMaintenanceTasks.kind, query.kinds));
4910
+ conditions.push(inArray3(memoryMaintenanceTasks.kind, query.kinds));
2465
4911
  }
2466
4912
  if (query.repo) {
2467
- conditions.push(eq7(memoryMaintenanceTasks.repo, query.repo));
4913
+ conditions.push(eq11(memoryMaintenanceTasks.repo, query.repo));
2468
4914
  }
2469
4915
  let stmt = db.select().from(memoryMaintenanceTasks).$dynamic();
2470
- if (conditions.length) stmt = stmt.where(and2(...conditions));
2471
- stmt = stmt.orderBy(sql2`${memoryMaintenanceTasks.priority} DESC`, memoryMaintenanceTasks.created_at).limit(query.limit ?? 50);
4916
+ if (conditions.length) stmt = stmt.where(and4(...conditions));
4917
+ stmt = stmt.orderBy(sql3`${memoryMaintenanceTasks.priority} DESC`, memoryMaintenanceTasks.created_at).limit(query.limit ?? 50);
2472
4918
  return stmt.all().map(rowToTask);
2473
4919
  }
2474
4920
  function sweepExpiredLeases(db, now = /* @__PURE__ */ new Date()) {
@@ -2478,9 +4924,9 @@ function sweepExpiredLeases(db, now = /* @__PURE__ */ new Date()) {
2478
4924
  claimed_by: null,
2479
4925
  claimed_at: null,
2480
4926
  claim_expires_at: null,
2481
- attempts: sql2`${memoryMaintenanceTasks.attempts} + 1`
2482
- }).where(and2(
2483
- eq7(memoryMaintenanceTasks.status, "claimed"),
4927
+ attempts: sql3`${memoryMaintenanceTasks.attempts} + 1`
4928
+ }).where(and4(
4929
+ eq11(memoryMaintenanceTasks.status, "claimed"),
2484
4930
  lt(memoryMaintenanceTasks.claim_expires_at, nowIso)
2485
4931
  )).run();
2486
4932
  return result.changes;
@@ -2491,8 +4937,8 @@ function expireStalePendingTasks(db, maxAgeDays, now = /* @__PURE__ */ new Date(
2491
4937
  status: "abandoned",
2492
4938
  failure_reason: "expired_no_dispatcher",
2493
4939
  completed_at: now.toISOString()
2494
- }).where(and2(
2495
- eq7(memoryMaintenanceTasks.status, "pending"),
4940
+ }).where(and4(
4941
+ eq11(memoryMaintenanceTasks.status, "pending"),
2496
4942
  lt(memoryMaintenanceTasks.created_at, cutoff)
2497
4943
  )).run();
2498
4944
  return result.changes;
@@ -2503,9 +4949,9 @@ function abandonOverAttemptTasks(db) {
2503
4949
  status: "abandoned",
2504
4950
  failure_reason: "max_attempts_exceeded",
2505
4951
  completed_at: nowIso
2506
- }).where(and2(
2507
- inArray2(memoryMaintenanceTasks.status, ["pending", "claimed"]),
2508
- sql2`${memoryMaintenanceTasks.attempts} >= ${memoryMaintenanceTasks.max_attempts}`
4952
+ }).where(and4(
4953
+ inArray3(memoryMaintenanceTasks.status, ["pending", "claimed"]),
4954
+ sql3`${memoryMaintenanceTasks.attempts} >= ${memoryMaintenanceTasks.max_attempts}`
2509
4955
  )).run();
2510
4956
  return result.changes;
2511
4957
  }
@@ -2513,27 +4959,44 @@ function applyBacklogCaps(db, config) {
2513
4959
  let dropped = 0;
2514
4960
  const overKindRows = db.select({
2515
4961
  kind: memoryMaintenanceTasks.kind,
2516
- count: sql2`count(*)`.as("count")
2517
- }).from(memoryMaintenanceTasks).where(eq7(memoryMaintenanceTasks.status, "pending")).groupBy(memoryMaintenanceTasks.kind).all();
4962
+ count: sql3`count(*)`.as("count")
4963
+ }).from(memoryMaintenanceTasks).where(eq11(memoryMaintenanceTasks.status, "pending")).groupBy(memoryMaintenanceTasks.kind).all();
2518
4964
  for (const { kind, count } of overKindRows) {
2519
4965
  if (count <= config.max_per_kind) continue;
2520
4966
  const toDrop = count - config.max_per_kind;
2521
4967
  dropped += dropLowestPriorityPending(db, toDrop, { kind });
2522
4968
  }
2523
- const pendingCount = db.select({ n: sql2`count(*)` }).from(memoryMaintenanceTasks).where(eq7(memoryMaintenanceTasks.status, "pending")).get()?.n ?? 0;
4969
+ const pendingCount = db.select({ n: sql3`count(*)` }).from(memoryMaintenanceTasks).where(eq11(memoryMaintenanceTasks.status, "pending")).get()?.n ?? 0;
2524
4970
  if (pendingCount > config.max_pending) {
2525
4971
  dropped += dropLowestPriorityPending(db, pendingCount - config.max_pending);
2526
4972
  }
2527
4973
  return dropped;
2528
4974
  }
2529
4975
  function dropLowestPriorityPending(db, limit, filter = {}) {
2530
- const conditions = [eq7(memoryMaintenanceTasks.status, "pending")];
2531
- if (filter.kind) conditions.push(eq7(memoryMaintenanceTasks.kind, filter.kind));
2532
- const ids = db.select({ id: memoryMaintenanceTasks.id }).from(memoryMaintenanceTasks).where(and2(...conditions)).orderBy(memoryMaintenanceTasks.priority, sql2`${memoryMaintenanceTasks.created_at} DESC`).limit(limit).all().map((r) => r.id);
4976
+ const conditions = [eq11(memoryMaintenanceTasks.status, "pending")];
4977
+ if (filter.kind) conditions.push(eq11(memoryMaintenanceTasks.kind, filter.kind));
4978
+ const ids = db.select({ id: memoryMaintenanceTasks.id }).from(memoryMaintenanceTasks).where(and4(...conditions)).orderBy(memoryMaintenanceTasks.priority, sql3`${memoryMaintenanceTasks.created_at} DESC`).limit(limit).all().map((r) => r.id);
2533
4979
  if (!ids.length) return 0;
2534
- const result = db.delete(memoryMaintenanceTasks).where(inArray2(memoryMaintenanceTasks.id, ids)).run();
4980
+ const result = db.delete(memoryMaintenanceTasks).where(inArray3(memoryMaintenanceTasks.id, ids)).run();
2535
4981
  return result.changes;
2536
4982
  }
4983
+ function enqueueExtractRulesFromPrompt(db, payload) {
4984
+ return insertTaskIdempotent(db, {
4985
+ kind: "extract_rules_from_prompt",
4986
+ target: payload.prompt_id,
4987
+ repo: payload.repo,
4988
+ payload: {
4989
+ prompt_id: payload.prompt_id,
4990
+ raw_prompt: payload.raw_prompt,
4991
+ repo: payload.repo,
4992
+ path: payload.path,
4993
+ agent: payload.agent,
4994
+ session_id: payload.session_id,
4995
+ prev_assistant_turn: payload.prev_assistant_turn ?? null,
4996
+ recent_tool_calls: payload.recent_tool_calls ?? null
4997
+ }
4998
+ });
4999
+ }
2537
5000
  function enqueueVerifyCapture(db, memory) {
2538
5001
  return insertTaskIdempotent(db, {
2539
5002
  kind: "verify_capture",
@@ -2550,9 +5013,9 @@ function enqueueVerifyCapture(db, memory) {
2550
5013
  });
2551
5014
  }
2552
5015
  function produceRefineCandidateTasks(db, config) {
2553
- const candidates = db.select().from(memories).where(and2(
2554
- eq7(memories.status, "candidate"),
2555
- or(eq7(memories.scope, "repo"), isNull(memories.path_scope))
5016
+ const candidates = db.select().from(memories).where(and4(
5017
+ eq11(memories.status, "candidate"),
5018
+ or(eq11(memories.scope, "repo"), isNull(memories.path_scope))
2556
5019
  )).all();
2557
5020
  let enqueued = 0;
2558
5021
  for (const row of candidates) {
@@ -2578,7 +5041,7 @@ function produceRefineCandidateTasks(db, config) {
2578
5041
  }
2579
5042
  function produceSummarizeHistoryTasks(db, config) {
2580
5043
  const cutoff = new Date(Date.now() - config.summary_max_age_days * 864e5).toISOString();
2581
- const snippets = db.select().from(historySnippets).where(sql2`${historySnippets.created_at} >= ${cutoff}`).all();
5044
+ const snippets = db.select().from(historySnippets).where(sql3`${historySnippets.created_at} >= ${cutoff}`).all();
2582
5045
  let enqueued = 0;
2583
5046
  for (const snippet of snippets) {
2584
5047
  if (!snippetHasMeaningfulContent(snippet.text)) continue;
@@ -2609,7 +5072,7 @@ function snippetHasMeaningfulContent(text) {
2609
5072
  async function produceMergeDuplicateTasks(db, config) {
2610
5073
  const embeddingConfig = loadEmbeddingConfigFromEnv();
2611
5074
  if (!embeddingConfig) return 0;
2612
- const activeMemories = db.select().from(memories).where(eq7(memories.status, "active")).all();
5075
+ const activeMemories = db.select().from(memories).where(eq11(memories.status, "active")).all();
2613
5076
  const visited = /* @__PURE__ */ new Set();
2614
5077
  let enqueued = 0;
2615
5078
  for (const mem of activeMemories) {
@@ -2630,7 +5093,7 @@ async function produceMergeDuplicateTasks(db, config) {
2630
5093
  for (const id2 of cluster) visited.add(id2);
2631
5094
  continue;
2632
5095
  }
2633
- const clusterRows = db.select().from(memories).where(inArray2(memories.id, cluster)).all();
5096
+ const clusterRows = db.select().from(memories).where(inArray3(memories.id, cluster)).all();
2634
5097
  const candidates = clusterRows.map((row) => ({
2635
5098
  id: row.id,
2636
5099
  text: row.text,
@@ -2655,18 +5118,18 @@ async function produceMergeDuplicateTasks(db, config) {
2655
5118
  }
2656
5119
  function produceSummarizeSessionTasks(db, config) {
2657
5120
  const cutoff = new Date(Date.now() - config.summary_max_age_days * 864e5).toISOString();
2658
- const sessionEnds = db.select().from(activityEvents).where(and2(
2659
- eq7(activityEvents.event_type, "session_end"),
5121
+ const sessionEnds = db.select().from(activityEvents).where(and4(
5122
+ eq11(activityEvents.event_type, "session_end"),
2660
5123
  gt(activityEvents.created_at, cutoff)
2661
5124
  )).orderBy(desc2(activityEvents.created_at)).all();
2662
5125
  let enqueued = 0;
2663
5126
  for (const end of sessionEnds) {
2664
5127
  if (!end.session_id) continue;
2665
- const events = db.select().from(activityEvents).where(eq7(activityEvents.session_id, end.session_id)).all();
5128
+ const events = db.select().from(activityEvents).where(eq11(activityEvents.session_id, end.session_id)).all();
2666
5129
  if (events.length < config.session_min_activity_events) continue;
2667
- const existing = db.select().from(historySnippets).where(and2(
2668
- eq7(historySnippets.session_id, end.session_id),
2669
- eq7(historySnippets.kind, "session_summary")
5130
+ const existing = db.select().from(historySnippets).where(and4(
5131
+ eq11(historySnippets.session_id, end.session_id),
5132
+ eq11(historySnippets.kind, "session_summary")
2670
5133
  )).get();
2671
5134
  if (existing) continue;
2672
5135
  const repo = end.repo ?? events.find((e) => e.repo)?.repo ?? null;
@@ -2690,22 +5153,22 @@ function produceSummarizeSessionTasks(db, config) {
2690
5153
  function produceSynthesizeRepoTasks(db, config) {
2691
5154
  const rows = db.select({
2692
5155
  repo: memories.repo,
2693
- count: sql2`count(*)`.as("count")
2694
- }).from(memories).where(eq7(memories.status, "active")).groupBy(memories.repo).all();
5156
+ count: sql3`count(*)`.as("count")
5157
+ }).from(memories).where(eq11(memories.status, "active")).groupBy(memories.repo).all();
2695
5158
  const cutoff = new Date(Date.now() - config.repo_synthesis_refresh_days * 864e5).toISOString();
2696
5159
  let enqueued = 0;
2697
5160
  for (const { repo, count } of rows) {
2698
5161
  if (!repo) continue;
2699
5162
  if (count < config.repo_synthesis_min_memories) continue;
2700
- const recent = db.select().from(historySnippets).where(and2(
2701
- eq7(historySnippets.repo, repo),
2702
- eq7(historySnippets.kind, "repo_synthesis"),
5163
+ const recent = db.select().from(historySnippets).where(and4(
5164
+ eq11(historySnippets.repo, repo),
5165
+ eq11(historySnippets.kind, "repo_synthesis"),
2703
5166
  gt(historySnippets.updated_at, cutoff)
2704
5167
  )).get();
2705
5168
  if (recent) continue;
2706
- const topMemories = db.select().from(memories).where(and2(
2707
- eq7(memories.repo, repo),
2708
- eq7(memories.status, "active")
5169
+ const topMemories = db.select().from(memories).where(and4(
5170
+ eq11(memories.repo, repo),
5171
+ eq11(memories.status, "active")
2709
5172
  )).orderBy(desc2(memories.confidence)).limit(20).all().map((row) => ({
2710
5173
  id: row.id,
2711
5174
  text: row.text,
@@ -2783,13 +5246,27 @@ var SummarizeSessionResult = z2.object({
2783
5246
  var SynthesizeRepoResult = z2.object({
2784
5247
  summary_text: z2.string().min(1).max(8e3)
2785
5248
  });
5249
+ var ExtractedRule = z2.object({
5250
+ text: z2.string().min(1).max(2e3),
5251
+ type: z2.enum(["rule", "decision", "review_pattern", "command", "gotcha"]),
5252
+ scope: MemoryScope2,
5253
+ path_scope: z2.string().max(512).nullable().optional(),
5254
+ confidence: z2.number().min(0).max(1),
5255
+ is_destructive_risky: z2.boolean().optional(),
5256
+ rationale: z2.string().max(500).optional()
5257
+ });
5258
+ var ExtractRulesFromPromptResult = z2.object({
5259
+ rules: z2.array(ExtractedRule).max(10),
5260
+ dropped_reason: z2.string().max(500).optional()
5261
+ });
2786
5262
  var RESULT_SCHEMAS = {
2787
5263
  verify_capture: VerifyCaptureResult,
2788
5264
  refine_candidate: RefineCandidateResult,
2789
5265
  summarize_history: SummarizeHistoryResult,
2790
5266
  merge_duplicates: MergeDuplicatesResult,
2791
5267
  summarize_session: SummarizeSessionResult,
2792
- synthesize_repo: SynthesizeRepoResult
5268
+ synthesize_repo: SynthesizeRepoResult,
5269
+ extract_rules_from_prompt: ExtractRulesFromPromptResult
2793
5270
  };
2794
5271
  function payloadSummary(payload) {
2795
5272
  const out = {};
@@ -2830,7 +5307,7 @@ var TaskClaimConflictError = class extends Error {
2830
5307
  taskId;
2831
5308
  reason;
2832
5309
  };
2833
- function claimTask(db, taskId, agent, leaseSeconds = DEFAULT_LEASE_SECONDS) {
5310
+ function claimTask(db, taskId, agent, leaseSeconds = DEFAULT_LEASE_SECONDS2) {
2834
5311
  const now = /* @__PURE__ */ new Date();
2835
5312
  const expiresAt = new Date(now.getTime() + leaseSeconds * 1e3).toISOString();
2836
5313
  const nowIso = now.toISOString();
@@ -2839,9 +5316,9 @@ function claimTask(db, taskId, agent, leaseSeconds = DEFAULT_LEASE_SECONDS) {
2839
5316
  claimed_by: agent,
2840
5317
  claimed_at: nowIso,
2841
5318
  claim_expires_at: expiresAt
2842
- }).where(and2(
2843
- eq7(memoryMaintenanceTasks.id, taskId),
2844
- eq7(memoryMaintenanceTasks.status, "pending")
5319
+ }).where(and4(
5320
+ eq11(memoryMaintenanceTasks.id, taskId),
5321
+ eq11(memoryMaintenanceTasks.status, "pending")
2845
5322
  )).run();
2846
5323
  if (result.changes === 0) {
2847
5324
  const existing = getTask(db, taskId);
@@ -2873,7 +5350,7 @@ function submitTask(db, taskId, agent, result) {
2873
5350
  attempts,
2874
5351
  failure_reason: parsed.error.issues.map((i) => `${i.path.join(".")}:${i.message}`).join("; ").slice(0, 500),
2875
5352
  completed_at: abandoned ? now2 : null
2876
- }).where(eq7(memoryMaintenanceTasks.id, taskId)).run();
5353
+ }).where(eq11(memoryMaintenanceTasks.id, taskId)).run();
2877
5354
  return {
2878
5355
  status: "rejected",
2879
5356
  task_id: taskId,
@@ -2899,7 +5376,7 @@ function submitTask(db, taskId, agent, result) {
2899
5376
  attempts,
2900
5377
  failure_reason: `apply-failed: ${message}`.slice(0, 500),
2901
5378
  completed_at: abandoned ? now2 : null
2902
- }).where(eq7(memoryMaintenanceTasks.id, taskId)).run();
5379
+ }).where(eq11(memoryMaintenanceTasks.id, taskId)).run();
2903
5380
  return {
2904
5381
  status: "rejected",
2905
5382
  task_id: taskId,
@@ -2915,7 +5392,7 @@ function submitTask(db, taskId, agent, result) {
2915
5392
  submitted_at: now,
2916
5393
  completed_at: now,
2917
5394
  failure_reason: null
2918
- }).where(eq7(memoryMaintenanceTasks.id, taskId)).run();
5395
+ }).where(eq11(memoryMaintenanceTasks.id, taskId)).run();
2919
5396
  return {
2920
5397
  status: "applied",
2921
5398
  task_id: taskId,
@@ -2937,13 +5414,12 @@ function releaseTask(db, taskId, agent, reason) {
2937
5414
  claimed_at: null,
2938
5415
  claim_expires_at: null,
2939
5416
  failure_reason: reason ? reason.slice(0, 500) : null
2940
- }).where(eq7(memoryMaintenanceTasks.id, taskId)).run();
5417
+ }).where(eq11(memoryMaintenanceTasks.id, taskId)).run();
2941
5418
  return { status: "released" };
2942
5419
  }
2943
5420
 
2944
5421
  export {
2945
5422
  getEmbeddingCacheRoot,
2946
- memoryDedupeKey,
2947
5423
  historySnippetDedupeKey,
2948
5424
  activityEventDedupeKey,
2949
5425
  hookCallDedupeKey,
@@ -2963,8 +5439,6 @@ export {
2963
5439
  verifyEmbeddings,
2964
5440
  rebuildEmbeddingIndex,
2965
5441
  hybridSearch,
2966
- findSemanticDuplicates,
2967
- findSimilarRejectedExemplar,
2968
5442
  statusFromConfidence,
2969
5443
  createMemory,
2970
5444
  getMemory,
@@ -2976,21 +5450,21 @@ export {
2976
5450
  demoteGlobalMemory,
2977
5451
  rejectMemory,
2978
5452
  confirmMemory,
2979
- appendEvidence,
2980
- updateMemoryCaptureContext,
2981
- incrementMemoryRepetition,
2982
- countDistinctCorrectionSessions,
2983
5453
  recordFeedback,
2984
- getMemoryFeedback,
2985
5454
  getMemoryFeedbackSummaries,
2986
5455
  feedbackWeightedScore,
5456
+ computeHealthScore,
5457
+ computeAllHealthScores,
5458
+ formatHealthReport,
5459
+ getRepoQualityProfile,
5460
+ seedScannedConfidence,
2987
5461
  recordAudit,
2988
5462
  recordAuditWithSnapshot,
2989
5463
  getAuditTrail,
2990
5464
  getRecentAudit,
2991
5465
  rollbackMemory,
2992
5466
  formatAuditTrail,
2993
- DEFAULT_LEASE_SECONDS,
5467
+ DEFAULT_LEASE_SECONDS2 as DEFAULT_LEASE_SECONDS,
2994
5468
  DEFAULT_PRIORITIES,
2995
5469
  DEFAULT_ENQUEUE_CONFIG,
2996
5470
  targetKey,
@@ -3004,6 +5478,7 @@ export {
3004
5478
  expireStalePendingTasks,
3005
5479
  abandonOverAttemptTasks,
3006
5480
  applyBacklogCaps,
5481
+ enqueueExtractRulesFromPrompt,
3007
5482
  enqueueVerifyCapture,
3008
5483
  produceRefineCandidateTasks,
3009
5484
  produceSummarizeHistoryTasks,
@@ -3016,6 +5491,30 @@ export {
3016
5491
  TaskClaimConflictError,
3017
5492
  claimTask,
3018
5493
  submitTask,
3019
- releaseTask
5494
+ releaseTask,
5495
+ dispatchPendingTasks,
5496
+ hasAnyLlmProvider,
5497
+ buildPrompt,
5498
+ formatDispatchReport,
5499
+ inferScope,
5500
+ detectContradictions,
5501
+ resolveContradiction,
5502
+ autoResolveContradictions,
5503
+ listContradictions,
5504
+ runDeterministicCleanup,
5505
+ planDedupeExact,
5506
+ planRejectFragments,
5507
+ qualityReasons,
5508
+ planPromoteRepeats,
5509
+ planSuppressCommands,
5510
+ planGlobalizeCrossRepo,
5511
+ revertCleanupRun,
5512
+ listCleanupRuns,
5513
+ formatCleanupReport,
5514
+ isTriggerTemplateRule,
5515
+ isHighRiskRule,
5516
+ detectCorrections,
5517
+ processCorrection,
5518
+ processReviewFeedback
3020
5519
  };
3021
- //# sourceMappingURL=chunk-IILLSHLM.js.map
5520
+ //# sourceMappingURL=chunk-KAGIAOD7.js.map