clementine-agent 1.16.0 → 1.18.0
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.js +32 -1
- package/dist/agent/complexity-classifier.js +23 -1
- package/dist/agent/orchestrator.d.ts +2 -0
- package/dist/agent/orchestrator.js +17 -0
- package/dist/agent/self-improve-loop.d.ts +2 -0
- package/dist/agent/self-improve-loop.js +15 -0
- package/dist/cli/dashboard.js +95 -1
- package/dist/gateway/cron-scheduler.d.ts +2 -0
- package/dist/gateway/cron-scheduler.js +17 -0
- package/dist/memory/store.d.ts +22 -0
- package/dist/memory/store.js +81 -0
- package/dist/tools/shared.d.ts +88 -0
- package/package.json +1 -1
package/dist/agent/assistant.js
CHANGED
|
@@ -4769,13 +4769,28 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
4769
4769
|
// Periodic progress beacon — sends a status update every 5 minutes
|
|
4770
4770
|
// so the user knows the task is still alive during long phases.
|
|
4771
4771
|
// Capped at 3 messages per phase to prevent notification spam.
|
|
4772
|
+
// Also refreshes status.json so dashboard polls see liveness even
|
|
4773
|
+
// when the SDK stream hasn't emitted a result yet.
|
|
4772
4774
|
const BEACON_INTERVAL_MS = 5 * 60 * 1000;
|
|
4773
4775
|
const MAX_BEACONS_PER_PHASE = 3;
|
|
4774
4776
|
let beaconCount = 0;
|
|
4775
4777
|
const beaconTimer = setInterval(() => {
|
|
4778
|
+
const mins = Math.round((Date.now() - phaseStart) / 60_000);
|
|
4779
|
+
try {
|
|
4780
|
+
writeStatus({
|
|
4781
|
+
jobName,
|
|
4782
|
+
status: 'running',
|
|
4783
|
+
phase,
|
|
4784
|
+
startedAt,
|
|
4785
|
+
maxHours: effectiveMaxHours,
|
|
4786
|
+
phaseStartedAt: new Date(phaseStart).toISOString(),
|
|
4787
|
+
phaseElapsedMin: mins,
|
|
4788
|
+
toolCallsThisPhase: phaseToolCount,
|
|
4789
|
+
});
|
|
4790
|
+
}
|
|
4791
|
+
catch { /* non-fatal */ }
|
|
4776
4792
|
if (this.onPhaseProgress && beaconCount < MAX_BEACONS_PER_PHASE) {
|
|
4777
4793
|
beaconCount++;
|
|
4778
|
-
const mins = Math.round((Date.now() - phaseStart) / 60_000);
|
|
4779
4794
|
try {
|
|
4780
4795
|
// Conversational beacon — no technical jargon
|
|
4781
4796
|
const msg = mins < 3
|
|
@@ -4826,6 +4841,22 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
4826
4841
|
// Capture terminal reason for execution advisor
|
|
4827
4842
|
this._lastTerminalReason = result.terminal_reason ?? undefined;
|
|
4828
4843
|
this.logQueryResult(result, 'unleashed', `unleashed:${jobName}`, jobName);
|
|
4844
|
+
// Refresh status.json the moment the SDK reports result —
|
|
4845
|
+
// even if the underlying stream stalls afterward, the dashboard
|
|
4846
|
+
// sees liveness instead of a frozen "phase 0 / running" row.
|
|
4847
|
+
try {
|
|
4848
|
+
writeStatus({
|
|
4849
|
+
jobName,
|
|
4850
|
+
status: 'running',
|
|
4851
|
+
phase,
|
|
4852
|
+
startedAt,
|
|
4853
|
+
maxHours: effectiveMaxHours,
|
|
4854
|
+
lastResultAt: new Date().toISOString(),
|
|
4855
|
+
lastResultIsError: !!result.is_error,
|
|
4856
|
+
toolCallsThisPhase: phaseToolCount,
|
|
4857
|
+
});
|
|
4858
|
+
}
|
|
4859
|
+
catch { /* non-fatal */ }
|
|
4829
4860
|
// Detect dollar-budget exceeded (strict marker — see cron
|
|
4830
4861
|
// handler above for the reasoning).
|
|
4831
4862
|
if (result.is_error && 'result' in result) {
|
|
@@ -50,6 +50,21 @@ const CHAIN_MARKERS = [
|
|
|
50
50
|
/\bonce\s+(that|you)\b.*,/i,
|
|
51
51
|
/\bnext\b.*,/i,
|
|
52
52
|
];
|
|
53
|
+
/**
|
|
54
|
+
* Patterns that look like a pasted error message or stack trace.
|
|
55
|
+
* Error pastes are long and entity-heavy (file paths, quoted strings,
|
|
56
|
+
* "Error:" prefixes), which previously tripped the deepWorthy gate
|
|
57
|
+
* even when the user was just asking "what's wrong with this?". We
|
|
58
|
+
* still allow the plan-first directive to fire; we just don't auto-spawn
|
|
59
|
+
* an expensive multi-phase background task on a debug request.
|
|
60
|
+
*/
|
|
61
|
+
const ERROR_PASTE_MARKERS = [
|
|
62
|
+
/\b(Error|Exception|Traceback|Stack ?trace):\s/i,
|
|
63
|
+
/^\s*at\s+[\w.$<>]+\s*\(/m, // JS/TS stack frame: "at foo.bar (file:line)"
|
|
64
|
+
/\bfailed:\s*Error\b/i,
|
|
65
|
+
/Reached maximum number of turns/i,
|
|
66
|
+
/\bENOENT\b|\bECONNREFUSED\b|\bETIMEDOUT\b/,
|
|
67
|
+
];
|
|
53
68
|
/**
|
|
54
69
|
* Phrasings that explicitly ask for plan-first behavior. Triggers
|
|
55
70
|
* regardless of other heuristics.
|
|
@@ -148,7 +163,14 @@ export function classifyComplexity(text) {
|
|
|
148
163
|
isLong,
|
|
149
164
|
entities >= 3,
|
|
150
165
|
].filter(Boolean).length;
|
|
151
|
-
|
|
166
|
+
// Suppress deepWorthy on pasted error messages. They're long and
|
|
167
|
+
// entity-heavy (file paths, quoted strings) but the user is asking
|
|
168
|
+
// "what's wrong here?", not requesting sustained autonomous work.
|
|
169
|
+
// The plan-first path still fires when complex=true.
|
|
170
|
+
const looksLikeErrorPaste = ERROR_PASTE_MARKERS.some((re) => re.test(trimmed));
|
|
171
|
+
if (looksLikeErrorPaste)
|
|
172
|
+
signals.push('error-paste');
|
|
173
|
+
const deepWorthy = strongCount >= 2 && !looksLikeErrorPaste;
|
|
152
174
|
if (complex) {
|
|
153
175
|
return {
|
|
154
176
|
complex: true,
|
|
@@ -40,6 +40,8 @@ export declare class PlanOrchestrator {
|
|
|
40
40
|
private stateId;
|
|
41
41
|
private agentProfiles;
|
|
42
42
|
constructor(assistant: PersonalAssistant);
|
|
43
|
+
/** Fire-and-forget autonomy telemetry — must never throw. */
|
|
44
|
+
private logAutonomy;
|
|
43
45
|
private saveState;
|
|
44
46
|
private cleanupState;
|
|
45
47
|
/** Load a previously interrupted plan state (for future resumability). */
|
|
@@ -127,6 +127,17 @@ export class PlanOrchestrator {
|
|
|
127
127
|
this.assistant = assistant;
|
|
128
128
|
this.stateId = `plan-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
129
129
|
}
|
|
130
|
+
/** Fire-and-forget autonomy telemetry — must never throw. */
|
|
131
|
+
async logAutonomy(event, details) {
|
|
132
|
+
try {
|
|
133
|
+
const { getStore } = await import('../tools/shared.js');
|
|
134
|
+
const store = await getStore();
|
|
135
|
+
store.logAutonomyEvent({ component: 'orchestrator', event, details });
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// Best-effort — telemetry must never break plan execution.
|
|
139
|
+
}
|
|
140
|
+
}
|
|
130
141
|
// ── State persistence ────────────────────────────────────────────────
|
|
131
142
|
saveState(state) {
|
|
132
143
|
try {
|
|
@@ -206,6 +217,7 @@ export class PlanOrchestrator {
|
|
|
206
217
|
return this.runSingleStep(taskDescription);
|
|
207
218
|
}
|
|
208
219
|
logger.info({ goal: effectiveTask, stepCount: plan.steps.length, steps: plan.steps.map(s => s.id), revision: revisionCount }, 'Plan generated');
|
|
220
|
+
this.logAutonomy('plan_created', { stepCount: plan.steps.length, revision: revisionCount });
|
|
209
221
|
// 2. Initialize statuses
|
|
210
222
|
this.stepStatuses.clear();
|
|
211
223
|
for (const step of plan.steps) {
|
|
@@ -312,6 +324,7 @@ export class PlanOrchestrator {
|
|
|
312
324
|
durationMs: elapsed,
|
|
313
325
|
resultPreview: resultText.slice(0, 100),
|
|
314
326
|
});
|
|
327
|
+
this.logAutonomy('step_completed', { stepId: step.id, description: step.description, durationMs: elapsed, model: step.model });
|
|
315
328
|
}
|
|
316
329
|
else {
|
|
317
330
|
const errMsg = `[FAILED: ${outcome.reason}]`;
|
|
@@ -324,6 +337,7 @@ export class PlanOrchestrator {
|
|
|
324
337
|
resultPreview: errMsg.slice(0, 100),
|
|
325
338
|
});
|
|
326
339
|
logger.error({ stepId: step.id, err: outcome.reason }, 'Plan step failed');
|
|
340
|
+
this.logAutonomy('step_failed', { stepId: step.id, description: step.description, error: String(outcome.reason).slice(0, 500) });
|
|
327
341
|
}
|
|
328
342
|
}
|
|
329
343
|
await safeProgress(this.getAllUpdates());
|
|
@@ -367,6 +381,7 @@ export class PlanOrchestrator {
|
|
|
367
381
|
logger.warn({ stepId: issue.stepId }, 'Repair decision: abort plan');
|
|
368
382
|
state.status = 'failed';
|
|
369
383
|
this.saveState(state);
|
|
384
|
+
this.logAutonomy('plan_aborted', { stepId: issue.stepId, reason: issue.issue });
|
|
370
385
|
return `Plan aborted — step "${this.stepStatuses.get(issue.stepId)?.description ?? issue.stepId}" failed critically and could not be repaired.`;
|
|
371
386
|
}
|
|
372
387
|
// 'skip' falls through — annotate and continue
|
|
@@ -438,6 +453,7 @@ export class PlanOrchestrator {
|
|
|
438
453
|
}
|
|
439
454
|
catch (err) {
|
|
440
455
|
logger.error({ err }, 'Synthesis step failed');
|
|
456
|
+
this.logAutonomy('synthesis_failed', { error: String(err).slice(0, 500) });
|
|
441
457
|
// Fallback: concatenate results
|
|
442
458
|
finalResult = Array.from(results.entries())
|
|
443
459
|
.map(([id, r]) => `**${this.stepStatuses.get(id)?.description ?? id}:**\n${r}`)
|
|
@@ -453,6 +469,7 @@ export class PlanOrchestrator {
|
|
|
453
469
|
await safeProgress(this.getAllUpdates());
|
|
454
470
|
const totalMs = Date.now() - this.startTime;
|
|
455
471
|
logger.info({ totalMs, steps: plan.steps.length }, 'Plan execution complete');
|
|
472
|
+
this.logAutonomy('plan_completed', { totalMs, steps: plan.steps.length, errors: state.errors.length });
|
|
456
473
|
// Mark state as complete and clean up
|
|
457
474
|
state.status = 'complete';
|
|
458
475
|
this.saveState(state);
|
|
@@ -112,6 +112,8 @@ export declare class SelfImproveLoop {
|
|
|
112
112
|
/** Coalesce a burst of fs.watch events (multiple triggers landing in
|
|
113
113
|
* quick succession) into a single tick. */
|
|
114
114
|
private scheduleDebouncedTick;
|
|
115
|
+
/** Fire-and-forget autonomy telemetry — must never throw. */
|
|
116
|
+
private logAutonomy;
|
|
115
117
|
/**
|
|
116
118
|
* Process all pending triggers. Public so tests + manual invocations
|
|
117
119
|
* (e.g., a `clementine self-improve tick` CLI command) can call it.
|
|
@@ -313,6 +313,17 @@ export class SelfImproveLoop {
|
|
|
313
313
|
this.tick().catch((err) => logger.error({ err }, 'Self-improve event-driven tick failed'));
|
|
314
314
|
}, WATCH_DEBOUNCE_MS);
|
|
315
315
|
}
|
|
316
|
+
/** Fire-and-forget autonomy telemetry — must never throw. */
|
|
317
|
+
async logAutonomy(event, trigger, details) {
|
|
318
|
+
try {
|
|
319
|
+
const { getStore } = await import('../tools/shared.js');
|
|
320
|
+
const store = await getStore();
|
|
321
|
+
store.logAutonomyEvent({ component: 'self_improve', event, agentSlug: trigger.agentSlug ?? null, details });
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
// Best-effort — telemetry must never break the self-improve loop.
|
|
325
|
+
}
|
|
326
|
+
}
|
|
316
327
|
/**
|
|
317
328
|
* Process all pending triggers. Public so tests + manual invocations
|
|
318
329
|
* (e.g., a `clementine self-improve tick` CLI command) can call it.
|
|
@@ -346,11 +357,13 @@ export class SelfImproveLoop {
|
|
|
346
357
|
catch { /* ignore */ }
|
|
347
358
|
continue;
|
|
348
359
|
}
|
|
360
|
+
this.logAutonomy('trigger_detected', trigger, { consecutiveErrors: trigger.consecutiveErrors });
|
|
349
361
|
try {
|
|
350
362
|
await this.processOne(trigger, counts);
|
|
351
363
|
}
|
|
352
364
|
catch (err) {
|
|
353
365
|
logger.warn({ err, jobName: trigger.jobName }, 'Failed to process trigger — leaving in place for next tick');
|
|
366
|
+
this.logAutonomy('process_failed', trigger, { error: String(err).slice(0, 500) });
|
|
354
367
|
continue;
|
|
355
368
|
}
|
|
356
369
|
// Successfully handled — remove the trigger
|
|
@@ -419,6 +432,7 @@ export class SelfImproveLoop {
|
|
|
419
432
|
if (recipe.category === 'noop') {
|
|
420
433
|
counts.noop++;
|
|
421
434
|
logger.info({ jobName: trigger.jobName, reason: recipe.description }, 'Self-improve: no-op');
|
|
435
|
+
this.logAutonomy('fix_noop', trigger, { reason: recipe.description });
|
|
422
436
|
return;
|
|
423
437
|
}
|
|
424
438
|
// risky | unknown → write proposal + DM agent
|
|
@@ -435,6 +449,7 @@ export class SelfImproveLoop {
|
|
|
435
449
|
};
|
|
436
450
|
const file = writePendingChange(record, this.pendingDir);
|
|
437
451
|
counts.pending++;
|
|
452
|
+
this.logAutonomy('proposal_written', trigger, { category: recipe.category, proposalId: id });
|
|
438
453
|
await this.notifyAgent(agentSlug, [
|
|
439
454
|
`⚠️ **${trigger.jobName}** has failed ${trigger.consecutiveErrors} times in a row.`,
|
|
440
455
|
'',
|
package/dist/cli/dashboard.js
CHANGED
|
@@ -1297,7 +1297,84 @@ export async function cmdDashboard(opts) {
|
|
|
1297
1297
|
index: false,
|
|
1298
1298
|
}));
|
|
1299
1299
|
// Health check — always responds, no auth, no middleware dependency
|
|
1300
|
-
app.get('/health', (_req, res) => {
|
|
1300
|
+
app.get('/health', async (_req, res) => {
|
|
1301
|
+
const checks = {};
|
|
1302
|
+
let overall = 'healthy';
|
|
1303
|
+
// 1. Daemon process liveness
|
|
1304
|
+
const pid = readPid();
|
|
1305
|
+
const daemonAlive = pid ? isProcessAlive(pid) : false;
|
|
1306
|
+
checks.daemon = daemonAlive ? 'ok' : 'unavailable';
|
|
1307
|
+
if (!daemonAlive)
|
|
1308
|
+
overall = 'unhealthy';
|
|
1309
|
+
// 2. SQLite + memory health
|
|
1310
|
+
try {
|
|
1311
|
+
const { getStore } = await import('../tools/shared.js');
|
|
1312
|
+
const store = await getStore();
|
|
1313
|
+
const health = store.getMemoryHealth();
|
|
1314
|
+
checks.sqlite = 'ok';
|
|
1315
|
+
checks.fts5 = health.lastIntegrityReport?.ftsOk ? 'ok' : 'degraded';
|
|
1316
|
+
checks.writeQueue = (health.writeQueue?.size ?? 0) > 100 ? 'degraded' : 'ok';
|
|
1317
|
+
if (checks.fts5 === 'degraded' || checks.writeQueue === 'degraded') {
|
|
1318
|
+
overall = overall === 'unhealthy' ? 'unhealthy' : 'degraded';
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
catch {
|
|
1322
|
+
checks.sqlite = 'unavailable';
|
|
1323
|
+
overall = 'unhealthy';
|
|
1324
|
+
}
|
|
1325
|
+
// 3. Graph store availability
|
|
1326
|
+
try {
|
|
1327
|
+
const { getSharedGraphStore } = await import('../memory/graph-store.js');
|
|
1328
|
+
const graphDbDir = path.join(BASE_DIR, '.graph.db');
|
|
1329
|
+
const gs = await getSharedGraphStore(graphDbDir);
|
|
1330
|
+
checks.graphStore = gs?.isAvailable() ? 'ok' : 'unavailable';
|
|
1331
|
+
if (checks.graphStore === 'unavailable' && overall !== 'unhealthy') {
|
|
1332
|
+
overall = 'degraded';
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
catch {
|
|
1336
|
+
checks.graphStore = 'unavailable';
|
|
1337
|
+
if (overall !== 'unhealthy')
|
|
1338
|
+
overall = 'degraded';
|
|
1339
|
+
}
|
|
1340
|
+
// 4. Recent cron failures (last 24h)
|
|
1341
|
+
try {
|
|
1342
|
+
const runsDir = path.join(BASE_DIR, 'cron', 'runs');
|
|
1343
|
+
let recentFailures = 0;
|
|
1344
|
+
let recentRuns = 0;
|
|
1345
|
+
const dayAgo = Date.now() - 24 * 60 * 60 * 1000;
|
|
1346
|
+
if (existsSync(runsDir)) {
|
|
1347
|
+
for (const file of readdirSync(runsDir).filter(f => f.endsWith('.jsonl'))) {
|
|
1348
|
+
const lines = readFileSync(path.join(runsDir, file), 'utf-8').trim().split('\n').filter(Boolean);
|
|
1349
|
+
for (const line of lines.slice(-50)) {
|
|
1350
|
+
try {
|
|
1351
|
+
const entry = JSON.parse(line);
|
|
1352
|
+
const ts = entry.startedAt ? new Date(entry.startedAt).getTime() : 0;
|
|
1353
|
+
if (ts > dayAgo) {
|
|
1354
|
+
recentRuns++;
|
|
1355
|
+
if (entry.status === 'failed' || entry.status === 'error')
|
|
1356
|
+
recentFailures++;
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
catch { /* ignore parse errors */ }
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
checks.cron = recentFailures > 3 ? 'degraded' : recentFailures > 0 ? 'degraded' : 'ok';
|
|
1364
|
+
if (checks.cron === 'degraded' && overall !== 'unhealthy')
|
|
1365
|
+
overall = 'degraded';
|
|
1366
|
+
}
|
|
1367
|
+
catch {
|
|
1368
|
+
checks.cron = 'unavailable';
|
|
1369
|
+
}
|
|
1370
|
+
const statusCode = overall === 'healthy' ? 200 : overall === 'degraded' ? 503 : 503;
|
|
1371
|
+
res.status(statusCode).json({
|
|
1372
|
+
status: overall,
|
|
1373
|
+
checks,
|
|
1374
|
+
daemon: { pid: pid ?? null, alive: daemonAlive },
|
|
1375
|
+
timestamp: new Date().toISOString(),
|
|
1376
|
+
});
|
|
1377
|
+
});
|
|
1301
1378
|
// ── Webhook ingestion (raw-body, HMAC-authed) ─────────────────────
|
|
1302
1379
|
// MUST be registered BEFORE the generic json parser below — otherwise
|
|
1303
1380
|
// that parser consumes the body and HMAC verification fails since we
|
|
@@ -2203,6 +2280,23 @@ export async function cmdDashboard(opts) {
|
|
|
2203
2280
|
res.status(500).json({ error: String(err).slice(0, 200) });
|
|
2204
2281
|
}
|
|
2205
2282
|
});
|
|
2283
|
+
app.get('/api/autonomy', async (req, res) => {
|
|
2284
|
+
try {
|
|
2285
|
+
const { getStore } = await import('../tools/shared.js');
|
|
2286
|
+
const store = await getStore();
|
|
2287
|
+
const logs = store.queryAutonomyLog({
|
|
2288
|
+
component: typeof req.query.component === 'string' ? req.query.component : undefined,
|
|
2289
|
+
event: typeof req.query.event === 'string' ? req.query.event : undefined,
|
|
2290
|
+
agentSlug: typeof req.query.agentSlug === 'string' ? req.query.agentSlug : undefined,
|
|
2291
|
+
limit: typeof req.query.limit === 'string' ? parseInt(req.query.limit, 10) : 100,
|
|
2292
|
+
since: typeof req.query.since === 'string' ? req.query.since : undefined,
|
|
2293
|
+
});
|
|
2294
|
+
res.json({ logs });
|
|
2295
|
+
}
|
|
2296
|
+
catch (err) {
|
|
2297
|
+
res.status(500).json({ error: String(err).slice(0, 200) });
|
|
2298
|
+
}
|
|
2299
|
+
});
|
|
2206
2300
|
app.get('/api/heartbeat/agent/:slug', (req, res) => {
|
|
2207
2301
|
const slug = req.params.slug;
|
|
2208
2302
|
const state = getHeartbeat();
|
|
@@ -93,6 +93,8 @@ export declare class CronScheduler {
|
|
|
93
93
|
private statusChangeListeners;
|
|
94
94
|
private static readonly RUNNING_JOBS_FILE;
|
|
95
95
|
constructor(gateway: Gateway, dispatcher: NotificationDispatcher);
|
|
96
|
+
/** Fire-and-forget autonomy telemetry — must never throw. */
|
|
97
|
+
private logAutonomy;
|
|
96
98
|
/**
|
|
97
99
|
* Atomically persist the current runningJobs set to disk. Uses write-then-
|
|
98
100
|
* rename so a crash mid-write cannot corrupt the file.
|
|
@@ -401,6 +401,17 @@ export class CronScheduler {
|
|
|
401
401
|
// query jobs on connect which happens before start().
|
|
402
402
|
this.loadJobDefinitions();
|
|
403
403
|
}
|
|
404
|
+
/** Fire-and-forget autonomy telemetry — must never throw. */
|
|
405
|
+
async logAutonomy(event, job, details) {
|
|
406
|
+
try {
|
|
407
|
+
const { getStore } = await import('../tools/shared.js');
|
|
408
|
+
const store = await getStore();
|
|
409
|
+
store.logAutonomyEvent({ component: 'cron', event, agentSlug: job.agentSlug ?? null, details });
|
|
410
|
+
}
|
|
411
|
+
catch {
|
|
412
|
+
// Best-effort — telemetry must never break cron execution.
|
|
413
|
+
}
|
|
414
|
+
}
|
|
404
415
|
/**
|
|
405
416
|
* Atomically persist the current runningJobs set to disk. Uses write-then-
|
|
406
417
|
* rename so a crash mid-write cannot corrupt the file.
|
|
@@ -974,6 +985,7 @@ export class CronScheduler {
|
|
|
974
985
|
});
|
|
975
986
|
this.persistRunningJobs(this.runMetadata);
|
|
976
987
|
this.emitStatusChange();
|
|
988
|
+
this.logAutonomy('started', job, { model: job.model, mode: job.mode, tier: job.tier });
|
|
977
989
|
try {
|
|
978
990
|
logger.info(`Running cron job: ${job.name}${job.agentSlug ? ` (agent: ${job.agentSlug})` : ''}`);
|
|
979
991
|
// Set agent profile for scoped cron jobs
|
|
@@ -1084,6 +1096,7 @@ export class CronScheduler {
|
|
|
1084
1096
|
}
|
|
1085
1097
|
}
|
|
1086
1098
|
this._logRun(entry);
|
|
1099
|
+
this.logAutonomy('completed', job, { durationMs: entry.durationMs, deliveryFailed: entry.deliveryFailed, advisorApplied: !!advisorApplied });
|
|
1087
1100
|
// Fire-and-forget: extract procedural skill from successful long-running cron jobs
|
|
1088
1101
|
if (entry.status === 'ok' && entry.durationMs > 30_000 && response && response.length > 500) {
|
|
1089
1102
|
this.gateway.extractCronSkill(job.name, job.prompt, response, entry.durationMs, job.agentSlug)
|
|
@@ -1135,6 +1148,7 @@ export class CronScheduler {
|
|
|
1135
1148
|
}
|
|
1136
1149
|
else {
|
|
1137
1150
|
logger.error({ err, job: job.name }, `Cron job '${job.name}' failed after ${attempt} attempt(s)`);
|
|
1151
|
+
this.logAutonomy('failed', job, { errorType, attempts: attempt, error: String(err).slice(0, 500) });
|
|
1138
1152
|
await this.dispatcher.send(CronScheduler.formatCronError(job.name, err), { agentSlug: job.agentSlug });
|
|
1139
1153
|
}
|
|
1140
1154
|
}
|
|
@@ -1170,6 +1184,7 @@ export class CronScheduler {
|
|
|
1170
1184
|
const consErrors = this.runLog.consecutiveErrors(job.name);
|
|
1171
1185
|
if (consErrors === 5) {
|
|
1172
1186
|
// Circuit breaker just engaged — notify
|
|
1187
|
+
this.logAutonomy('circuit_breaker', job, { consecutiveErrors: consErrors });
|
|
1173
1188
|
this.logAdvisorEvent('circuit-breaker', job.name, `Circuit breaker engaged after ${consErrors} consecutive errors`);
|
|
1174
1189
|
this.dispatcher.send(`⚡ **Circuit breaker engaged** for \`${job.name}\` — ${consErrors} consecutive errors. Will retry in 1 hour.`, { agentSlug: job.agentSlug }).catch(err => logger.debug({ err }, 'Failed to send circuit breaker notification'));
|
|
1175
1190
|
}
|
|
@@ -1189,6 +1204,7 @@ export class CronScheduler {
|
|
|
1189
1204
|
// defined in per-agent CRON.md files (vault/00-System/agents/{slug}/CRON.md)
|
|
1190
1205
|
// rather than only the central one.
|
|
1191
1206
|
if (consErrors >= 3) {
|
|
1207
|
+
this.logAutonomy('self_improve_triggered', job, { consecutiveErrors: consErrors });
|
|
1192
1208
|
try {
|
|
1193
1209
|
const triggerDir = path.join(BASE_DIR, 'self-improve', 'triggers');
|
|
1194
1210
|
mkdirSync(triggerDir, { recursive: true });
|
|
@@ -1218,6 +1234,7 @@ export class CronScheduler {
|
|
|
1218
1234
|
this.scheduledTasks.delete(job.name);
|
|
1219
1235
|
}
|
|
1220
1236
|
logger.error({ job: job.name, consErrors }, `Auto-disabled cron after ${consErrors} consecutive failures`);
|
|
1237
|
+
this.logAutonomy('auto_disabled', job, { consecutiveErrors: consErrors });
|
|
1221
1238
|
this.logAdvisorEvent('auto-disabled', job.name, `Auto-disabled after ${consErrors} consecutive failures`);
|
|
1222
1239
|
this.dispatcher.send(`🛑 **Cron auto-disabled** — \`${job.name}\` failed ${consErrors} times in a row. Fix the job and re-enable it from the dashboard.`, { agentSlug: job.agentSlug }).catch(err => logger.debug({ err }, 'Failed to send auto-disable notification'));
|
|
1223
1240
|
}
|
package/dist/memory/store.d.ts
CHANGED
|
@@ -1233,6 +1233,28 @@ export declare class MemoryStore {
|
|
|
1233
1233
|
* Reduces salience so they appear lower in search results (but aren't deleted).
|
|
1234
1234
|
*/
|
|
1235
1235
|
markConsolidated(chunkIds: number[]): void;
|
|
1236
|
+
private _stmtLogAutonomy;
|
|
1237
|
+
private _stmtQueryAutonomy;
|
|
1238
|
+
logAutonomyEvent(row: {
|
|
1239
|
+
component: string;
|
|
1240
|
+
event: string;
|
|
1241
|
+
agentSlug?: string | null;
|
|
1242
|
+
details?: Record<string, unknown>;
|
|
1243
|
+
}): void;
|
|
1244
|
+
queryAutonomyLog(opts?: {
|
|
1245
|
+
component?: string;
|
|
1246
|
+
event?: string;
|
|
1247
|
+
agentSlug?: string;
|
|
1248
|
+
limit?: number;
|
|
1249
|
+
since?: string;
|
|
1250
|
+
}): Array<{
|
|
1251
|
+
id: number;
|
|
1252
|
+
component: string;
|
|
1253
|
+
event: string;
|
|
1254
|
+
agentSlug: string | null;
|
|
1255
|
+
details: Record<string, unknown>;
|
|
1256
|
+
createdAt: string;
|
|
1257
|
+
}>;
|
|
1236
1258
|
/**
|
|
1237
1259
|
* Aggregate memory-health snapshot for the dashboard.
|
|
1238
1260
|
*
|
package/dist/memory/store.js
CHANGED
|
@@ -834,6 +834,23 @@ export class MemoryStore {
|
|
|
834
834
|
value TEXT NOT NULL,
|
|
835
835
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
836
836
|
);
|
|
837
|
+
`);
|
|
838
|
+
// Autonomy log — structured event stream for the self-improving,
|
|
839
|
+
// proactive, and planning layers. Makes the autonomous subsystem
|
|
840
|
+
// observable without grepping logs.
|
|
841
|
+
this.conn.exec(`
|
|
842
|
+
CREATE TABLE IF NOT EXISTS autonomy_log (
|
|
843
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
844
|
+
component TEXT NOT NULL,
|
|
845
|
+
event TEXT NOT NULL,
|
|
846
|
+
agent_slug TEXT,
|
|
847
|
+
details_json TEXT DEFAULT '{}',
|
|
848
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
849
|
+
);
|
|
850
|
+
CREATE INDEX IF NOT EXISTS idx_autonomy_component ON autonomy_log(component, created_at DESC);
|
|
851
|
+
CREATE INDEX IF NOT EXISTS idx_autonomy_event ON autonomy_log(event, created_at DESC);
|
|
852
|
+
CREATE INDEX IF NOT EXISTS idx_autonomy_agent ON autonomy_log(agent_slug, created_at DESC);
|
|
853
|
+
CREATE INDEX IF NOT EXISTS idx_autonomy_created ON autonomy_log(created_at DESC);
|
|
837
854
|
`);
|
|
838
855
|
}
|
|
839
856
|
// ── Skill usage telemetry ─────────────────────────────────────────
|
|
@@ -4331,6 +4348,70 @@ export class MemoryStore {
|
|
|
4331
4348
|
WHERE id IN (${placeholders})`)
|
|
4332
4349
|
.run(...chunkIds);
|
|
4333
4350
|
}
|
|
4351
|
+
// ── Autonomy log ───────────────────────────────────────────────────
|
|
4352
|
+
_stmtLogAutonomy = null;
|
|
4353
|
+
_stmtQueryAutonomy = null;
|
|
4354
|
+
logAutonomyEvent(row) {
|
|
4355
|
+
try {
|
|
4356
|
+
if (!this._stmtLogAutonomy) {
|
|
4357
|
+
this._stmtLogAutonomy = this.conn.prepare('INSERT INTO autonomy_log (component, event, agent_slug, details_json) VALUES (?, ?, ?, ?)');
|
|
4358
|
+
}
|
|
4359
|
+
this._stmtLogAutonomy.run(row.component, row.event, row.agentSlug ?? null, JSON.stringify(row.details ?? {}));
|
|
4360
|
+
}
|
|
4361
|
+
catch {
|
|
4362
|
+
// Best-effort — autonomy telemetry must never break the system.
|
|
4363
|
+
}
|
|
4364
|
+
}
|
|
4365
|
+
queryAutonomyLog(opts = {}) {
|
|
4366
|
+
const conditions = [];
|
|
4367
|
+
const params = [];
|
|
4368
|
+
if (opts.component) {
|
|
4369
|
+
conditions.push('component = ?');
|
|
4370
|
+
params.push(opts.component);
|
|
4371
|
+
}
|
|
4372
|
+
if (opts.event) {
|
|
4373
|
+
conditions.push('event = ?');
|
|
4374
|
+
params.push(opts.event);
|
|
4375
|
+
}
|
|
4376
|
+
if (opts.agentSlug) {
|
|
4377
|
+
conditions.push('agent_slug = ?');
|
|
4378
|
+
params.push(opts.agentSlug);
|
|
4379
|
+
}
|
|
4380
|
+
if (opts.since) {
|
|
4381
|
+
conditions.push('created_at >= ?');
|
|
4382
|
+
params.push(opts.since);
|
|
4383
|
+
}
|
|
4384
|
+
const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
4385
|
+
const limit = opts.limit ?? 100;
|
|
4386
|
+
if (!this._stmtQueryAutonomy || conditions.length) {
|
|
4387
|
+
// Ad-hoc query — conditions vary, use generic runner
|
|
4388
|
+
return this.conn
|
|
4389
|
+
.prepare(`SELECT * FROM autonomy_log ${where} ORDER BY created_at DESC LIMIT ?`)
|
|
4390
|
+
.all(...params, limit)
|
|
4391
|
+
.map((row) => {
|
|
4392
|
+
const r = row;
|
|
4393
|
+
return {
|
|
4394
|
+
id: r.id,
|
|
4395
|
+
component: r.component,
|
|
4396
|
+
event: r.event,
|
|
4397
|
+
agentSlug: r.agent_slug,
|
|
4398
|
+
details: JSON.parse(r.details_json || '{}'),
|
|
4399
|
+
createdAt: r.created_at,
|
|
4400
|
+
};
|
|
4401
|
+
});
|
|
4402
|
+
}
|
|
4403
|
+
return this._stmtQueryAutonomy.all(limit).map((row) => {
|
|
4404
|
+
const r = row;
|
|
4405
|
+
return {
|
|
4406
|
+
id: r.id,
|
|
4407
|
+
component: r.component,
|
|
4408
|
+
event: r.event,
|
|
4409
|
+
agentSlug: r.agent_slug,
|
|
4410
|
+
details: JSON.parse(r.details_json || '{}'),
|
|
4411
|
+
createdAt: r.created_at,
|
|
4412
|
+
};
|
|
4413
|
+
});
|
|
4414
|
+
}
|
|
4334
4415
|
/**
|
|
4335
4416
|
* Aggregate memory-health snapshot for the dashboard.
|
|
4336
4417
|
*
|
package/dist/tools/shared.d.ts
CHANGED
|
@@ -459,6 +459,94 @@ export type MemoryStoreType = {
|
|
|
459
459
|
newestUpdated: string | null;
|
|
460
460
|
};
|
|
461
461
|
db: unknown;
|
|
462
|
+
getMemoryHealth(opts?: {
|
|
463
|
+
graphStore?: {
|
|
464
|
+
isAvailable(): boolean;
|
|
465
|
+
nodeCount?(): Promise<number>;
|
|
466
|
+
edgeCount?(): Promise<number>;
|
|
467
|
+
};
|
|
468
|
+
topCitedLimit?: number;
|
|
469
|
+
}): {
|
|
470
|
+
chunks: {
|
|
471
|
+
total: number;
|
|
472
|
+
consolidated: number;
|
|
473
|
+
pinned: number;
|
|
474
|
+
softDeleted: number;
|
|
475
|
+
zombieCount: number;
|
|
476
|
+
};
|
|
477
|
+
chunksByCategory: Array<{
|
|
478
|
+
category: string | null;
|
|
479
|
+
count: number;
|
|
480
|
+
}>;
|
|
481
|
+
tableRowCounts: Record<string, number>;
|
|
482
|
+
topCitedLast30d: Array<{
|
|
483
|
+
chunkId: number;
|
|
484
|
+
sourceFile: string;
|
|
485
|
+
section: string;
|
|
486
|
+
refCount: number;
|
|
487
|
+
}>;
|
|
488
|
+
staleUserModelSlots: Array<{
|
|
489
|
+
slot: string;
|
|
490
|
+
ageDays: number;
|
|
491
|
+
agentSlug: string | null;
|
|
492
|
+
}>;
|
|
493
|
+
staleHighSalienceChunks: Array<{
|
|
494
|
+
chunkId: number;
|
|
495
|
+
sourceFile: string;
|
|
496
|
+
section: string;
|
|
497
|
+
salience: number;
|
|
498
|
+
lastOutcomeScore: number;
|
|
499
|
+
}>;
|
|
500
|
+
chunkCacheStats: {
|
|
501
|
+
hits: number;
|
|
502
|
+
misses: number;
|
|
503
|
+
evictions: number;
|
|
504
|
+
size: number;
|
|
505
|
+
};
|
|
506
|
+
writeQueue: {
|
|
507
|
+
size: number;
|
|
508
|
+
dropped: number;
|
|
509
|
+
} | null;
|
|
510
|
+
lastIntegrityReport: {
|
|
511
|
+
ftsOk: boolean;
|
|
512
|
+
ftsRebuilt: boolean;
|
|
513
|
+
orphanRefsNulled: number;
|
|
514
|
+
missingEmbeddings: number;
|
|
515
|
+
ranAt: string;
|
|
516
|
+
} | null;
|
|
517
|
+
dbSizeBytes: number;
|
|
518
|
+
lastVacuumAt: string | null;
|
|
519
|
+
denseEmbeddings: {
|
|
520
|
+
withDense: number;
|
|
521
|
+
total: number;
|
|
522
|
+
models: Array<{
|
|
523
|
+
model: string;
|
|
524
|
+
count: number;
|
|
525
|
+
}>;
|
|
526
|
+
currentModel: string;
|
|
527
|
+
ready: boolean;
|
|
528
|
+
};
|
|
529
|
+
};
|
|
530
|
+
logAutonomyEvent(row: {
|
|
531
|
+
component: string;
|
|
532
|
+
event: string;
|
|
533
|
+
agentSlug?: string | null;
|
|
534
|
+
details?: Record<string, unknown>;
|
|
535
|
+
}): void;
|
|
536
|
+
queryAutonomyLog(opts?: {
|
|
537
|
+
component?: string;
|
|
538
|
+
event?: string;
|
|
539
|
+
agentSlug?: string;
|
|
540
|
+
limit?: number;
|
|
541
|
+
since?: string;
|
|
542
|
+
}): Array<{
|
|
543
|
+
id: number;
|
|
544
|
+
component: string;
|
|
545
|
+
event: string;
|
|
546
|
+
agentSlug: string | null;
|
|
547
|
+
details: Record<string, unknown>;
|
|
548
|
+
createdAt: string;
|
|
549
|
+
}>;
|
|
462
550
|
};
|
|
463
551
|
export declare function getStore(): Promise<MemoryStoreType>;
|
|
464
552
|
export declare function getStoreSync(): MemoryStoreType | null;
|