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.
@@ -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
- const deepWorthy = strongCount >= 2;
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
  '',
@@ -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) => { res.json({ ok: true, ts: Date.now() }); });
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
  }
@@ -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
  *
@@ -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
  *
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.16.0",
3
+ "version": "1.18.0",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",