clementine-agent 1.8.0 → 1.8.1
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.
|
@@ -26,6 +26,19 @@
|
|
|
26
26
|
*/
|
|
27
27
|
export interface TriggerFile {
|
|
28
28
|
jobName: string;
|
|
29
|
+
/**
|
|
30
|
+
* Bare job name (without `{agentSlug}:` prefix). Set by cron-scheduler
|
|
31
|
+
* for agent-scoped jobs so the loop can look the job up in
|
|
32
|
+
* agents/{agentSlug}/CRON.md. Optional for backward compat with
|
|
33
|
+
* triggers written before this field existed.
|
|
34
|
+
*/
|
|
35
|
+
bareName?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Owning agent slug, set by cron-scheduler. When present, the loop
|
|
38
|
+
* applies fixes to vault/00-System/agents/{agentSlug}/CRON.md instead
|
|
39
|
+
* of the central CRON.md. Falls back to scanning if absent (older triggers).
|
|
40
|
+
*/
|
|
41
|
+
agentSlug?: string;
|
|
29
42
|
consecutiveErrors: number;
|
|
30
43
|
recentErrors: string[];
|
|
31
44
|
triggeredAt: string;
|
|
@@ -61,6 +74,11 @@ export interface SelfImproveLoopOptions {
|
|
|
61
74
|
triggersDir?: string;
|
|
62
75
|
pendingDir?: string;
|
|
63
76
|
cronPath?: string;
|
|
77
|
+
/**
|
|
78
|
+
* Override the agents root (vault/00-System/agents). When a trigger
|
|
79
|
+
* has agentSlug, the loop reads/writes `${agentsDir}/${agentSlug}/CRON.md`.
|
|
80
|
+
*/
|
|
81
|
+
agentsDir?: string;
|
|
64
82
|
/**
|
|
65
83
|
* Disable the fs.watch event-driven path. Tests use this so they can
|
|
66
84
|
* call tick() directly without racing the watcher.
|
|
@@ -73,6 +91,7 @@ export declare class SelfImproveLoop {
|
|
|
73
91
|
private readonly triggersDir;
|
|
74
92
|
private readonly pendingDir;
|
|
75
93
|
private readonly cronPath;
|
|
94
|
+
private readonly agentsDir;
|
|
76
95
|
private readonly dispatcher;
|
|
77
96
|
private readonly watchEnabled;
|
|
78
97
|
private timer;
|
|
@@ -28,7 +28,7 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, watch, wr
|
|
|
28
28
|
import path from 'node:path';
|
|
29
29
|
import matter from 'gray-matter';
|
|
30
30
|
import pino from 'pino';
|
|
31
|
-
import { BASE_DIR, SYSTEM_DIR } from '../config.js';
|
|
31
|
+
import { AGENTS_DIR, BASE_DIR, SYSTEM_DIR } from '../config.js';
|
|
32
32
|
const logger = pino({ name: 'clementine.self-improve-loop' });
|
|
33
33
|
/**
|
|
34
34
|
* Fallback tick interval. The loop is primarily event-driven via fs.watch
|
|
@@ -46,6 +46,7 @@ const WATCH_DEBOUNCE_MS = 2000;
|
|
|
46
46
|
const TRIGGERS_DIR = path.join(BASE_DIR, 'self-improve', 'triggers');
|
|
47
47
|
const PENDING_CHANGES_DIR = path.join(BASE_DIR, 'self-improve', 'pending-changes');
|
|
48
48
|
const CRON_PATH = path.join(SYSTEM_DIR, 'CRON.md');
|
|
49
|
+
const AGENTS_ROOT = AGENTS_DIR;
|
|
49
50
|
// ── Pattern recognition ──────────────────────────────────────────────
|
|
50
51
|
const PATTERNS = [
|
|
51
52
|
{
|
|
@@ -108,36 +109,108 @@ export function classifyFailure(recentErrors) {
|
|
|
108
109
|
description: 'Unrecognized failure pattern. Owner needs to inspect the trigger file.',
|
|
109
110
|
};
|
|
110
111
|
}
|
|
111
|
-
function
|
|
112
|
+
function readJobsFromFile(cronPath) {
|
|
112
113
|
if (!existsSync(cronPath))
|
|
113
114
|
return null;
|
|
114
115
|
const raw = readFileSync(cronPath, 'utf-8');
|
|
115
116
|
const parsed = matter(raw);
|
|
116
117
|
const jobs = (parsed.data.jobs ?? []);
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
118
|
+
return { raw, parsed, jobs };
|
|
119
|
+
}
|
|
120
|
+
function readAgentSlug(job) {
|
|
121
|
+
if (typeof job.agentSlug === 'string')
|
|
122
|
+
return job.agentSlug;
|
|
123
|
+
if (typeof job.agent_slug === 'string')
|
|
124
|
+
return job.agent_slug;
|
|
125
|
+
return undefined;
|
|
122
126
|
}
|
|
123
127
|
/**
|
|
124
|
-
*
|
|
125
|
-
*
|
|
128
|
+
* Locate a job's frontmatter entry in either the central CRON.md or an
|
|
129
|
+
* agent-scoped CRON.md. Search priority:
|
|
130
|
+
*
|
|
131
|
+
* 1. If trigger.agentSlug is set, look in agents/{slug}/CRON.md by bareName.
|
|
132
|
+
* 2. Otherwise look in central CRON.md by exact name.
|
|
133
|
+
* 3. Fall back to scanning agents/* for the bareName (covers older triggers
|
|
134
|
+
* that lack agentSlug — the cron-scheduler-prefixed jobName like
|
|
135
|
+
* `slug:name` lets us recover the slug).
|
|
126
136
|
*/
|
|
127
|
-
function
|
|
137
|
+
function loadCronJob(trigger, cronPath, agentsDir) {
|
|
138
|
+
const explicitSlug = trigger.agentSlug;
|
|
139
|
+
const bare = trigger.bareName ?? (explicitSlug && trigger.jobName.startsWith(`${explicitSlug}:`)
|
|
140
|
+
? trigger.jobName.slice(explicitSlug.length + 1)
|
|
141
|
+
: trigger.jobName);
|
|
142
|
+
// 1. Agent-scoped file when slug is known
|
|
143
|
+
if (explicitSlug) {
|
|
144
|
+
const agentCronPath = path.join(agentsDir, explicitSlug, 'CRON.md');
|
|
145
|
+
const file = readJobsFromFile(agentCronPath);
|
|
146
|
+
if (file) {
|
|
147
|
+
const job = file.jobs.find((j) => String(j.name ?? '') === bare);
|
|
148
|
+
if (job) {
|
|
149
|
+
return {
|
|
150
|
+
agentSlug: explicitSlug,
|
|
151
|
+
cronPath: agentCronPath,
|
|
152
|
+
bareName: bare,
|
|
153
|
+
job,
|
|
154
|
+
raw: file.raw,
|
|
155
|
+
parsed: file.parsed,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// 2. Central CRON.md by full jobName (handles globally-defined jobs and
|
|
161
|
+
// legacy jobs tagged with agentSlug field directly in the central file)
|
|
162
|
+
const central = readJobsFromFile(cronPath);
|
|
163
|
+
if (central) {
|
|
164
|
+
const job = central.jobs.find((j) => String(j.name ?? '') === trigger.jobName);
|
|
165
|
+
if (job) {
|
|
166
|
+
return {
|
|
167
|
+
agentSlug: explicitSlug ?? readAgentSlug(job),
|
|
168
|
+
cronPath,
|
|
169
|
+
bareName: String(job.name ?? ''),
|
|
170
|
+
job,
|
|
171
|
+
raw: central.raw,
|
|
172
|
+
parsed: central.parsed,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// 3. Recover via scan: trigger jobName follows `{slug}:{bareName}` for
|
|
177
|
+
// agent-scoped jobs even when older triggers omit agentSlug.
|
|
178
|
+
if (!explicitSlug && trigger.jobName.includes(':')) {
|
|
179
|
+
const [slug, ...rest] = trigger.jobName.split(':');
|
|
180
|
+
const inferredBare = rest.join(':');
|
|
181
|
+
if (slug && inferredBare) {
|
|
182
|
+
const agentCronPath = path.join(agentsDir, slug, 'CRON.md');
|
|
183
|
+
const file = readJobsFromFile(agentCronPath);
|
|
184
|
+
if (file) {
|
|
185
|
+
const job = file.jobs.find((j) => String(j.name ?? '') === inferredBare);
|
|
186
|
+
if (job) {
|
|
187
|
+
return {
|
|
188
|
+
agentSlug: slug,
|
|
189
|
+
cronPath: agentCronPath,
|
|
190
|
+
bareName: inferredBare,
|
|
191
|
+
job,
|
|
192
|
+
raw: file.raw,
|
|
193
|
+
parsed: file.parsed,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Apply the recipe's mutator to the job's frontmatter and write the CRON.md
|
|
203
|
+
* (central or agent-scoped, whichever the lookup resolved to) back atomically.
|
|
204
|
+
* Returns true if a change was actually written.
|
|
205
|
+
*/
|
|
206
|
+
function applyCronEdit(lookup, recipe) {
|
|
128
207
|
if (!recipe.apply)
|
|
129
208
|
return false;
|
|
130
|
-
const lookup = loadCronJob(jobName, cronPath);
|
|
131
|
-
if (!lookup) {
|
|
132
|
-
logger.warn({ jobName }, 'Job not found in CRON.md — cannot apply fix');
|
|
133
|
-
return false;
|
|
134
|
-
}
|
|
135
209
|
const changed = recipe.apply(lookup.job);
|
|
136
210
|
if (!changed)
|
|
137
211
|
return false;
|
|
138
|
-
// Re-stringify with the existing content body preserved.
|
|
139
212
|
const updated = matter.stringify(lookup.parsed.content, lookup.parsed.data);
|
|
140
|
-
writeFileSync(cronPath, updated);
|
|
213
|
+
writeFileSync(lookup.cronPath, updated);
|
|
141
214
|
return true;
|
|
142
215
|
}
|
|
143
216
|
function writePendingChange(record, dir) {
|
|
@@ -152,6 +225,7 @@ export class SelfImproveLoop {
|
|
|
152
225
|
triggersDir;
|
|
153
226
|
pendingDir;
|
|
154
227
|
cronPath;
|
|
228
|
+
agentsDir;
|
|
155
229
|
dispatcher;
|
|
156
230
|
watchEnabled;
|
|
157
231
|
timer = null;
|
|
@@ -165,6 +239,7 @@ export class SelfImproveLoop {
|
|
|
165
239
|
this.triggersDir = opts.triggersDir ?? TRIGGERS_DIR;
|
|
166
240
|
this.pendingDir = opts.pendingDir ?? PENDING_CHANGES_DIR;
|
|
167
241
|
this.cronPath = opts.cronPath ?? CRON_PATH;
|
|
242
|
+
this.agentsDir = opts.agentsDir ?? AGENTS_ROOT;
|
|
168
243
|
this.watchEnabled = opts.disableWatch !== true;
|
|
169
244
|
}
|
|
170
245
|
start() {
|
|
@@ -286,23 +361,32 @@ export class SelfImproveLoop {
|
|
|
286
361
|
}
|
|
287
362
|
async processOne(trigger, counts) {
|
|
288
363
|
const recipe = classifyFailure(trigger.recentErrors);
|
|
289
|
-
const lookup = loadCronJob(trigger.
|
|
290
|
-
const agentSlug = lookup?.agentSlug;
|
|
364
|
+
const lookup = loadCronJob(trigger, this.cronPath, this.agentsDir);
|
|
365
|
+
const agentSlug = trigger.agentSlug ?? lookup?.agentSlug;
|
|
291
366
|
if (recipe.category === 'safe-cron-config') {
|
|
292
|
-
|
|
367
|
+
if (!lookup) {
|
|
368
|
+
// Job vanished from CRON files (renamed/deleted). Nothing to fix.
|
|
369
|
+
counts.noop++;
|
|
370
|
+
logger.warn({ jobName: trigger.jobName, agentSlug }, 'Job not found in any CRON.md — cannot apply fix');
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
const applied = applyCronEdit(lookup, recipe);
|
|
293
374
|
if (applied) {
|
|
294
375
|
counts.applied++;
|
|
376
|
+
const where = lookup.agentSlug
|
|
377
|
+
? `\`agents/${lookup.agentSlug}/CRON.md\``
|
|
378
|
+
: '`CRON.md`';
|
|
295
379
|
await this.notifyAgent(agentSlug, [
|
|
296
380
|
`🔧 **Auto-fixed** \`${trigger.jobName}\` after ${trigger.consecutiveErrors} consecutive failures.`,
|
|
297
381
|
'',
|
|
298
382
|
recipe.description,
|
|
299
383
|
'',
|
|
300
|
-
|
|
384
|
+
`Edit applied to ${where}. I'll watch the next run to confirm it lands cleanly.`,
|
|
301
385
|
].join('\n'));
|
|
302
386
|
}
|
|
303
387
|
else {
|
|
304
388
|
counts.noop++;
|
|
305
|
-
logger.info({ jobName: trigger.jobName }, 'Fix recipe applied is already in place — trigger removed without further action');
|
|
389
|
+
logger.info({ jobName: trigger.jobName, agentSlug }, 'Fix recipe applied is already in place — trigger removed without further action');
|
|
306
390
|
}
|
|
307
391
|
return;
|
|
308
392
|
}
|
package/dist/cli/dashboard.js
CHANGED
|
@@ -21292,8 +21292,8 @@ async function refreshMemoryHealth() {
|
|
|
21292
21292
|
html += '<div style="font-weight:600;margin-bottom:4px">Retrieval running on sparse vectors for ' + missing.toLocaleString() + ' chunks</div>';
|
|
21293
21293
|
html += '<div style="font-size:12px;color:var(--text-muted)">Backfill builds 768-dim neural embeddings for semantic search. First run downloads ~440MB.</div>';
|
|
21294
21294
|
html += '</div>';
|
|
21295
|
-
html += '<button class="btn-sm" onclick="memoryHealthAction(
|
|
21296
|
-
html += '<button class="btn-sm" onclick="memoryHealthAction(
|
|
21295
|
+
html += '<button class="btn-sm" onclick="memoryHealthAction(\\'reembed-dense\\', { limit: 200 })" title="Embed up to 200 chunks now">Backfill 200</button>';
|
|
21296
|
+
html += '<button class="btn-sm" onclick="memoryHealthAction(\\'reembed-dense\\', { limit: 2000 })" title="Embed up to 2000 chunks now (slower)">Backfill 2000</button>';
|
|
21297
21297
|
html += '</div></div>';
|
|
21298
21298
|
}
|
|
21299
21299
|
|
|
@@ -1184,19 +1184,27 @@ export class CronScheduler {
|
|
|
1184
1184
|
if (advice.shouldEscalate) {
|
|
1185
1185
|
this.logAdvisorEvent('escalation', job.name, advice.escalationReason ?? 'Escalated to unleashed');
|
|
1186
1186
|
}
|
|
1187
|
-
// Write targeted self-improvement trigger when consecutive errors are high
|
|
1187
|
+
// Write targeted self-improvement trigger when consecutive errors are high.
|
|
1188
|
+
// Include agentSlug + bareName so the self-improve loop can locate jobs
|
|
1189
|
+
// defined in per-agent CRON.md files (vault/00-System/agents/{slug}/CRON.md)
|
|
1190
|
+
// rather than only the central one.
|
|
1188
1191
|
if (consErrors >= 3) {
|
|
1189
1192
|
try {
|
|
1190
1193
|
const triggerDir = path.join(BASE_DIR, 'self-improve', 'triggers');
|
|
1191
1194
|
mkdirSync(triggerDir, { recursive: true });
|
|
1192
1195
|
const triggerPath = path.join(triggerDir, `${job.name.replace(/[^a-zA-Z0-9_-]/g, '_')}.json`);
|
|
1196
|
+
const bareName = job.agentSlug && job.name.startsWith(`${job.agentSlug}:`)
|
|
1197
|
+
? job.name.slice(job.agentSlug.length + 1)
|
|
1198
|
+
: job.name;
|
|
1193
1199
|
writeFileSync(triggerPath, JSON.stringify({
|
|
1194
1200
|
jobName: job.name,
|
|
1201
|
+
bareName,
|
|
1202
|
+
agentSlug: job.agentSlug,
|
|
1195
1203
|
consecutiveErrors: consErrors,
|
|
1196
1204
|
recentErrors: this.runLog.readRecent(job.name, 3).map(e => e.error?.slice(0, 200)),
|
|
1197
1205
|
triggeredAt: new Date().toISOString(),
|
|
1198
1206
|
}, null, 2));
|
|
1199
|
-
logger.info({ job: job.name, consErrors }, 'Wrote self-improvement trigger for failing job');
|
|
1207
|
+
logger.info({ job: job.name, agentSlug: job.agentSlug, consErrors }, 'Wrote self-improvement trigger for failing job');
|
|
1200
1208
|
}
|
|
1201
1209
|
catch { /* non-fatal */ }
|
|
1202
1210
|
}
|