clementine-agent 1.18.90 → 1.18.91
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/cli/dashboard.js +77 -1
- package/dist/gateway/router.d.ts +11 -0
- package/dist/gateway/router.js +31 -0
- package/package.json +1 -1
package/dist/cli/dashboard.js
CHANGED
|
@@ -4587,6 +4587,56 @@ export async function cmdDashboard(opts) {
|
|
|
4587
4587
|
});
|
|
4588
4588
|
// SSE events handler moved before auth middleware (see above)
|
|
4589
4589
|
// ── POST routes (actions) ──────────────────────────────────────
|
|
4590
|
+
// PRD §10 / 1.18.91: cancel an in-flight cron run. The daemon runs every
|
|
4591
|
+
// cron job in-process (cron-running.json's `pid` is the daemon's PID — we
|
|
4592
|
+
// can't SIGTERM it without crashing everything else). Instead we look up
|
|
4593
|
+
// the job's AbortController in the gateway registry and abort it; the SDK
|
|
4594
|
+
// path on runAgentCron honors the signal and unwinds cleanly. The
|
|
4595
|
+
// CronScheduler's own catch path then writes the closing CronRunEntry
|
|
4596
|
+
// with status='error' + an "AbortError" message.
|
|
4597
|
+
app.post('/api/cron/run/:job/cancel', async (req, res) => {
|
|
4598
|
+
try {
|
|
4599
|
+
const jobName = req.params.job;
|
|
4600
|
+
if (!jobName) {
|
|
4601
|
+
res.status(400).json({ ok: false, error: 'job required' });
|
|
4602
|
+
return;
|
|
4603
|
+
}
|
|
4604
|
+
const gw = await getGateway();
|
|
4605
|
+
if (!gw || typeof gw.cancelCronJob !== 'function') {
|
|
4606
|
+
res.status(503).json({ ok: false, error: 'gateway not initialized — try again in a moment' });
|
|
4607
|
+
return;
|
|
4608
|
+
}
|
|
4609
|
+
const runningFile = path.join(BASE_DIR, 'cron-running.json');
|
|
4610
|
+
let runId;
|
|
4611
|
+
if (existsSync(runningFile)) {
|
|
4612
|
+
try {
|
|
4613
|
+
const entries = JSON.parse(readFileSync(runningFile, 'utf-8'));
|
|
4614
|
+
const match = Array.isArray(entries) ? entries.find((e) => String(e.jobName ?? '').toLowerCase() === jobName.toLowerCase()) : null;
|
|
4615
|
+
if (match?.runId)
|
|
4616
|
+
runId = match.runId;
|
|
4617
|
+
}
|
|
4618
|
+
catch { /* non-fatal — cancel can still proceed without runId */ }
|
|
4619
|
+
}
|
|
4620
|
+
const aborted = gw.cancelCronJob(jobName, 'cancelled-by-dashboard');
|
|
4621
|
+
if (!aborted) {
|
|
4622
|
+
res.status(404).json({ ok: false, error: `Job "${jobName}" is not running on this daemon` });
|
|
4623
|
+
return;
|
|
4624
|
+
}
|
|
4625
|
+
// Broadcast so other tabs drop the running card without polling.
|
|
4626
|
+
try {
|
|
4627
|
+
broadcastEvent({ type: 'cron_cancelled', data: { job: jobName, runId } });
|
|
4628
|
+
}
|
|
4629
|
+
catch { /* non-fatal */ }
|
|
4630
|
+
res.json({
|
|
4631
|
+
ok: true,
|
|
4632
|
+
message: `Cancel signal sent to "${jobName}". The runner will close the run within a few seconds.`,
|
|
4633
|
+
runId,
|
|
4634
|
+
});
|
|
4635
|
+
}
|
|
4636
|
+
catch (err) {
|
|
4637
|
+
res.status(500).json({ ok: false, error: String(err) });
|
|
4638
|
+
}
|
|
4639
|
+
});
|
|
4590
4640
|
app.post('/api/cron/run/:job', (req, res) => {
|
|
4591
4641
|
const jobName = req.params.job;
|
|
4592
4642
|
try {
|
|
@@ -22682,6 +22732,24 @@ async function toggleCronJob(name) {
|
|
|
22682
22732
|
} catch(e) { toast('Failed to toggle: ' + e, 'error'); }
|
|
22683
22733
|
}
|
|
22684
22734
|
|
|
22735
|
+
// PRD §10 / 1.18.91: SIGTERM the in-flight runner for this cron job. The
|
|
22736
|
+
// daemon endpoint reads PID from cron-running.json (already written by
|
|
22737
|
+
// CronScheduler). The runner's signal handler closes the run gracefully —
|
|
22738
|
+
// the SSE cron_cancelled broadcast will refresh the card.
|
|
22739
|
+
async function cancelCronRun(name) {
|
|
22740
|
+
if (!confirm('Cancel the in-flight run for "' + name + '"? The runner will stop within a few seconds.')) return;
|
|
22741
|
+
try {
|
|
22742
|
+
var r = await apiFetch('/api/cron/run/' + encodeURIComponent(name) + '/cancel', { method: 'POST' });
|
|
22743
|
+
var d = await r.json();
|
|
22744
|
+
if (!r.ok || d.ok === false) {
|
|
22745
|
+
toast(d.error || ('Cancel failed (HTTP ' + r.status + ')'), 'error');
|
|
22746
|
+
return;
|
|
22747
|
+
}
|
|
22748
|
+
toast(d.message || ('Cancel signal sent to ' + name), 'success');
|
|
22749
|
+
setTimeout(refreshCron, 800);
|
|
22750
|
+
} catch(e) { toast('Failed to cancel: ' + e, 'error'); }
|
|
22751
|
+
}
|
|
22752
|
+
|
|
22685
22753
|
async function toggleScheduledWorkflow(id) {
|
|
22686
22754
|
try {
|
|
22687
22755
|
var r = await apiFetch('/api/builder/workflows/' + encodeURIComponent(id));
|
|
@@ -23663,6 +23731,14 @@ function renderScheduledTaskCard(task) {
|
|
|
23663
23731
|
badges += '<span class="badge ' + (enabled ? 'badge-green' : 'badge-gray') + '">' + (enabled ? 'Enabled' : 'Disabled') + '</span>';
|
|
23664
23732
|
badges += '<span class="badge ' + (task.health === 'broken' || task.health === 'failed' ? 'badge-yellow' : 'badge-gray') + '">' + esc(task.healthLabel || task.health) + '</span>';
|
|
23665
23733
|
var safeName = jsStr(task.name);
|
|
23734
|
+
// PRD §10 / 1.18.91: when a task is mid-flight, Run Now is meaningless and
|
|
23735
|
+
// would race against the concurrency lock; replace it with a Cancel button
|
|
23736
|
+
// that calls /api/cron/run/:job/cancel (SIGTERMs the runner via the PID
|
|
23737
|
+
// recorded in cron-running.json).
|
|
23738
|
+
var isRunning = task.health === 'running';
|
|
23739
|
+
var runOrCancelBtn = isRunning
|
|
23740
|
+
? '<button class="btn-sm secondary btn-danger" onclick="cancelCronRun(\\x27' + safeName + '\\x27)" title="Stop this in-flight run (SIGTERM)">Cancel</button>'
|
|
23741
|
+
: '<button class="btn-sm secondary btn-success" onclick="apiPost(\\x27/api/cron/run/' + encodeURIComponent(task.name) + '\\x27)" title="Run this task once now">Run Now</button>';
|
|
23666
23742
|
return '<div class="' + cardCls + '" style="' + style + '">'
|
|
23667
23743
|
+ '<div class="task-card-header"><strong>' + esc(task.displayName || task.name) + '</strong>'
|
|
23668
23744
|
+ '<label class="toggle-switch"><input type="checkbox"' + (enabled ? ' checked' : '') + ' onchange="toggleCronJob(\\x27' + safeName + '\\x27)"><span class="toggle-slider"></span></label></div>'
|
|
@@ -23674,7 +23750,7 @@ function renderScheduledTaskCard(task) {
|
|
|
23674
23750
|
+ '<div class="task-card-badges">' + badges + '</div>'
|
|
23675
23751
|
+ '<div class="task-card-actions">'
|
|
23676
23752
|
+ '<button class="btn-sm primary" onclick="openEditCronModal(\\x27' + safeName + '\\x27)" title="Edit task" style="background:var(--bg-tertiary);border:1px solid var(--border);color:var(--text-primary)">Edit</button>'
|
|
23677
|
-
+
|
|
23753
|
+
+ runOrCancelBtn
|
|
23678
23754
|
+ '<button class="btn-sm secondary" onclick="openCronPreview(\\x27' + safeName + '\\x27)" title="See exactly what will run">Preview</button>'
|
|
23679
23755
|
+ '<button class="btn-sm secondary" data-trace-job="' + esc(task.name) + '" title="View execution trace">Trace</button>'
|
|
23680
23756
|
+ '<button class="btn-sm secondary btn-danger" onclick="confirmDeleteCron(\\x27' + safeName + '\\x27)" title="Delete task">Del</button>'
|
package/dist/gateway/router.d.ts
CHANGED
|
@@ -30,6 +30,11 @@ export declare class Gateway {
|
|
|
30
30
|
* `CronRunEntry`. Mirrors the `consumeLastTerminalReason` pattern so we
|
|
31
31
|
* don't have to refactor `handleCronJob`'s positional return shape. */
|
|
32
32
|
private _lastCronRunMetadata?;
|
|
33
|
+
/** PRD §10 / 1.18.91: registry of in-flight cron AbortControllers keyed by
|
|
34
|
+
* jobName. Lets the dashboard cancel endpoint abort an in-progress run
|
|
35
|
+
* without SIGTERMing the whole daemon. Populated/cleaned up by
|
|
36
|
+
* handleCronJob. */
|
|
37
|
+
private cronAbortControllers;
|
|
33
38
|
/** Persisted set of channel keys the owner has approved. Loaded lazily. */
|
|
34
39
|
private seenChannels;
|
|
35
40
|
private _authFailCount;
|
|
@@ -178,6 +183,12 @@ export declare class Gateway {
|
|
|
178
183
|
_mode?: 'standard' | 'unleashed', maxHours?: number, timeoutMs?: number, successCriteria?: string[], agentSlug?: string, pinnedSkills?: string[], allowedTools?: string[], allowedMcpServers?: string[],
|
|
179
184
|
/** Predictable (contract) mode — runner skips memory/team/auto-skills. */
|
|
180
185
|
predictable?: boolean): Promise<string>;
|
|
186
|
+
/**
|
|
187
|
+
* PRD §10 / 1.18.91 — cancel an in-flight cron run by name. Returns true if
|
|
188
|
+
* an AbortController was found and abort() was called, false if nothing was
|
|
189
|
+
* registered (job wasn't running on this daemon). Safe to call repeatedly.
|
|
190
|
+
*/
|
|
191
|
+
cancelCronJob(jobName: string, reason?: string): boolean;
|
|
181
192
|
/**
|
|
182
193
|
* Process a team message as an autonomous task — same multi-phase execution
|
|
183
194
|
* as cron unleashed jobs, so agents can work until done instead of being
|
package/dist/gateway/router.js
CHANGED
|
@@ -112,6 +112,11 @@ export class Gateway {
|
|
|
112
112
|
* `CronRunEntry`. Mirrors the `consumeLastTerminalReason` pattern so we
|
|
113
113
|
* don't have to refactor `handleCronJob`'s positional return shape. */
|
|
114
114
|
_lastCronRunMetadata;
|
|
115
|
+
/** PRD §10 / 1.18.91: registry of in-flight cron AbortControllers keyed by
|
|
116
|
+
* jobName. Lets the dashboard cancel endpoint abort an in-progress run
|
|
117
|
+
* without SIGTERMing the whole daemon. Populated/cleaned up by
|
|
118
|
+
* handleCronJob. */
|
|
119
|
+
cronAbortControllers = new Map();
|
|
115
120
|
/** Persisted set of channel keys the owner has approved. Loaded lazily. */
|
|
116
121
|
seenChannels = null;
|
|
117
122
|
// Auth circuit breaker — suppresses repeated error spam after consecutive failures
|
|
@@ -1987,6 +1992,10 @@ export class Gateway {
|
|
|
1987
1992
|
cronAc.abort();
|
|
1988
1993
|
logger.warn({ jobName, wallMs }, 'Cron job hit wall-clock cap — aborting');
|
|
1989
1994
|
}, wallMs);
|
|
1995
|
+
// PRD §10 / 1.18.91: register so the dashboard cancel endpoint can find
|
|
1996
|
+
// and abort this controller. Last-write-wins if a duplicate fires (the
|
|
1997
|
+
// concurrency lock prevents that for manual runs, but be defensive).
|
|
1998
|
+
this.cronAbortControllers.set(jobName, cronAc);
|
|
1990
1999
|
try {
|
|
1991
2000
|
logger.info(`Running cron job: ${jobName}${workDir ? ` in ${workDir}` : ''}${agentSlug && agentSlug !== 'clementine' ? ` as ${agentSlug}` : ''}`);
|
|
1992
2001
|
const cronStart = Date.now();
|
|
@@ -2047,7 +2056,29 @@ export class Gateway {
|
|
|
2047
2056
|
finally {
|
|
2048
2057
|
clearTimeout(cronTimer);
|
|
2049
2058
|
releaseLane();
|
|
2059
|
+
// PRD §10 / 1.18.91: deregister only if we're still the owner (a
|
|
2060
|
+
// theoretical re-entry could have replaced us; don't clobber).
|
|
2061
|
+
if (this.cronAbortControllers.get(jobName) === cronAc) {
|
|
2062
|
+
this.cronAbortControllers.delete(jobName);
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
/**
|
|
2067
|
+
* PRD §10 / 1.18.91 — cancel an in-flight cron run by name. Returns true if
|
|
2068
|
+
* an AbortController was found and abort() was called, false if nothing was
|
|
2069
|
+
* registered (job wasn't running on this daemon). Safe to call repeatedly.
|
|
2070
|
+
*/
|
|
2071
|
+
cancelCronJob(jobName, reason = 'cancelled-by-dashboard') {
|
|
2072
|
+
const ac = this.cronAbortControllers.get(jobName);
|
|
2073
|
+
if (!ac)
|
|
2074
|
+
return false;
|
|
2075
|
+
if (!ac.signal.aborted) {
|
|
2076
|
+
try {
|
|
2077
|
+
ac.abort(reason);
|
|
2078
|
+
}
|
|
2079
|
+
catch { /* ignore */ }
|
|
2050
2080
|
}
|
|
2081
|
+
return true;
|
|
2051
2082
|
}
|
|
2052
2083
|
// ── Team task execution ──────────────────────────────────────────────
|
|
2053
2084
|
/**
|