clementine-agent 1.18.75 → 1.18.76
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/advisor-rules/engine.js +7 -3
- package/dist/agent/execution-advisor.js +1 -1
- package/dist/channels/discord-agent-bot.js +1 -1
- package/dist/cli/cron.js +1 -1
- package/dist/cli/dashboard.js +38 -16
- package/dist/gateway/job-health.js +10 -2
- package/dist/types.d.ts +11 -2
- package/package.json +1 -1
|
@@ -70,14 +70,18 @@ export function evaluateWhen(c, ctx) {
|
|
|
70
70
|
const lastRun = ctx.recentRuns[0];
|
|
71
71
|
if (!lastRun)
|
|
72
72
|
return false;
|
|
73
|
-
|
|
73
|
+
// finishedAt is optional (in-progress runs are 'running' with no
|
|
74
|
+
// finishedAt). Fall back to startedAt so the rule still has a clock.
|
|
75
|
+
const lastRunTime = new Date(lastRun.finishedAt ?? lastRun.startedAt).getTime();
|
|
74
76
|
return ctx.nowMs - lastRunTime > c.ms;
|
|
75
77
|
}
|
|
76
78
|
case 'lastRunWithinMs': {
|
|
77
79
|
const lastRun = ctx.recentRuns[0];
|
|
78
80
|
if (!lastRun)
|
|
79
81
|
return false;
|
|
80
|
-
|
|
82
|
+
// finishedAt is optional (in-progress runs are 'running' with no
|
|
83
|
+
// finishedAt). Fall back to startedAt so the rule still has a clock.
|
|
84
|
+
const lastRunTime = new Date(lastRun.finishedAt ?? lastRun.startedAt).getTime();
|
|
81
85
|
return ctx.nowMs - lastRunTime <= c.ms;
|
|
82
86
|
}
|
|
83
87
|
case 'noRecentRuns':
|
|
@@ -185,7 +189,7 @@ const TEMPLATE_VARS = {
|
|
|
185
189
|
const lastRun = ctx.recentRuns[0];
|
|
186
190
|
if (!lastRun)
|
|
187
191
|
return 0;
|
|
188
|
-
const lastRunTime = new Date(lastRun.finishedAt).getTime();
|
|
192
|
+
const lastRunTime = new Date(lastRun.finishedAt ?? lastRun.startedAt).getTime();
|
|
189
193
|
const elapsed = ctx.nowMs - lastRunTime;
|
|
190
194
|
const cooldown = 60 * 60 * 1000;
|
|
191
195
|
return Math.max(0, Math.ceil((cooldown - elapsed) / 60_000));
|
|
@@ -75,7 +75,7 @@ function computeLegacyAdvice(jobName, job) {
|
|
|
75
75
|
advice.skipReason = `${consecutiveErrors} consecutive errors — circuit breaker engaged`;
|
|
76
76
|
return advice;
|
|
77
77
|
}
|
|
78
|
-
const lastRunTime = new Date(lastRun.finishedAt).getTime();
|
|
78
|
+
const lastRunTime = new Date(lastRun.finishedAt ?? lastRun.startedAt).getTime();
|
|
79
79
|
const elapsed = Date.now() - lastRunTime;
|
|
80
80
|
if (elapsed < CIRCUIT_BREAKER_COOLDOWN_MS) {
|
|
81
81
|
advice.shouldSkip = true;
|
|
@@ -366,7 +366,7 @@ export class AgentBotClient {
|
|
|
366
366
|
if (recent.length > 0) {
|
|
367
367
|
const r = recent[0];
|
|
368
368
|
const icon = r.status === 'ok' ? '\u2705' : r.status === 'error' ? '\u274C' : '\u23ED';
|
|
369
|
-
const ago = Math.round((Date.now() - new Date(r.finishedAt).getTime()) / 60000);
|
|
369
|
+
const ago = Math.round((Date.now() - new Date(r.finishedAt ?? r.startedAt).getTime()) / 60000);
|
|
370
370
|
lastRunLines.push(`${icon} ${display} \u2014 ${formatAgentDuration(ago)} ago`);
|
|
371
371
|
}
|
|
372
372
|
else {
|
package/dist/cli/cron.js
CHANGED
|
@@ -48,7 +48,7 @@ export async function cmdCronList() {
|
|
|
48
48
|
const status = job.enabled ? 'enabled' : 'disabled';
|
|
49
49
|
const recent = runLog.readRecent(job.name, 1);
|
|
50
50
|
const lastRun = recent.length > 0
|
|
51
|
-
? `last run: ${recent[0].finishedAt.slice(0, 16).replace('T', ' ')} (${recent[0].status})`
|
|
51
|
+
? `last run: ${(recent[0].finishedAt ?? recent[0].startedAt).slice(0, 16).replace('T', ' ')} (${recent[0].status})`
|
|
52
52
|
: 'never run';
|
|
53
53
|
const errors = runLog.consecutiveErrors(job.name);
|
|
54
54
|
const errorTag = errors > 0 ? ` [${errors} consecutive error(s)]` : '';
|
package/dist/cli/dashboard.js
CHANGED
|
@@ -1350,20 +1350,13 @@ function getCronJobs() {
|
|
|
1350
1350
|
}
|
|
1351
1351
|
catch { /* ignore */ }
|
|
1352
1352
|
}
|
|
1353
|
-
// Attach recent run history
|
|
1354
|
-
|
|
1353
|
+
// Attach recent run history. Single source of truth via CronRunLog.readRecent
|
|
1354
|
+
// — same path the new /api/cron/runs cross-job endpoint uses, so per-card
|
|
1355
|
+
// last-run and the Recent History zone never disagree.
|
|
1356
|
+
const log = new CronRunLog();
|
|
1355
1357
|
const enriched = jobs.map((job) => {
|
|
1356
1358
|
const name = String(job.name ?? '');
|
|
1357
|
-
const
|
|
1358
|
-
const logPath = path.join(runsDir, `${safe}.jsonl`);
|
|
1359
|
-
let recentRuns = [];
|
|
1360
|
-
if (existsSync(logPath)) {
|
|
1361
|
-
try {
|
|
1362
|
-
const lines = readFileSync(logPath, 'utf-8').trim().split('\n').filter(Boolean);
|
|
1363
|
-
recentRuns = lines.slice(-10).map((l) => JSON.parse(l)).reverse();
|
|
1364
|
-
}
|
|
1365
|
-
catch { /* ignore */ }
|
|
1366
|
-
}
|
|
1359
|
+
const recentRuns = log.readRecent(name, 10);
|
|
1367
1360
|
return { ...job, recentRuns };
|
|
1368
1361
|
});
|
|
1369
1362
|
return { jobs: enriched };
|
|
@@ -4597,6 +4590,25 @@ export async function cmdDashboard(opts) {
|
|
|
4597
4590
|
app.post('/api/cron/run/:job', (req, res) => {
|
|
4598
4591
|
const jobName = req.params.job;
|
|
4599
4592
|
try {
|
|
4593
|
+
// Concurrency lock (1.18.76+): the CronScheduler persists a sidecar
|
|
4594
|
+
// ~/.clementine/cron-running.json with one entry per in-flight job.
|
|
4595
|
+
// If a manual Run Now collides with an already-running job (whether
|
|
4596
|
+
// from another Run Now click or a scheduled tick), reject 409 so the
|
|
4597
|
+
// user gets immediate feedback instead of two parallel runs racing
|
|
4598
|
+
// each other into the same outputs.
|
|
4599
|
+
try {
|
|
4600
|
+
const runningFile = path.join(BASE_DIR, 'cron-running.json');
|
|
4601
|
+
if (existsSync(runningFile)) {
|
|
4602
|
+
const entries = JSON.parse(readFileSync(runningFile, 'utf-8'));
|
|
4603
|
+
const collision = Array.isArray(entries) && entries.find(e => String(e.jobName ?? '').toLowerCase() === jobName.toLowerCase());
|
|
4604
|
+
if (collision) {
|
|
4605
|
+
const since = collision.startedAt ? ` (running since ${collision.startedAt})` : '';
|
|
4606
|
+
res.status(409).json({ ok: false, error: `Job "${jobName}" is already running${since}` });
|
|
4607
|
+
return;
|
|
4608
|
+
}
|
|
4609
|
+
}
|
|
4610
|
+
}
|
|
4611
|
+
catch { /* if the sidecar is unreadable, fall through and let the runner handle dedup */ }
|
|
4600
4612
|
const child = spawn('node', [DIST_ENTRY, 'cron', 'run', jobName], {
|
|
4601
4613
|
detached: true,
|
|
4602
4614
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
@@ -6584,8 +6596,18 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
6584
6596
|
}
|
|
6585
6597
|
jobs[idx].enabled = !jobs[idx].enabled;
|
|
6586
6598
|
writeCronFileAt(cronFile, parsed, jobs);
|
|
6587
|
-
const
|
|
6588
|
-
|
|
6599
|
+
const enabled = jobs[idx].enabled;
|
|
6600
|
+
const state = enabled ? 'enabled' : 'disabled';
|
|
6601
|
+
// Broadcast so other open dashboard tabs flip the toggle without polling.
|
|
6602
|
+
try {
|
|
6603
|
+
broadcastEvent({ type: 'cron_toggled', data: { job: bareJobName, enabled } });
|
|
6604
|
+
}
|
|
6605
|
+
catch { /* non-fatal */ }
|
|
6606
|
+
// The CronScheduler's watchFile polls CRON.md every 2s and reloads
|
|
6607
|
+
// automatically (cron-scheduler.ts:756 watchCronFile), so the toggle
|
|
6608
|
+
// takes effect within 2 seconds without an explicit reload here. We
|
|
6609
|
+
// surface a hint to the user about that latency in the response.
|
|
6610
|
+
res.json({ ok: true, message: `${jobName} is now ${state}`, enabled });
|
|
6589
6611
|
}
|
|
6590
6612
|
catch (err) {
|
|
6591
6613
|
res.status(500).json({ error: String(err) });
|
|
@@ -23441,7 +23463,7 @@ async function openTraceViewer(jobName) {
|
|
|
23441
23463
|
traceData = d.traces || [];
|
|
23442
23464
|
|
|
23443
23465
|
if (traceData.length === 0) {
|
|
23444
|
-
document.getElementById('trace-content').innerHTML = '<div style="padding:
|
|
23466
|
+
document.getElementById('trace-content').innerHTML = '<div style="padding:24px;color:var(--text-muted);line-height:1.6"><div style="font-weight:500;color:var(--text-secondary);margin-bottom:8px">No traces recorded yet</div><div style="font-size:12px">Traces are captured per run and show every tool call the agent made.<br/>If a recent run errored before any tool was invoked, see the <strong>Recent history</strong> zone on the Tasks page for the error message.</div></div>';
|
|
23445
23467
|
document.getElementById('trace-run-selector').innerHTML = '';
|
|
23446
23468
|
return;
|
|
23447
23469
|
}
|
|
@@ -23493,7 +23515,7 @@ function renderTrace(idx) {
|
|
|
23493
23515
|
+ '</div>';
|
|
23494
23516
|
}
|
|
23495
23517
|
|
|
23496
|
-
document.getElementById('trace-content').innerHTML = html || '<div style="padding:
|
|
23518
|
+
document.getElementById('trace-content').innerHTML = html || '<div style="padding:24px;color:var(--text-muted);line-height:1.6"><div style="font-weight:500;color:var(--text-secondary);margin-bottom:8px">This run produced no tool calls</div><div style="font-size:12px">The run started but didn\\x27t reach any tool — it likely errored before the agent acted, or was a no-op response.<br/>Check <strong>Recent history</strong> on the Tasks page for the error message or output preview from this run.</div></div>';
|
|
23497
23519
|
}
|
|
23498
23520
|
|
|
23499
23521
|
// ── Inline Trace (Execution tab) ──
|
|
@@ -85,12 +85,20 @@ export function classifyRunHealth(entry) {
|
|
|
85
85
|
requiresApproval: true,
|
|
86
86
|
};
|
|
87
87
|
}
|
|
88
|
-
|
|
88
|
+
// 'timeout' and 'lost' are explicit terminal failure states (1.18.76+).
|
|
89
|
+
// 'timeout' = the job exceeded max_hours. 'lost' = the daemon-boot sweep
|
|
90
|
+
// closed an orphaned 'running' entry that never got a finishedAt because
|
|
91
|
+
// the daemon crashed mid-run.
|
|
92
|
+
if (entry.status === 'error' || entry.status === 'retried' || entry.status === 'timeout' || entry.status === 'lost') {
|
|
89
93
|
return {
|
|
90
94
|
...base,
|
|
91
95
|
status: 'failed',
|
|
92
96
|
evidence: compactEvidence(entry.error, entry.outputPreview, terminalReason ? `terminalReason=${terminalReason}` : undefined),
|
|
93
|
-
recommendedAction:
|
|
97
|
+
recommendedAction: entry.status === 'lost'
|
|
98
|
+
? 'The daemon crashed before this run completed. Check the daemon logs around the startedAt timestamp; if this repeats, lower max_hours or investigate the underlying job.'
|
|
99
|
+
: entry.status === 'timeout'
|
|
100
|
+
? 'The job exceeded its time budget. Lower max_hours, narrow the prompt, or split into smaller steps.'
|
|
101
|
+
: 'Inspect the latest error and current job definition before applying a fix.',
|
|
94
102
|
requiresApproval: true,
|
|
95
103
|
};
|
|
96
104
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -392,8 +392,17 @@ export type TerminalReason = 'blocking_limit' | 'rapid_refill_breaker' | 'prompt
|
|
|
392
392
|
export interface CronRunEntry {
|
|
393
393
|
jobName: string;
|
|
394
394
|
startedAt: string;
|
|
395
|
-
|
|
396
|
-
|
|
395
|
+
/** Optional: in-progress runs are appended with status='running' before the
|
|
396
|
+
* finishedAt is known. The runner replaces or supersedes the entry on
|
|
397
|
+
* completion. The stale-running sweep emits a closing 'lost' entry for
|
|
398
|
+
* any 'running' row whose startedAt has aged past the deadline. */
|
|
399
|
+
finishedAt?: string;
|
|
400
|
+
/** 'ok' | 'error' | 'retried' | 'skipped' are terminal. 'running' is in-flight.
|
|
401
|
+
* 'timeout' fires when max_hours is exceeded. 'lost' is appended by the
|
|
402
|
+
* daemon-boot stale sweep when a 'running' entry has no companion close
|
|
403
|
+
* (daemon likely crashed mid-run). */
|
|
404
|
+
status: 'ok' | 'error' | 'retried' | 'skipped' | 'running' | 'timeout' | 'lost';
|
|
405
|
+
/** 0 for in-progress 'running' rows; populated when terminal. */
|
|
397
406
|
durationMs: number;
|
|
398
407
|
error?: string;
|
|
399
408
|
errorType?: 'transient' | 'permanent';
|