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
- /** Override scan interval for tests. */
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
- const TICK_MS = 10 * 60 * 1000;
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 ?? TICK_MS;
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 10-minute wait.
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({ tickMs: this.tickMs }, 'Self-improve loop started');
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.0.92",
3
+ "version": "1.0.93",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",