engram-sdk 0.5.1 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/cli.js +85 -2
- package/dist/cli.js.map +1 -1
- package/dist/hosted.d.ts.map +1 -1
- package/dist/hosted.js +38 -1
- package/dist/hosted.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +218 -0
- package/dist/server.js.map +1 -1
- package/dist/telemetry.js +1 -1
- package/dist/telemetry.js.map +1 -1
- package/package.json +1 -1
- package/.mcpregistry_github_token +0 -1
- package/.mcpregistry_registry_token +0 -1
- package/deploy/fly.toml +0 -26
- package/eval-codebase-v2-NOTE.md +0 -22
- package/fly.toml +0 -33
- package/hackernews-post.md +0 -45
- package/hn-posts/2026-02-23.md +0 -64
- package/rescore-codebase.ts +0 -184
- package/rescore-vscode.ts +0 -142
- package/signal-quality-plan.md +0 -23
package/rescore-codebase.ts
DELETED
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env npx tsx
|
|
2
|
-
/**
|
|
3
|
-
* rescore-codebase.ts — Re-score existing codebase eval results using LLM judge
|
|
4
|
-
* Uses the saved answers + ground truth, just re-runs scoring
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { readFileSync, writeFileSync } from 'fs';
|
|
8
|
-
import { resolve, dirname } from 'path';
|
|
9
|
-
import { fileURLToPath } from 'url';
|
|
10
|
-
|
|
11
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
|
-
const GEMINI_KEY = readFileSync(resolve(process.env.HOME!, '.config/engram/gemini-key'), 'utf8').trim();
|
|
13
|
-
const RESULTS_PATH = resolve(__dirname, 'eval-scale-data/codebase-results-vscode.json');
|
|
14
|
-
const REPORT_PATH = resolve(__dirname, 'eval-scale-data/codebase-report-vscode-v2.json');
|
|
15
|
-
|
|
16
|
-
async function geminiCall(prompt: string, maxTokens = 100): Promise<string> {
|
|
17
|
-
for (let attempt = 0; attempt < 3; attempt++) {
|
|
18
|
-
try {
|
|
19
|
-
const response = await fetch(
|
|
20
|
-
`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${GEMINI_KEY}`,
|
|
21
|
-
{
|
|
22
|
-
method: 'POST',
|
|
23
|
-
headers: { 'Content-Type': 'application/json' },
|
|
24
|
-
body: JSON.stringify({
|
|
25
|
-
contents: [{ parts: [{ text: prompt }] }],
|
|
26
|
-
generationConfig: { maxOutputTokens: maxTokens, temperature: 0 },
|
|
27
|
-
}),
|
|
28
|
-
}
|
|
29
|
-
);
|
|
30
|
-
if (response.status === 429) {
|
|
31
|
-
const retryAfter = parseInt(response.headers.get('retry-after') || '10');
|
|
32
|
-
console.log(` Rate limited, waiting ${retryAfter}s...`);
|
|
33
|
-
await new Promise(r => setTimeout(r, retryAfter * 1000));
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
|
-
if (!response.ok) {
|
|
37
|
-
console.log(` API error ${response.status}, retrying...`);
|
|
38
|
-
await new Promise(r => setTimeout(r, 2000));
|
|
39
|
-
continue;
|
|
40
|
-
}
|
|
41
|
-
const data = await response.json() as any;
|
|
42
|
-
return data.candidates?.[0]?.content?.parts?.[0]?.text ?? '';
|
|
43
|
-
} catch (e: any) {
|
|
44
|
-
console.log(` Fetch error: ${e.message}, retrying...`);
|
|
45
|
-
await new Promise(r => setTimeout(r, 2000));
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return '';
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
async function scoreAnswer(question: string, truth: string, answer: string): Promise<number> {
|
|
52
|
-
const prompt = `You are evaluating an AI's answer about a codebase. Score it from 0.0 to 1.0.
|
|
53
|
-
|
|
54
|
-
- 1.0 = Correct and complete
|
|
55
|
-
- 0.7 = Mostly correct, minor gaps
|
|
56
|
-
- 0.5 = Partially correct
|
|
57
|
-
- 0.3 = Mentions something relevant but mostly wrong
|
|
58
|
-
- 0.0 = Wrong or "I don't know"
|
|
59
|
-
|
|
60
|
-
Question: ${question}
|
|
61
|
-
Ground Truth: ${truth}
|
|
62
|
-
AI's Answer: ${answer}
|
|
63
|
-
|
|
64
|
-
Respond with ONLY a decimal number (e.g. 0.7). Nothing else.`;
|
|
65
|
-
|
|
66
|
-
const response = await geminiCall(prompt);
|
|
67
|
-
const cleaned = response.trim();
|
|
68
|
-
|
|
69
|
-
// Try direct float parse first
|
|
70
|
-
const direct = parseFloat(cleaned);
|
|
71
|
-
if (!isNaN(direct) && direct >= 0 && direct <= 1) return direct;
|
|
72
|
-
|
|
73
|
-
// Try regex
|
|
74
|
-
const match = cleaned.match(/(0\.\d+|1\.0|0|1)/);
|
|
75
|
-
if (match) return parseFloat(match[1]);
|
|
76
|
-
|
|
77
|
-
console.log(` Failed to parse score: "${cleaned}"`);
|
|
78
|
-
return -1; // Mark as failed, don't default to 0
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
async function main() {
|
|
82
|
-
const results = JSON.parse(readFileSync(RESULTS_PATH, 'utf8'));
|
|
83
|
-
console.log(`Rescoring ${results.length} results...\n`);
|
|
84
|
-
|
|
85
|
-
const systems = ['engram', 'cappedContext', 'naiveRag', 'grepSearch'] as const;
|
|
86
|
-
let totalScored = 0;
|
|
87
|
-
let totalFailed = 0;
|
|
88
|
-
|
|
89
|
-
for (let i = 0; i < results.length; i++) {
|
|
90
|
-
const r = results[i];
|
|
91
|
-
console.log(`[${i+1}/${results.length}] (${r.category}/${r.difficulty}) ${r.question.slice(0, 70)}...`);
|
|
92
|
-
|
|
93
|
-
const scores: Record<string, number> = {};
|
|
94
|
-
for (const sys of systems) {
|
|
95
|
-
if (!r[sys]?.answer) { scores[sys] = 0; continue; }
|
|
96
|
-
const score = await scoreAnswer(r.question, r.groundTruth, r[sys].answer);
|
|
97
|
-
if (score === -1) {
|
|
98
|
-
totalFailed++;
|
|
99
|
-
scores[sys] = 0;
|
|
100
|
-
} else {
|
|
101
|
-
scores[sys] = score;
|
|
102
|
-
}
|
|
103
|
-
r[sys].score = scores[sys];
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
totalScored += systems.length;
|
|
107
|
-
const line = systems.map(s => `${s[0].toUpperCase()}:${scores[s].toFixed(2)}`).join(' ');
|
|
108
|
-
console.log(` ${line}`);
|
|
109
|
-
|
|
110
|
-
// Save progress every 5 questions
|
|
111
|
-
if ((i + 1) % 5 === 0 || i === results.length - 1) {
|
|
112
|
-
writeFileSync(RESULTS_PATH.replace('.json', '-rescored2.json'), JSON.stringify(results, null, 2));
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
console.log(`\nScored: ${totalScored}, Failed parses: ${totalFailed}\n`);
|
|
117
|
-
|
|
118
|
-
// Generate report
|
|
119
|
-
const avg = (sys: string) => {
|
|
120
|
-
const vals = results.map((r: any) => r[sys]?.score ?? 0);
|
|
121
|
-
return vals.reduce((a: number, b: number) => a + b, 0) / vals.length;
|
|
122
|
-
};
|
|
123
|
-
const avgTokens = (sys: string) => {
|
|
124
|
-
const vals = results.map((r: any) => r[sys]?.tokensUsed ?? 0);
|
|
125
|
-
return Math.round(vals.reduce((a: number, b: number) => a + b, 0) / vals.length);
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
console.log('=== VS Code Codebase Evaluation Report ===\n');
|
|
129
|
-
console.log('OVERALL (50 questions)');
|
|
130
|
-
console.log(`${'System'.padEnd(20)} ${'Accuracy'.padEnd(12)} Avg Tokens`);
|
|
131
|
-
for (const sys of systems) {
|
|
132
|
-
const acc = (avg(sys) * 100).toFixed(1);
|
|
133
|
-
console.log(`${sys.padEnd(20)} ${(acc + '%').padEnd(12)} ${avgTokens(sys)}`);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Per category
|
|
137
|
-
const categories = [...new Set(results.map((r: any) => r.category))];
|
|
138
|
-
for (const cat of categories) {
|
|
139
|
-
const catResults = results.filter((r: any) => r.category === cat);
|
|
140
|
-
const catAvg = (sys: string) => {
|
|
141
|
-
const vals = catResults.map((r: any) => r[sys]?.score ?? 0);
|
|
142
|
-
return (vals.reduce((a: number, b: number) => a + b, 0) / vals.length * 100).toFixed(1);
|
|
143
|
-
};
|
|
144
|
-
console.log(`\n ${cat.toUpperCase()} (n=${catResults.length}): ${systems.map(s => `${s[0].toUpperCase()}:${catAvg(s)}%`).join(' ')}`);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Per difficulty
|
|
148
|
-
const diffs = [...new Set(results.map((r: any) => r.difficulty))];
|
|
149
|
-
for (const diff of diffs) {
|
|
150
|
-
const diffResults = results.filter((r: any) => r.difficulty === diff);
|
|
151
|
-
const diffAvg = (sys: string) => {
|
|
152
|
-
const vals = diffResults.map((r: any) => r[sys]?.score ?? 0);
|
|
153
|
-
return (vals.reduce((a: number, b: number) => a + b, 0) / vals.length * 100).toFixed(1);
|
|
154
|
-
};
|
|
155
|
-
console.log(`\n ${diff.toUpperCase()} (n=${diffResults.length}): ${systems.map(s => `${s[0].toUpperCase()}:${diffAvg(s)}%`).join(' ')}`);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const tokenSavings = (1 - avgTokens('engram') / avgTokens('cappedContext')) * 100;
|
|
159
|
-
console.log(`\n Token savings vs capped context: ${tokenSavings.toFixed(1)}%`);
|
|
160
|
-
|
|
161
|
-
// Save report
|
|
162
|
-
const report = {
|
|
163
|
-
timestamp: new Date().toISOString(),
|
|
164
|
-
totalQuestions: results.length,
|
|
165
|
-
failedParses: totalFailed,
|
|
166
|
-
overall: Object.fromEntries(systems.map(s => [s, {
|
|
167
|
-
accuracy: (avg(s) * 100).toFixed(1),
|
|
168
|
-
avgTokens: avgTokens(s),
|
|
169
|
-
}])),
|
|
170
|
-
byCategory: Object.fromEntries(categories.map(c => {
|
|
171
|
-
const cr = results.filter((r: any) => r.category === c);
|
|
172
|
-
return [c, Object.fromEntries(systems.map(s => [s, (cr.reduce((a: number, r: any) => a + (r[s]?.score ?? 0), 0) / cr.length * 100).toFixed(1)]))];
|
|
173
|
-
})),
|
|
174
|
-
byDifficulty: Object.fromEntries(diffs.map(d => {
|
|
175
|
-
const dr = results.filter((r: any) => r.difficulty === d);
|
|
176
|
-
return [d, Object.fromEntries(systems.map(s => [s, (dr.reduce((a: number, r: any) => a + (r[s]?.score ?? 0), 0) / dr.length * 100).toFixed(1)]))];
|
|
177
|
-
})),
|
|
178
|
-
tokenSavingsVsCapped: tokenSavings.toFixed(1) + '%',
|
|
179
|
-
};
|
|
180
|
-
writeFileSync(REPORT_PATH, JSON.stringify(report, null, 2));
|
|
181
|
-
console.log(`\nReport saved: ${REPORT_PATH}`);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
main().catch(console.error);
|
package/rescore-vscode.ts
DELETED
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env npx tsx
|
|
2
|
-
/**
|
|
3
|
-
* rescore-vscode.ts -- Re-score the VS Code codebase eval results
|
|
4
|
-
*
|
|
5
|
-
* The original eval generated good answers but the judge scoring returned
|
|
6
|
-
* unparseable responses (all 0s). This script re-runs ONLY the scoring step.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { readFileSync, writeFileSync } from 'fs';
|
|
10
|
-
import { homedir } from 'os';
|
|
11
|
-
import { join } from 'path';
|
|
12
|
-
|
|
13
|
-
const GEMINI_KEY = readFileSync(join(homedir(), '.config/engram/gemini-key'), 'utf8').trim();
|
|
14
|
-
const EVAL_DIR = join(homedir(), '.openclaw/workspace/engram/eval-scale-data');
|
|
15
|
-
const RESULTS_PATH = join(EVAL_DIR, 'codebase-results-vscode.json');
|
|
16
|
-
const RESCORED_PATH = join(EVAL_DIR, 'codebase-results-vscode-rescored.json');
|
|
17
|
-
const RATE_LIMIT_MS = 1500;
|
|
18
|
-
|
|
19
|
-
async function sleep(ms: number) { return new Promise(r => setTimeout(r, ms)); }
|
|
20
|
-
|
|
21
|
-
async function withRetry<T>(fn: () => Promise<T>, retries = 5): Promise<T> {
|
|
22
|
-
for (let i = 0; i < retries; i++) {
|
|
23
|
-
try {
|
|
24
|
-
return await fn();
|
|
25
|
-
} catch (err: any) {
|
|
26
|
-
if (err.message?.includes('429') && i < retries - 1) {
|
|
27
|
-
const backoff = Math.min(1500 * Math.pow(2, i + 1), 60000);
|
|
28
|
-
console.log(` [Retry ${i + 1}/${retries}] 429, waiting ${backoff}ms...`);
|
|
29
|
-
await sleep(backoff);
|
|
30
|
-
} else {
|
|
31
|
-
throw err;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
throw new Error('Exhausted retries');
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
async function geminiCall(prompt: string, maxTokens = 200): Promise<string> {
|
|
39
|
-
const res = await fetch(
|
|
40
|
-
`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${GEMINI_KEY}`,
|
|
41
|
-
{
|
|
42
|
-
method: 'POST',
|
|
43
|
-
headers: { 'Content-Type': 'application/json' },
|
|
44
|
-
body: JSON.stringify({
|
|
45
|
-
contents: [{ parts: [{ text: prompt }] }],
|
|
46
|
-
generationConfig: { maxOutputTokens: maxTokens, temperature: 0.0 },
|
|
47
|
-
}),
|
|
48
|
-
}
|
|
49
|
-
);
|
|
50
|
-
if (!res.ok) {
|
|
51
|
-
const body = await res.text();
|
|
52
|
-
throw new Error(`Gemini ${res.status}: ${body.slice(0, 200)}`);
|
|
53
|
-
}
|
|
54
|
-
const data = await res.json() as any;
|
|
55
|
-
return data.candidates?.[0]?.content?.parts?.[0]?.text?.trim() || '';
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async function scoreAnswer(question: string, groundTruth: string, answer: string): Promise<number> {
|
|
59
|
-
const prompt = `You are a strict code knowledge evaluator. Score this answer about the VS Code codebase.
|
|
60
|
-
|
|
61
|
-
Question: ${question}
|
|
62
|
-
|
|
63
|
-
Correct Answer: ${groundTruth}
|
|
64
|
-
|
|
65
|
-
Given Answer: ${answer}
|
|
66
|
-
|
|
67
|
-
Score from 0.0 to 1.0:
|
|
68
|
-
- 1.0 = Completely correct, mentions the right files/classes/functions
|
|
69
|
-
- 0.7 = Mostly correct, minor details missing
|
|
70
|
-
- 0.5 = Partially correct, gets the general area but misses specifics
|
|
71
|
-
- 0.3 = Vaguely related but mostly wrong
|
|
72
|
-
- 0.0 = Completely wrong or says "insufficient context"
|
|
73
|
-
|
|
74
|
-
Respond with ONLY a single number between 0.0 and 1.0. Nothing else.`;
|
|
75
|
-
|
|
76
|
-
const response = await withRetry(() => geminiCall(prompt, 10));
|
|
77
|
-
const score = parseFloat(response);
|
|
78
|
-
if (isNaN(score) || score < 0 || score > 1) {
|
|
79
|
-
console.log(` Warning: unparseable score "${response}", defaulting to 0`);
|
|
80
|
-
return 0;
|
|
81
|
-
}
|
|
82
|
-
return score;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
async function main() {
|
|
86
|
-
console.log('=== Re-scoring VS Code Codebase Eval ===\n');
|
|
87
|
-
|
|
88
|
-
const results = JSON.parse(readFileSync(RESULTS_PATH, 'utf8'));
|
|
89
|
-
console.log(`Loaded ${results.length} results\n`);
|
|
90
|
-
|
|
91
|
-
for (let i = 0; i < results.length; i++) {
|
|
92
|
-
const r = results[i];
|
|
93
|
-
console.log(`[${i + 1}/${results.length}] (${r.category}/${r.difficulty}) ${r.question.slice(0, 65)}...`);
|
|
94
|
-
|
|
95
|
-
// Score each system individually (more reliable than 4-at-once)
|
|
96
|
-
await sleep(RATE_LIMIT_MS);
|
|
97
|
-
r.engram.score = await scoreAnswer(r.question, r.groundTruth, r.engram.answer);
|
|
98
|
-
|
|
99
|
-
await sleep(RATE_LIMIT_MS);
|
|
100
|
-
r.cappedContext.score = await scoreAnswer(r.question, r.groundTruth, r.cappedContext.answer);
|
|
101
|
-
|
|
102
|
-
await sleep(RATE_LIMIT_MS);
|
|
103
|
-
r.naiveRag.score = await scoreAnswer(r.question, r.groundTruth, r.naiveRag.answer);
|
|
104
|
-
|
|
105
|
-
await sleep(RATE_LIMIT_MS);
|
|
106
|
-
r.grepSearch.score = await scoreAnswer(r.question, r.groundTruth, r.grepSearch.answer);
|
|
107
|
-
|
|
108
|
-
console.log(` E:${r.engram.score.toFixed(2)} C:${r.cappedContext.score.toFixed(2)} R:${r.naiveRag.score.toFixed(2)} G:${r.grepSearch.score.toFixed(2)}`);
|
|
109
|
-
|
|
110
|
-
// Save every 5
|
|
111
|
-
if ((i + 1) % 5 === 0) {
|
|
112
|
-
writeFileSync(RESCORED_PATH, JSON.stringify(results, null, 2));
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
writeFileSync(RESCORED_PATH, JSON.stringify(results, null, 2));
|
|
117
|
-
console.log(`\nSaved to ${RESCORED_PATH}`);
|
|
118
|
-
|
|
119
|
-
// Print summary
|
|
120
|
-
const n = results.length;
|
|
121
|
-
const avg = (key: string) => (results.reduce((s: number, r: any) => s + r[key].score, 0) / n * 100).toFixed(1);
|
|
122
|
-
const avgTok = (key: string) => Math.round(results.reduce((s: number, r: any) => s + r[key].tokensUsed, 0) / n);
|
|
123
|
-
|
|
124
|
-
console.log(`\n=== RESULTS (${n} questions) ===`);
|
|
125
|
-
console.log(`Engram: ${avg('engram')}% (${avgTok('engram')} tok/q)`);
|
|
126
|
-
console.log(`Capped Context: ${avg('cappedContext')}% (${avgTok('cappedContext')} tok/q)`);
|
|
127
|
-
console.log(`Naive RAG: ${avg('naiveRag')}% (${avgTok('naiveRag')} tok/q)`);
|
|
128
|
-
console.log(`Grep Search: ${avg('grepSearch')}% (${avgTok('grepSearch')} tok/q)`);
|
|
129
|
-
|
|
130
|
-
// By category
|
|
131
|
-
const cats = [...new Set(results.map((r: any) => r.category))];
|
|
132
|
-
for (const cat of cats) {
|
|
133
|
-
const cr = results.filter((r: any) => r.category === cat);
|
|
134
|
-
const catAvg = (key: string) => (cr.reduce((s: number, r: any) => s + r[key].score, 0) / cr.length * 100).toFixed(1);
|
|
135
|
-
console.log(`\n${(cat as string).toUpperCase()} (n=${cr.length}): E:${catAvg('engram')}% C:${catAvg('cappedContext')}% R:${catAvg('naiveRag')}% G:${catAvg('grepSearch')}%`);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
main().catch(err => {
|
|
140
|
-
console.error('Fatal:', err);
|
|
141
|
-
process.exit(1);
|
|
142
|
-
});
|
package/signal-quality-plan.md
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
# Signal Quality Plan — More Memories, Better Organization
|
|
2
|
-
|
|
3
|
-
Philosophy: More memories is GOOD. The graph handles scale. The problem isn't quantity — it's classification accuracy in the features that scan broadly (alerts, briefing, consolidation).
|
|
4
|
-
|
|
5
|
-
## Fix 1: Smarter Status Classification at Intake
|
|
6
|
-
**Problem:** Auto-ingest marks too many things as "pending" — descriptions of future plans in conversation get flagged as commitments even when they're just discussed possibilities.
|
|
7
|
-
**Fix:** Tighten the prompt to distinguish "committed to doing X" from "discussed doing X". Add a post-intake validation in remember() that checks if the content actually represents a real commitment.
|
|
8
|
-
|
|
9
|
-
## Fix 2: Salience Calibration
|
|
10
|
-
**Problem:** 84% of memories have salience ≥ 0.6, making the signal useless for differentiation.
|
|
11
|
-
**Fix:** Recalibrate extract.ts salience estimation. Most memories should be 0.3-0.5 (normal), with only truly important ones (decisions, preferences, corrections) at 0.7+.
|
|
12
|
-
|
|
13
|
-
## Fix 3: Briefing as MEMORY.md Replacement
|
|
14
|
-
**Problem:** Briefing dumps a flat list of facts. MEMORY.md is curated with sections and narrative. Agents prefer MEMORY.md.
|
|
15
|
-
**Fix:** Make briefing() cluster memories by entity/topic and present them as organized sections. Add a "what changed since last session" section.
|
|
16
|
-
|
|
17
|
-
## Fix 4: Alerts Precision
|
|
18
|
-
**Problem:** 238 pending items, most are completed tasks or discussed-but-not-committed plans. False positive rate is too high.
|
|
19
|
-
**Fix:** Add a "fulfilled" detection sweep — if a pending memory's content matches a later completed action, auto-mark it fulfilled. Also add confidence threshold for pending alerts.
|
|
20
|
-
|
|
21
|
-
## Fix 5: Consolidation Quality Gate
|
|
22
|
-
**Problem:** Consolidation processes all episodes including low-value ones, wasting LLM tokens.
|
|
23
|
-
**Fix:** Filter episodes by salience before consolidation. Only consolidate episodes with salience ≥ 0.3.
|