@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.
- package/dist/{chunk-K5FZ47NN.js → chunk-7XCLKPJ3.js} +6 -8
- package/dist/{chunk-K5FZ47NN.js.map → chunk-7XCLKPJ3.js.map} +1 -1
- package/dist/{chunk-A5UIRZU6.js → chunk-A6XEULA4.js} +3 -2
- package/dist/chunk-A6XEULA4.js.map +1 -0
- package/dist/{chunk-F56Y3SHS.js → chunk-E4HJDGCW.js} +7 -9
- package/dist/{chunk-F56Y3SHS.js.map → chunk-E4HJDGCW.js.map} +1 -1
- package/dist/{chunk-IILLSHLM.js → chunk-KAGIAOD7.js} +2583 -84
- package/dist/chunk-KAGIAOD7.js.map +1 -0
- package/dist/{chunk-FHKV6ELT.js → chunk-MJ4GGBTL.js} +11 -13
- package/dist/{chunk-FHKV6ELT.js.map → chunk-MJ4GGBTL.js.map} +1 -1
- package/dist/{chunk-LVQW6WHK.js → chunk-XUM7JEJU.js} +2 -2
- package/dist/{cleanup-TVOX2S2S.js → cleanup-MYSQ44EP.js} +4 -4
- package/dist/cli.js +206 -33
- package/dist/cli.js.map +1 -1
- package/dist/daemon.js +60 -49
- package/dist/daemon.js.map +1 -1
- package/dist/dispatcher-SUUX5AX6.js +16 -0
- package/dist/mcp.js +5 -5
- package/dist/{quality-Z7LPMMBC.js → quality-YTQKAEY6.js} +3 -3
- package/dist/{tasks-UOLSPXJQ.js → tasks-GSQUHD4F.js} +6 -3
- package/dist/{usage-CY3V72YN.js → usage-DU4TKVJH.js} +2 -2
- package/package.json +1 -1
- package/dist/chunk-A5UIRZU6.js.map +0 -1
- package/dist/chunk-GC5XMBG4.js +0 -551
- package/dist/chunk-GC5XMBG4.js.map +0 -1
- package/dist/chunk-IILLSHLM.js.map +0 -1
- package/dist/chunk-VEPXEHRZ.js +0 -1763
- package/dist/chunk-VEPXEHRZ.js.map +0 -1
- package/dist/dispatcher-UGMU6THT.js +0 -15
- /package/dist/{chunk-LVQW6WHK.js.map → chunk-XUM7JEJU.js.map} +0 -0
- /package/dist/{cleanup-TVOX2S2S.js.map → cleanup-MYSQ44EP.js.map} +0 -0
- /package/dist/{dispatcher-UGMU6THT.js.map → dispatcher-SUUX5AX6.js.map} +0 -0
- /package/dist/{quality-Z7LPMMBC.js.map → quality-YTQKAEY6.js.map} +0 -0
- /package/dist/{tasks-UOLSPXJQ.js.map → tasks-GSQUHD4F.js.map} +0 -0
- /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-
|
|
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
|
|
13
|
-
import { randomUUID as
|
|
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
|
|
1540
|
-
import { randomUUID as
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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 =
|
|
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
|
|
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(
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
4907
|
+
conditions.push(inArray3(memoryMaintenanceTasks.status, statuses));
|
|
2462
4908
|
}
|
|
2463
4909
|
if (query.kinds?.length) {
|
|
2464
|
-
conditions.push(
|
|
4910
|
+
conditions.push(inArray3(memoryMaintenanceTasks.kind, query.kinds));
|
|
2465
4911
|
}
|
|
2466
4912
|
if (query.repo) {
|
|
2467
|
-
conditions.push(
|
|
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(
|
|
2471
|
-
stmt = stmt.orderBy(
|
|
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:
|
|
2482
|
-
}).where(
|
|
2483
|
-
|
|
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(
|
|
2495
|
-
|
|
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(
|
|
2507
|
-
|
|
2508
|
-
|
|
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:
|
|
2517
|
-
}).from(memoryMaintenanceTasks).where(
|
|
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:
|
|
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 = [
|
|
2531
|
-
if (filter.kind) conditions.push(
|
|
2532
|
-
const ids = db.select({ id: memoryMaintenanceTasks.id }).from(memoryMaintenanceTasks).where(
|
|
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(
|
|
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(
|
|
2554
|
-
|
|
2555
|
-
or(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
2659
|
-
|
|
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(
|
|
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(
|
|
2668
|
-
|
|
2669
|
-
|
|
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:
|
|
2694
|
-
}).from(memories).where(
|
|
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(
|
|
2701
|
-
|
|
2702
|
-
|
|
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(
|
|
2707
|
-
|
|
2708
|
-
|
|
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 =
|
|
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(
|
|
2843
|
-
|
|
2844
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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-
|
|
5520
|
+
//# sourceMappingURL=chunk-KAGIAOD7.js.map
|