clementine-agent 1.0.92 → 1.0.93
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.
|
@@ -51,12 +51,21 @@ export interface SelfImproveDispatcher {
|
|
|
51
51
|
}>;
|
|
52
52
|
}
|
|
53
53
|
export interface SelfImproveLoopOptions {
|
|
54
|
-
/**
|
|
54
|
+
/**
|
|
55
|
+
* Override the fallback safety-net tick interval. The loop is primarily
|
|
56
|
+
* event-driven; this is just a backstop. Used by tests to disable the
|
|
57
|
+
* timer entirely (set to 0 or a very large number).
|
|
58
|
+
*/
|
|
55
59
|
tickMs?: number;
|
|
56
60
|
/** Override directories for tests. */
|
|
57
61
|
triggersDir?: string;
|
|
58
62
|
pendingDir?: string;
|
|
59
63
|
cronPath?: string;
|
|
64
|
+
/**
|
|
65
|
+
* Disable the fs.watch event-driven path. Tests use this so they can
|
|
66
|
+
* call tick() directly without racing the watcher.
|
|
67
|
+
*/
|
|
68
|
+
disableWatch?: boolean;
|
|
60
69
|
}
|
|
61
70
|
export declare function classifyFailure(recentErrors: string[]): FixRecipe;
|
|
62
71
|
export declare class SelfImproveLoop {
|
|
@@ -65,12 +74,18 @@ export declare class SelfImproveLoop {
|
|
|
65
74
|
private readonly pendingDir;
|
|
66
75
|
private readonly cronPath;
|
|
67
76
|
private readonly dispatcher;
|
|
77
|
+
private readonly watchEnabled;
|
|
68
78
|
private timer;
|
|
79
|
+
private watcher;
|
|
80
|
+
private debounceTimer;
|
|
69
81
|
private running;
|
|
70
82
|
private ticking;
|
|
71
83
|
constructor(dispatcher: SelfImproveDispatcher, opts?: SelfImproveLoopOptions);
|
|
72
84
|
start(): void;
|
|
73
85
|
stop(): void;
|
|
86
|
+
/** Coalesce a burst of fs.watch events (multiple triggers landing in
|
|
87
|
+
* quick succession) into a single tick. */
|
|
88
|
+
private scheduleDebouncedTick;
|
|
74
89
|
/**
|
|
75
90
|
* Process all pending triggers. Public so tests + manual invocations
|
|
76
91
|
* (e.g., a `clementine self-improve tick` CLI command) can call it.
|
|
@@ -24,13 +24,25 @@
|
|
|
24
24
|
* Idempotent: re-applying the same fix to an already-fixed job is a
|
|
25
25
|
* no-op; the trigger gets removed regardless.
|
|
26
26
|
*/
|
|
27
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
27
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, watch, writeFileSync, } from 'node:fs';
|
|
28
28
|
import path from 'node:path';
|
|
29
29
|
import matter from 'gray-matter';
|
|
30
30
|
import pino from 'pino';
|
|
31
31
|
import { BASE_DIR, SYSTEM_DIR } from '../config.js';
|
|
32
32
|
const logger = pino({ name: 'clementine.self-improve-loop' });
|
|
33
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Fallback tick interval. The loop is primarily event-driven via fs.watch
|
|
35
|
+
* on the triggers directory — this is just a slow safety net for cases
|
|
36
|
+
* where fs.watch dropped an event (rare but possible) or where the
|
|
37
|
+
* daemon booted with triggers already in place from before fs.watch was
|
|
38
|
+
* registered. 1h is plenty: the upstream cron scheduler runs at most
|
|
39
|
+
* once per minute, and a job needs 3+ consecutive errors to even produce
|
|
40
|
+
* a trigger, so the situation is already hours-stale by the time we see
|
|
41
|
+
* a trigger.
|
|
42
|
+
*/
|
|
43
|
+
const FALLBACK_TICK_MS = 60 * 60 * 1000;
|
|
44
|
+
/** Coalesce a burst of fs.watch events into a single tick. */
|
|
45
|
+
const WATCH_DEBOUNCE_MS = 2000;
|
|
34
46
|
const TRIGGERS_DIR = path.join(BASE_DIR, 'self-improve', 'triggers');
|
|
35
47
|
const PENDING_CHANGES_DIR = path.join(BASE_DIR, 'self-improve', 'pending-changes');
|
|
36
48
|
const CRON_PATH = path.join(SYSTEM_DIR, 'CRON.md');
|
|
@@ -141,27 +153,49 @@ export class SelfImproveLoop {
|
|
|
141
153
|
pendingDir;
|
|
142
154
|
cronPath;
|
|
143
155
|
dispatcher;
|
|
156
|
+
watchEnabled;
|
|
144
157
|
timer = null;
|
|
158
|
+
watcher = null;
|
|
159
|
+
debounceTimer = null;
|
|
145
160
|
running = false;
|
|
146
161
|
ticking = false;
|
|
147
162
|
constructor(dispatcher, opts = {}) {
|
|
148
163
|
this.dispatcher = dispatcher;
|
|
149
|
-
this.tickMs = opts.tickMs ??
|
|
164
|
+
this.tickMs = opts.tickMs ?? FALLBACK_TICK_MS;
|
|
150
165
|
this.triggersDir = opts.triggersDir ?? TRIGGERS_DIR;
|
|
151
166
|
this.pendingDir = opts.pendingDir ?? PENDING_CHANGES_DIR;
|
|
152
167
|
this.cronPath = opts.cronPath ?? CRON_PATH;
|
|
168
|
+
this.watchEnabled = opts.disableWatch !== true;
|
|
153
169
|
}
|
|
154
170
|
start() {
|
|
155
171
|
if (this.running)
|
|
156
172
|
return;
|
|
157
173
|
this.running = true;
|
|
158
174
|
// Run immediately so any backlog from the prior daemon gets handled
|
|
159
|
-
// without a
|
|
175
|
+
// without a long wait.
|
|
160
176
|
this.tick().catch((err) => logger.error({ err }, 'Initial self-improve tick failed'));
|
|
177
|
+
// Event-driven primary path: watch the triggers dir. cron-scheduler
|
|
178
|
+
// writes a file when a job hits consErrors >= 3; we react within
|
|
179
|
+
// ~2 seconds (debounce window) instead of polling every 10 minutes
|
|
180
|
+
// for a directory that's empty 99% of the time.
|
|
181
|
+
if (this.watchEnabled) {
|
|
182
|
+
try {
|
|
183
|
+
mkdirSync(this.triggersDir, { recursive: true });
|
|
184
|
+
this.watcher = watch(this.triggersDir, (eventType, filename) => {
|
|
185
|
+
if (eventType !== 'rename' || !filename || !filename.endsWith('.json'))
|
|
186
|
+
return;
|
|
187
|
+
this.scheduleDebouncedTick();
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
logger.warn({ err, dir: this.triggersDir }, 'Failed to watch triggers dir — falling back to polling only');
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
// Slow fallback safety net — covers fs.watch event drops + boot-with-backlog.
|
|
161
195
|
this.timer = setInterval(() => {
|
|
162
|
-
this.tick().catch((err) => logger.error({ err }, 'Self-improve tick failed'));
|
|
196
|
+
this.tick().catch((err) => logger.error({ err }, 'Self-improve fallback tick failed'));
|
|
163
197
|
}, this.tickMs);
|
|
164
|
-
logger.info({
|
|
198
|
+
logger.info({ fallbackTickMs: this.tickMs, watchEnabled: this.watchEnabled && this.watcher !== null }, 'Self-improve loop started');
|
|
165
199
|
}
|
|
166
200
|
stop() {
|
|
167
201
|
if (!this.running)
|
|
@@ -171,8 +205,29 @@ export class SelfImproveLoop {
|
|
|
171
205
|
clearInterval(this.timer);
|
|
172
206
|
this.timer = null;
|
|
173
207
|
}
|
|
208
|
+
if (this.watcher) {
|
|
209
|
+
try {
|
|
210
|
+
this.watcher.close();
|
|
211
|
+
}
|
|
212
|
+
catch { /* ignore */ }
|
|
213
|
+
this.watcher = null;
|
|
214
|
+
}
|
|
215
|
+
if (this.debounceTimer) {
|
|
216
|
+
clearTimeout(this.debounceTimer);
|
|
217
|
+
this.debounceTimer = null;
|
|
218
|
+
}
|
|
174
219
|
logger.info('Self-improve loop stopped');
|
|
175
220
|
}
|
|
221
|
+
/** Coalesce a burst of fs.watch events (multiple triggers landing in
|
|
222
|
+
* quick succession) into a single tick. */
|
|
223
|
+
scheduleDebouncedTick() {
|
|
224
|
+
if (this.debounceTimer)
|
|
225
|
+
clearTimeout(this.debounceTimer);
|
|
226
|
+
this.debounceTimer = setTimeout(() => {
|
|
227
|
+
this.debounceTimer = null;
|
|
228
|
+
this.tick().catch((err) => logger.error({ err }, 'Self-improve event-driven tick failed'));
|
|
229
|
+
}, WATCH_DEBOUNCE_MS);
|
|
230
|
+
}
|
|
176
231
|
/**
|
|
177
232
|
* Process all pending triggers. Public so tests + manual invocations
|
|
178
233
|
* (e.g., a `clementine self-improve tick` CLI command) can call it.
|