clementine-agent 1.1.6 → 1.1.8
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/agent/assistant.d.ts +1 -1
- package/dist/agent/assistant.js +2 -2
- package/dist/agent/insight-engine.d.ts +13 -1
- package/dist/agent/insight-engine.js +110 -1
- package/dist/gateway/fix-applier.js +21 -0
- package/dist/gateway/fix-verification.d.ts +43 -3
- package/dist/gateway/fix-verification.js +115 -9
- package/dist/gateway/router.d.ts +1 -1
- package/dist/gateway/router.js +2 -2
- package/dist/memory/store.js +11 -0
- package/package.json +1 -1
|
@@ -271,7 +271,7 @@ export declare class PersonalAssistant {
|
|
|
271
271
|
* so follow-up conversation has context.
|
|
272
272
|
*/
|
|
273
273
|
injectContext(sessionKey: string, userText: string, assistantText: string): void;
|
|
274
|
-
getRecentActivity(sinceIso: string): Array<{
|
|
274
|
+
getRecentActivity(sinceIso: string, maxEntries?: number): Array<{
|
|
275
275
|
sessionKey: string;
|
|
276
276
|
role: string;
|
|
277
277
|
content: string;
|
package/dist/agent/assistant.js
CHANGED
|
@@ -4828,11 +4828,11 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
4828
4828
|
}
|
|
4829
4829
|
}
|
|
4830
4830
|
}
|
|
4831
|
-
getRecentActivity(sinceIso) {
|
|
4831
|
+
getRecentActivity(sinceIso, maxEntries) {
|
|
4832
4832
|
if (!this.memoryStore)
|
|
4833
4833
|
return [];
|
|
4834
4834
|
try {
|
|
4835
|
-
return this.memoryStore.getRecentActivity(sinceIso);
|
|
4835
|
+
return this.memoryStore.getRecentActivity(sinceIso, maxEntries);
|
|
4836
4836
|
}
|
|
4837
4837
|
catch {
|
|
4838
4838
|
return [];
|
|
@@ -47,13 +47,25 @@ export declare function maybeIncreaseCooldown(state: InsightState): void;
|
|
|
47
47
|
* Returns structured event summaries that can be passed to an LLM for urgency rating.
|
|
48
48
|
*/
|
|
49
49
|
export declare function gatherInsightSignals(gateway: {
|
|
50
|
-
getRecentActivity: (since: string) => Array<{
|
|
50
|
+
getRecentActivity: (since: string, maxEntries?: number) => Array<{
|
|
51
51
|
sessionKey: string;
|
|
52
52
|
role: string;
|
|
53
53
|
content: string;
|
|
54
54
|
createdAt: string;
|
|
55
55
|
}>;
|
|
56
56
|
}): string[];
|
|
57
|
+
export declare function detectFrustrationSignals(activity: Array<{
|
|
58
|
+
sessionKey: string;
|
|
59
|
+
role: string;
|
|
60
|
+
content: string;
|
|
61
|
+
createdAt: string;
|
|
62
|
+
}>): string[];
|
|
63
|
+
export declare function detectRepeatedTopics(activity: Array<{
|
|
64
|
+
sessionKey: string;
|
|
65
|
+
role: string;
|
|
66
|
+
content: string;
|
|
67
|
+
createdAt: string;
|
|
68
|
+
}>): string[];
|
|
57
69
|
/**
|
|
58
70
|
* Build a prompt for urgency rating (to be sent to a lightweight LLM).
|
|
59
71
|
* Returns null if there are no signals worth evaluating.
|
|
@@ -189,7 +189,27 @@ export function gatherInsightSignals(gateway) {
|
|
|
189
189
|
catch (err) {
|
|
190
190
|
logger.debug({ err }, 'Failed to pull broken-jobs signals');
|
|
191
191
|
}
|
|
192
|
-
// 6.
|
|
192
|
+
// 6. Conversational signals derived from recent transcripts.
|
|
193
|
+
// Surfaces patterns IN the conversation itself, not just system events:
|
|
194
|
+
// user frustration markers, repeating topics, etc. These are early
|
|
195
|
+
// warning signs that the agent's responses may be off-track.
|
|
196
|
+
try {
|
|
197
|
+
const since24h = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
|
|
198
|
+
const since7d = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
|
|
199
|
+
// 24h frustration scan — 50 entries plenty to count corrections in a day.
|
|
200
|
+
const recent = gateway.getRecentActivity(since24h, 50);
|
|
201
|
+
for (const s of detectFrustrationSignals(recent))
|
|
202
|
+
signals.push(s);
|
|
203
|
+
// 7d repeat-topic scan — pull more entries since topics span sessions.
|
|
204
|
+
// Cap at 200 to keep keyword extraction cheap.
|
|
205
|
+
const week = gateway.getRecentActivity(since7d, 200);
|
|
206
|
+
for (const s of detectRepeatedTopics(week))
|
|
207
|
+
signals.push(s);
|
|
208
|
+
}
|
|
209
|
+
catch (err) {
|
|
210
|
+
logger.debug({ err }, 'Failed to pull conversational signals');
|
|
211
|
+
}
|
|
212
|
+
// 7. Claim tracker — failed claims in the last N hours erode trust.
|
|
193
213
|
// Surface them so the owner sees "Clementine said she'd do X; she
|
|
194
214
|
// didn't" instead of silently swallowing the miss.
|
|
195
215
|
try {
|
|
@@ -214,6 +234,95 @@ export function gatherInsightSignals(gateway) {
|
|
|
214
234
|
}
|
|
215
235
|
return signals;
|
|
216
236
|
}
|
|
237
|
+
// ── Conversational signal detectors ─────────────────────────────────
|
|
238
|
+
//
|
|
239
|
+
// Pure functions over recent transcript activity. Exported so the insight
|
|
240
|
+
// dashboard / debug commands can run them independently of the full
|
|
241
|
+
// gatherInsightSignals path.
|
|
242
|
+
/**
|
|
243
|
+
* Markers that suggest the user is correcting or frustrated with the
|
|
244
|
+
* agent's last response. Tuned to start-of-message tokens since
|
|
245
|
+
* mid-message "no" or "actually" is often just normal narrative.
|
|
246
|
+
*/
|
|
247
|
+
const CORRECTION_PATTERNS = [
|
|
248
|
+
/^(no|nope|not\b)/i,
|
|
249
|
+
/^(actually|wait)\b/i,
|
|
250
|
+
/^(that['’]?s| that is) (wrong|not|incorrect|backwards|opposite)/i,
|
|
251
|
+
/^I (meant|said|wanted|asked)\b/i,
|
|
252
|
+
/^you (didn['’]?t|misunderstood|got it wrong|missed)/i,
|
|
253
|
+
/^(stop|cancel|undo|nevermind|never mind)\b/i,
|
|
254
|
+
];
|
|
255
|
+
export function detectFrustrationSignals(activity) {
|
|
256
|
+
const signals = [];
|
|
257
|
+
let count = 0;
|
|
258
|
+
const sessionsAffected = new Set();
|
|
259
|
+
for (const entry of activity) {
|
|
260
|
+
if (entry.role !== 'user')
|
|
261
|
+
continue;
|
|
262
|
+
const trimmed = entry.content.trim();
|
|
263
|
+
for (const re of CORRECTION_PATTERNS) {
|
|
264
|
+
if (re.test(trimmed)) {
|
|
265
|
+
count++;
|
|
266
|
+
sessionsAffected.add(entry.sessionKey);
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (count >= 3) {
|
|
272
|
+
signals.push(`Conversation friction: ${count} user correction(s) across ${sessionsAffected.size} session(s) in the last 24h — recent agent responses may be off-track`);
|
|
273
|
+
}
|
|
274
|
+
return signals;
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Words too generic to count as a topic — would otherwise dominate the
|
|
278
|
+
* "recurring topic" signal with noise like "thanks", "okay", "please".
|
|
279
|
+
*/
|
|
280
|
+
const TOPIC_STOPWORDS = new Set([
|
|
281
|
+
'about', 'after', 'again', 'against', 'because', 'before', 'being', 'between',
|
|
282
|
+
'could', 'doing', 'don’t', 'down', 'during', 'each', 'from', 'further',
|
|
283
|
+
'going', 'gonna', 'have', 'having', 'here', 'into', 'just', 'know', 'like',
|
|
284
|
+
'maybe', 'might', 'more', 'most', 'much', 'need', 'okay', 'only', 'other',
|
|
285
|
+
'over', 'please', 'really', 'said', 'same', 'some', 'still', 'such', 'than',
|
|
286
|
+
'that', 'them', 'then', 'there', 'these', 'they', 'thing', 'think', 'this',
|
|
287
|
+
'those', 'through', 'thanks', 'time', 'told', 'under', 'until', 'using', 'very',
|
|
288
|
+
'want', 'wanted', 'wants', 'were', 'what', 'when', 'where', 'which', 'while',
|
|
289
|
+
'will', 'with', 'would', 'your', 'yours', 'yeah', 'yes',
|
|
290
|
+
'tonight', 'today', 'tomorrow', 'morning', 'evening', 'session', 'work',
|
|
291
|
+
'doing', 'made', 'make', 'making', 'sure', 'right', 'wrong', 'good', 'bad',
|
|
292
|
+
'much', 'many', 'lots',
|
|
293
|
+
]);
|
|
294
|
+
export function detectRepeatedTopics(activity) {
|
|
295
|
+
// Build a (keyword → set of session IDs) map. A keyword that shows up in
|
|
296
|
+
// 3+ DISTINCT sessions across the window is "recurring" — could be an
|
|
297
|
+
// unresolved thread, a project the user is grinding on, or a question
|
|
298
|
+
// they've asked multiple ways.
|
|
299
|
+
const sessionsForKeyword = new Map();
|
|
300
|
+
for (const entry of activity) {
|
|
301
|
+
if (entry.role !== 'user')
|
|
302
|
+
continue;
|
|
303
|
+
const text = entry.content.toLowerCase();
|
|
304
|
+
// Word extraction: 5+ chars, alpha-only (no numbers/punctuation).
|
|
305
|
+
const matches = text.match(/[a-z][a-z’]{4,15}/g) ?? [];
|
|
306
|
+
const seenInThisMessage = new Set();
|
|
307
|
+
for (const w of matches) {
|
|
308
|
+
if (TOPIC_STOPWORDS.has(w))
|
|
309
|
+
continue;
|
|
310
|
+
if (seenInThisMessage.has(w))
|
|
311
|
+
continue; // dedupe within a single message
|
|
312
|
+
seenInThisMessage.add(w);
|
|
313
|
+
if (!sessionsForKeyword.has(w))
|
|
314
|
+
sessionsForKeyword.set(w, new Set());
|
|
315
|
+
sessionsForKeyword.get(w).add(entry.sessionKey);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
// Rank by session-spread; surface the top 2 to avoid flooding insight
|
|
319
|
+
// notifications with too many topic mentions.
|
|
320
|
+
const ranked = [...sessionsForKeyword.entries()]
|
|
321
|
+
.filter(([, sessions]) => sessions.size >= 3)
|
|
322
|
+
.sort((a, b) => b[1].size - a[1].size)
|
|
323
|
+
.slice(0, 2);
|
|
324
|
+
return ranked.map(([keyword, sessions]) => `Recurring topic "${keyword}" came up across ${sessions.size} sessions this week — possible ongoing thread`);
|
|
325
|
+
}
|
|
217
326
|
/**
|
|
218
327
|
* Build a prompt for urgency rating (to be sent to a lightweight LLM).
|
|
219
328
|
* Returns null if there are no signals worth evaluating.
|
|
@@ -358,6 +358,18 @@ function applyAdvisorRuleFix(jobName, autoApply, opts) {
|
|
|
358
358
|
}
|
|
359
359
|
appendAudit({ kind: 'advisor-rule', jobName, file: targetPath, ruleId: autoApply.ruleId, diff });
|
|
360
360
|
logger.info({ jobName, ruleId: autoApply.ruleId, file: targetPath }, 'Applied advisor-rule fix');
|
|
361
|
+
// Phase 8.1 — record this autoApply for verification. The next
|
|
362
|
+
// AUTOAPPLY_VERDICT_WINDOW non-skipped runs decide whether the rule
|
|
363
|
+
// helped; if not, fix-verification auto-reverts (deletes the file).
|
|
364
|
+
// Lazy import to avoid circular dependency (fix-verification imports
|
|
365
|
+
// failure-monitor which transitively touches the cron path).
|
|
366
|
+
import('./fix-verification.js').then(({ recordAutoApplyForVerification }) => {
|
|
367
|
+
recordAutoApplyForVerification(jobName, {
|
|
368
|
+
kind: 'advisor-rule',
|
|
369
|
+
file: targetPath,
|
|
370
|
+
ruleId: autoApply.ruleId,
|
|
371
|
+
});
|
|
372
|
+
}).catch(err => logger.warn({ err, jobName }, 'Failed to record autoApply for verification'));
|
|
361
373
|
return {
|
|
362
374
|
ok: true,
|
|
363
375
|
message: `Wrote advisor rule ${autoApply.ruleId} (hot-reloads on next eval)`,
|
|
@@ -408,6 +420,15 @@ function applyPromptOverrideFix(jobName, autoApply, opts) {
|
|
|
408
420
|
diff,
|
|
409
421
|
});
|
|
410
422
|
logger.info({ jobName, scope: autoApply.scope, scopeKey: autoApply.scopeKey, file: targetPath }, 'Applied prompt-override fix');
|
|
423
|
+
// Phase 8.1 — same multi-run verification flow as advisor-rule.
|
|
424
|
+
import('./fix-verification.js').then(({ recordAutoApplyForVerification }) => {
|
|
425
|
+
recordAutoApplyForVerification(jobName, {
|
|
426
|
+
kind: 'prompt-override',
|
|
427
|
+
file: targetPath,
|
|
428
|
+
scope: autoApply.scope,
|
|
429
|
+
scopeKey: autoApply.scopeKey,
|
|
430
|
+
});
|
|
431
|
+
}).catch(err => logger.warn({ err, jobName }, 'Failed to record autoApply for verification'));
|
|
411
432
|
return {
|
|
412
433
|
ok: true,
|
|
413
434
|
message: `Wrote prompt override ${autoApply.scope}${autoApply.scopeKey ? `:${autoApply.scopeKey}` : ''}`,
|
|
@@ -13,6 +13,29 @@ interface PendingVerification {
|
|
|
13
13
|
recordedAt: string;
|
|
14
14
|
preFailureCount: number;
|
|
15
15
|
preLastError: string | null;
|
|
16
|
+
/** Used by Phase 8.1 — when set, the verifier is also responsible for
|
|
17
|
+
* deleting this artifact if the fix doesn't help. Existing CRON.md edits
|
|
18
|
+
* leave this unset (they're hand-edits, not auto-applies, so we never
|
|
19
|
+
* revert them automatically). */
|
|
20
|
+
autoApply?: AutoApplyTracker;
|
|
21
|
+
/** Run-by-run history accumulated since the fix was applied. Single-run
|
|
22
|
+
* verdicts (the original CRON.md flow) only need the first entry; multi-
|
|
23
|
+
* run autoApply verifications need the accumulated sample. */
|
|
24
|
+
postRunOutcomes?: Array<'ok' | 'error' | 'retried'>;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Tracks an autoApply that's currently being verified. When the verdict
|
|
28
|
+
* window closes negatively, revertFix() uses these fields to undo.
|
|
29
|
+
*/
|
|
30
|
+
export interface AutoApplyTracker {
|
|
31
|
+
kind: 'advisor-rule' | 'prompt-override';
|
|
32
|
+
/** Absolute path of the file the apply wrote. */
|
|
33
|
+
file: string;
|
|
34
|
+
/** advisor-rule only: the rule's id, used by the loader's hot-reload. */
|
|
35
|
+
ruleId?: string;
|
|
36
|
+
/** prompt-override only: scope label for the verdict message. */
|
|
37
|
+
scope?: 'global' | 'agent' | 'job';
|
|
38
|
+
scopeKey?: string;
|
|
16
39
|
}
|
|
17
40
|
/**
|
|
18
41
|
* Compare an old and new jobs list and record verifications for any job that:
|
|
@@ -25,15 +48,32 @@ interface PendingVerification {
|
|
|
25
48
|
* pending verification" rather than waiting for a run that will never come.
|
|
26
49
|
*/
|
|
27
50
|
export declare function recordEditsForFailingJobs(oldJobs: CronJobDefinition[], newJobs: CronJobDefinition[]): void;
|
|
51
|
+
/**
|
|
52
|
+
* Phase 8.1 — record a pending verification for an autoApply (advisor-rule
|
|
53
|
+
* or prompt-override) so the verifier can roll the fix back if the next
|
|
54
|
+
* AUTOAPPLY_VERDICT_WINDOW runs don't show improvement.
|
|
55
|
+
*
|
|
56
|
+
* Called from fix-applier.applyFix on success. Idempotent: if a previous
|
|
57
|
+
* verification for the same job is still pending, the new tracker overwrites
|
|
58
|
+
* it (the most-recent fix is the one we're verifying).
|
|
59
|
+
*/
|
|
60
|
+
export declare function recordAutoApplyForVerification(jobName: string, tracker: AutoApplyTracker): void;
|
|
28
61
|
/**
|
|
29
62
|
* After a cron run completes, check whether we were waiting on a fix
|
|
30
|
-
* verification for this job.
|
|
63
|
+
* verification for this job. Two flows:
|
|
64
|
+
*
|
|
65
|
+
* 1. Hand-edit (CRON.md) — verdict on the FIRST non-skipped run. Original
|
|
66
|
+
* Phase 7 behavior, preserved.
|
|
67
|
+
* 2. AutoApply (advisor-rule / prompt-override) — accumulate up to
|
|
68
|
+
* AUTOAPPLY_VERDICT_WINDOW outcomes, then decide. If 0 successes,
|
|
69
|
+
* revert the file. Either way, DM the verdict.
|
|
31
70
|
*
|
|
32
|
-
* Skipped runs
|
|
33
|
-
* and shouldn't count as a verdict either way.
|
|
71
|
+
* Skipped runs don't carry signal and don't advance the window in either flow.
|
|
34
72
|
*/
|
|
35
73
|
export declare function checkAndDeliverVerification(entry: CronRunEntry, send: (text: string) => Promise<unknown>): Promise<void>;
|
|
36
74
|
/** Read-only accessor for dashboards or debugging. */
|
|
37
75
|
export declare function listPendingVerifications(): PendingVerification[];
|
|
76
|
+
/** Test helper — clear all state. */
|
|
77
|
+
export declare function _resetVerificationState(): void;
|
|
38
78
|
export {};
|
|
39
79
|
//# sourceMappingURL=fix-verification.d.ts.map
|
|
@@ -15,6 +15,13 @@ import { BASE_DIR } from '../config.js';
|
|
|
15
15
|
import { computeBrokenJobs } from './failure-monitor.js';
|
|
16
16
|
const logger = pino({ name: 'clementine.fix-verification' });
|
|
17
17
|
const STATE_FILE = path.join(BASE_DIR, 'cron', 'fix-verifications.json');
|
|
18
|
+
/**
|
|
19
|
+
* Number of post-fix runs we accumulate before deciding an autoApply
|
|
20
|
+
* verdict. Single sample is too noisy; ten is too patient. Three is
|
|
21
|
+
* a tight window: 0/3 successes after a "fix" is overwhelming evidence
|
|
22
|
+
* the fix didn't help.
|
|
23
|
+
*/
|
|
24
|
+
const AUTOAPPLY_VERDICT_WINDOW = 3;
|
|
18
25
|
function loadState() {
|
|
19
26
|
try {
|
|
20
27
|
if (!existsSync(STATE_FILE))
|
|
@@ -109,12 +116,62 @@ export function recordEditsForFailingJobs(oldJobs, newJobs) {
|
|
|
109
116
|
if (mutated)
|
|
110
117
|
saveState(state);
|
|
111
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Phase 8.1 — record a pending verification for an autoApply (advisor-rule
|
|
121
|
+
* or prompt-override) so the verifier can roll the fix back if the next
|
|
122
|
+
* AUTOAPPLY_VERDICT_WINDOW runs don't show improvement.
|
|
123
|
+
*
|
|
124
|
+
* Called from fix-applier.applyFix on success. Idempotent: if a previous
|
|
125
|
+
* verification for the same job is still pending, the new tracker overwrites
|
|
126
|
+
* it (the most-recent fix is the one we're verifying).
|
|
127
|
+
*/
|
|
128
|
+
export function recordAutoApplyForVerification(jobName, tracker) {
|
|
129
|
+
const state = loadState();
|
|
130
|
+
const broken = computeBrokenJobs();
|
|
131
|
+
const b = broken.find(x => x.jobName === jobName);
|
|
132
|
+
state.pending[jobName] = {
|
|
133
|
+
jobName,
|
|
134
|
+
recordedAt: new Date().toISOString(),
|
|
135
|
+
preFailureCount: b?.errorCount48h ?? 0,
|
|
136
|
+
preLastError: b?.lastErrors[0] ?? null,
|
|
137
|
+
autoApply: tracker,
|
|
138
|
+
postRunOutcomes: [],
|
|
139
|
+
};
|
|
140
|
+
saveState(state);
|
|
141
|
+
logger.info({ job: jobName, kind: tracker.kind, file: tracker.file }, 'Recorded autoApply for verification — will track next runs');
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Undo an autoApply by deleting the file the apply wrote. Best-effort:
|
|
145
|
+
* a missing file is not an error (might have been hand-deleted). Returns
|
|
146
|
+
* true if a file was actually removed.
|
|
147
|
+
*/
|
|
148
|
+
function revertAutoApply(tracker) {
|
|
149
|
+
try {
|
|
150
|
+
if (existsSync(tracker.file)) {
|
|
151
|
+
// Use unlinkSync from fs — kept dynamic to avoid a top-of-file import
|
|
152
|
+
// we don't otherwise need.
|
|
153
|
+
const { unlinkSync } = require('node:fs');
|
|
154
|
+
unlinkSync(tracker.file);
|
|
155
|
+
logger.warn({ file: tracker.file, kind: tracker.kind }, 'Reverted autoApply — fix did not help');
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
logger.warn({ err, file: tracker.file }, 'Failed to delete autoApply file during revert');
|
|
161
|
+
}
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
112
164
|
/**
|
|
113
165
|
* After a cron run completes, check whether we were waiting on a fix
|
|
114
|
-
* verification for this job.
|
|
166
|
+
* verification for this job. Two flows:
|
|
167
|
+
*
|
|
168
|
+
* 1. Hand-edit (CRON.md) — verdict on the FIRST non-skipped run. Original
|
|
169
|
+
* Phase 7 behavior, preserved.
|
|
170
|
+
* 2. AutoApply (advisor-rule / prompt-override) — accumulate up to
|
|
171
|
+
* AUTOAPPLY_VERDICT_WINDOW outcomes, then decide. If 0 successes,
|
|
172
|
+
* revert the file. Either way, DM the verdict.
|
|
115
173
|
*
|
|
116
|
-
* Skipped runs
|
|
117
|
-
* and shouldn't count as a verdict either way.
|
|
174
|
+
* Skipped runs don't carry signal and don't advance the window in either flow.
|
|
118
175
|
*/
|
|
119
176
|
export async function checkAndDeliverVerification(entry, send) {
|
|
120
177
|
if (entry.status === 'skipped')
|
|
@@ -123,15 +180,60 @@ export async function checkAndDeliverVerification(entry, send) {
|
|
|
123
180
|
const pending = state.pending[entry.jobName];
|
|
124
181
|
if (!pending)
|
|
125
182
|
return;
|
|
183
|
+
// Hand-edit flow — single-run verdict, unchanged.
|
|
184
|
+
if (!pending.autoApply) {
|
|
185
|
+
delete state.pending[entry.jobName];
|
|
186
|
+
saveState(state);
|
|
187
|
+
const ok = entry.status === 'ok';
|
|
188
|
+
const verdict = ok ? '✅ succeeded' : '⚠️ still failing';
|
|
189
|
+
const ageMin = Math.max(1, Math.round((Date.now() - Date.parse(pending.recordedAt)) / 60000));
|
|
190
|
+
const detail = ok ? '' : `\nError: ${(entry.error ?? 'unknown').split('\n')[0].slice(0, 200)}`;
|
|
191
|
+
const msg = `**[Fix verification]** \`${entry.jobName}\` ${verdict} on its first run after edit (${ageMin}m later).${detail}`;
|
|
192
|
+
try {
|
|
193
|
+
await send(msg);
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
logger.warn({ err, job: entry.jobName }, 'Failed to send fix verification DM');
|
|
197
|
+
}
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
// AutoApply flow — accumulate the sample first.
|
|
201
|
+
const outcomes = pending.postRunOutcomes ?? [];
|
|
202
|
+
outcomes.push(entry.status);
|
|
203
|
+
pending.postRunOutcomes = outcomes;
|
|
204
|
+
if (outcomes.length < AUTOAPPLY_VERDICT_WINDOW) {
|
|
205
|
+
// Not enough sample yet — persist accumulated state, wait for more runs.
|
|
206
|
+
saveState(state);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
// Decision time.
|
|
126
210
|
delete state.pending[entry.jobName];
|
|
127
211
|
saveState(state);
|
|
128
|
-
const
|
|
129
|
-
const verdict = ok ? '✅ succeeded' : '⚠️ still failing';
|
|
212
|
+
const successes = outcomes.filter(o => o === 'ok').length;
|
|
130
213
|
const ageMin = Math.max(1, Math.round((Date.now() - Date.parse(pending.recordedAt)) / 60000));
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
214
|
+
const tracker = pending.autoApply;
|
|
215
|
+
const scopeLabel = tracker.scope
|
|
216
|
+
? `${tracker.kind}:${tracker.scope}${tracker.scopeKey ? `:${tracker.scopeKey}` : ''}`
|
|
217
|
+
: `${tracker.kind}${tracker.ruleId ? `:${tracker.ruleId}` : ''}`;
|
|
218
|
+
if (successes === 0) {
|
|
219
|
+
// Fix didn't help — revert and notify.
|
|
220
|
+
const reverted = revertAutoApply(tracker);
|
|
221
|
+
const msg = `**[Fix verification — REVERTED]** \`${entry.jobName}\`: ` +
|
|
222
|
+
`auto-applied ${scopeLabel} did not help (0/${outcomes.length} runs succeeded over ${ageMin}m). ` +
|
|
223
|
+
(reverted ? `Reverted ${path.basename(tracker.file)}.` : `Tried to revert but file was already gone.`);
|
|
224
|
+
try {
|
|
225
|
+
await send(msg);
|
|
226
|
+
}
|
|
227
|
+
catch (err) {
|
|
228
|
+
logger.warn({ err, job: entry.jobName }, 'Failed to send fix-revert DM');
|
|
229
|
+
}
|
|
230
|
+
logger.warn({ job: entry.jobName, scopeLabel, reverted }, 'Auto-reverted ineffective autoApply');
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const verdict = successes === outcomes.length
|
|
234
|
+
? `✅ verified — ${successes}/${outcomes.length} runs succeeded`
|
|
235
|
+
: `⚠️ partial — ${successes}/${outcomes.length} runs succeeded`;
|
|
236
|
+
const msg = `**[Fix verification]** \`${entry.jobName}\`: auto-applied ${scopeLabel} ${verdict} over ${ageMin}m.`;
|
|
135
237
|
try {
|
|
136
238
|
await send(msg);
|
|
137
239
|
}
|
|
@@ -143,4 +245,8 @@ export async function checkAndDeliverVerification(entry, send) {
|
|
|
143
245
|
export function listPendingVerifications() {
|
|
144
246
|
return Object.values(loadState().pending);
|
|
145
247
|
}
|
|
248
|
+
/** Test helper — clear all state. */
|
|
249
|
+
export function _resetVerificationState() {
|
|
250
|
+
saveState({ pending: {} });
|
|
251
|
+
}
|
|
146
252
|
//# sourceMappingURL=fix-verification.js.map
|
package/dist/gateway/router.d.ts
CHANGED
|
@@ -170,7 +170,7 @@ export declare class Gateway {
|
|
|
170
170
|
* Get recent transcript activity across all sessions.
|
|
171
171
|
* Used by heartbeat to know what happened since the last check.
|
|
172
172
|
*/
|
|
173
|
-
getRecentActivity(sinceIso: string): Array<{
|
|
173
|
+
getRecentActivity(sinceIso: string, maxEntries?: number): Array<{
|
|
174
174
|
sessionKey: string;
|
|
175
175
|
role: string;
|
|
176
176
|
content: string;
|
package/dist/gateway/router.js
CHANGED
|
@@ -1447,8 +1447,8 @@ export class Gateway {
|
|
|
1447
1447
|
* Get recent transcript activity across all sessions.
|
|
1448
1448
|
* Used by heartbeat to know what happened since the last check.
|
|
1449
1449
|
*/
|
|
1450
|
-
getRecentActivity(sinceIso) {
|
|
1451
|
-
return this.assistant.getRecentActivity(sinceIso);
|
|
1450
|
+
getRecentActivity(sinceIso, maxEntries) {
|
|
1451
|
+
return this.assistant.getRecentActivity(sinceIso, maxEntries);
|
|
1452
1452
|
}
|
|
1453
1453
|
/**
|
|
1454
1454
|
* Search memory (FTS5) for context relevant to a query.
|
package/dist/memory/store.js
CHANGED
|
@@ -1094,6 +1094,7 @@ export class MemoryStore {
|
|
|
1094
1094
|
}
|
|
1095
1095
|
const rows = this.conn.prepare(sql).all(...params);
|
|
1096
1096
|
const scored = [];
|
|
1097
|
+
const nowMs = Date.now();
|
|
1097
1098
|
for (const row of rows) {
|
|
1098
1099
|
try {
|
|
1099
1100
|
const vec = embeddingsModule.deserializeEmbedding(row.embedding);
|
|
@@ -1109,6 +1110,16 @@ export class MemoryStore {
|
|
|
1109
1110
|
// Soft isolation: apply boost (only when not strict)
|
|
1110
1111
|
if (!strict && agentSlug && row.agent_slug === agentSlug)
|
|
1111
1112
|
score *= 1.4;
|
|
1113
|
+
// Temporal decay — same policy as FTS scoring (Phase 9d). Without
|
|
1114
|
+
// this, vector and FTS rankings disagree on freshness: FTS prefers
|
|
1115
|
+
// recent at equal relevance but vector treats all timestamps
|
|
1116
|
+
// equally, so MMR rerank surfaces stale matches when vector wins.
|
|
1117
|
+
// Same 30-day half-life, same 0.4 floor — see store.ts FTS path
|
|
1118
|
+
// for design rationale.
|
|
1119
|
+
if (row.updated_at) {
|
|
1120
|
+
const daysOld = Math.max(0, (nowMs - new Date(row.updated_at).getTime()) / 86_400_000);
|
|
1121
|
+
score *= Math.max(0.4, temporalDecay(daysOld, 30));
|
|
1122
|
+
}
|
|
1112
1123
|
scored.push({
|
|
1113
1124
|
sourceFile: row.source_file,
|
|
1114
1125
|
section: row.section,
|