clementine-agent 1.0.13 → 1.0.15
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 -2
- package/dist/agent/self-improve.js +23 -0
- package/dist/agent/skill-extractor.d.ts +10 -0
- package/dist/agent/skill-extractor.js +61 -0
- package/dist/channels/discord-agent-bot.d.ts +4 -0
- package/dist/channels/discord-agent-bot.js +35 -0
- package/dist/channels/discord-bot-manager.d.ts +4 -0
- package/dist/channels/discord-bot-manager.js +16 -0
- package/dist/channels/discord.js +141 -0
- package/dist/channels/slack.js +51 -1
- package/dist/channels/telegram.js +28 -1
- package/dist/cli/dashboard.js +299 -5
- package/dist/gateway/cron-scheduler.d.ts +5 -0
- package/dist/gateway/cron-scheduler.js +32 -5
- package/dist/gateway/failure-monitor.d.ts +40 -0
- package/dist/gateway/failure-monitor.js +416 -0
- package/dist/gateway/fix-verification.d.ts +39 -0
- package/dist/gateway/fix-verification.js +144 -0
- package/dist/gateway/heartbeat-scheduler.js +61 -4
- package/dist/gateway/notifications.js +26 -1
- package/dist/gateway/router.js +2 -2
- package/dist/memory/store.d.ts +20 -0
- package/dist/memory/store.js +64 -0
- package/dist/types.d.ts +3 -0
- package/package.json +1 -1
package/dist/cli/dashboard.js
CHANGED
|
@@ -135,8 +135,18 @@ async function cachedAsync(key, ttlMs, compute) {
|
|
|
135
135
|
// ── Lazy gateway for chat ────────────────────────────────────────────
|
|
136
136
|
let gatewayInstance = null;
|
|
137
137
|
let gatewayInitializing = false;
|
|
138
|
+
let gatewayDispatcher = null;
|
|
139
|
+
/** SSE broadcaster; set once cmdDashboard has built the SSE infrastructure. */
|
|
140
|
+
let dashboardSseBroadcast = null;
|
|
138
141
|
/** Reset the cached gateway (called when daemon PID changes). */
|
|
139
142
|
function resetGateway() {
|
|
143
|
+
if (gatewayDispatcher) {
|
|
144
|
+
try {
|
|
145
|
+
gatewayDispatcher.shutdown();
|
|
146
|
+
}
|
|
147
|
+
catch { /* best-effort */ }
|
|
148
|
+
gatewayDispatcher = null;
|
|
149
|
+
}
|
|
140
150
|
gatewayInstance = null;
|
|
141
151
|
responseCache.clear();
|
|
142
152
|
}
|
|
@@ -156,6 +166,26 @@ async function getGateway() {
|
|
|
156
166
|
gatewayInstance = new GatewayClass(assistant);
|
|
157
167
|
const { setApprovalCallback } = await import('../agent/hooks.js');
|
|
158
168
|
setApprovalCallback(async () => false);
|
|
169
|
+
// Wire a local NotificationDispatcher so deep-task results launched from
|
|
170
|
+
// dashboard chat sessions can be pushed back into the browser via SSE.
|
|
171
|
+
try {
|
|
172
|
+
const { NotificationDispatcher } = await import('../gateway/notifications.js');
|
|
173
|
+
const dispatcher = new NotificationDispatcher();
|
|
174
|
+
dispatcher.register('dashboard', async (text, context) => {
|
|
175
|
+
if (!dashboardSseBroadcast)
|
|
176
|
+
return;
|
|
177
|
+
dashboardSseBroadcast({
|
|
178
|
+
type: 'deep_result',
|
|
179
|
+
data: { sessionKey: context?.sessionKey ?? null, text },
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
gatewayInstance.setDispatcher(dispatcher);
|
|
183
|
+
gatewayDispatcher = dispatcher;
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
// Non-fatal — deep-task results from dashboard sessions just won't surface live
|
|
187
|
+
console.warn('Failed to wire dashboard SSE dispatcher:', err);
|
|
188
|
+
}
|
|
159
189
|
return gatewayInstance;
|
|
160
190
|
}
|
|
161
191
|
catch (err) {
|
|
@@ -2009,6 +2039,8 @@ export async function cmdDashboard(opts) {
|
|
|
2009
2039
|
}
|
|
2010
2040
|
}
|
|
2011
2041
|
}
|
|
2042
|
+
// Let the lazy-gateway dispatcher publish deep_result events through SSE.
|
|
2043
|
+
dashboardSseBroadcast = broadcastEvent;
|
|
2012
2044
|
// SSE events handler moved before auth middleware (see above)
|
|
2013
2045
|
// ── POST routes (actions) ──────────────────────────────────────
|
|
2014
2046
|
app.post('/api/cron/run/:job', (req, res) => {
|
|
@@ -2043,6 +2075,16 @@ export async function cmdDashboard(opts) {
|
|
|
2043
2075
|
res.status(500).json({ error: String(err) });
|
|
2044
2076
|
}
|
|
2045
2077
|
});
|
|
2078
|
+
// ── Broken jobs (failure monitor) ───────────────────────────────
|
|
2079
|
+
app.get('/api/cron/broken-jobs', async (_req, res) => {
|
|
2080
|
+
try {
|
|
2081
|
+
const { computeBrokenJobs } = await import('../gateway/failure-monitor.js');
|
|
2082
|
+
res.json({ jobs: computeBrokenJobs() });
|
|
2083
|
+
}
|
|
2084
|
+
catch (err) {
|
|
2085
|
+
res.status(500).json({ error: String(err) });
|
|
2086
|
+
}
|
|
2087
|
+
});
|
|
2046
2088
|
// ── Cron trace viewer ──────────────────────────────────────────
|
|
2047
2089
|
app.get('/api/cron/traces/:job', (req, res) => {
|
|
2048
2090
|
try {
|
|
@@ -3743,19 +3785,87 @@ export async function cmdDashboard(opts) {
|
|
|
3743
3785
|
}
|
|
3744
3786
|
});
|
|
3745
3787
|
// ── Skills (Procedural Memory) API ──────────────────────────────────
|
|
3746
|
-
|
|
3788
|
+
// NOTE: /api/skills/pending routes must come before /api/skills/:name so
|
|
3789
|
+
// Express doesn't capture "pending" as a :name param.
|
|
3790
|
+
app.get('/api/skills/pending', async (_req, res) => {
|
|
3791
|
+
try {
|
|
3792
|
+
const { listPendingSkills } = await import('../agent/skill-extractor.js');
|
|
3793
|
+
res.json({ skills: listPendingSkills() });
|
|
3794
|
+
}
|
|
3795
|
+
catch (err) {
|
|
3796
|
+
res.status(500).json({ error: String(err) });
|
|
3797
|
+
}
|
|
3798
|
+
});
|
|
3799
|
+
app.post('/api/skills/pending/:name/approve', async (req, res) => {
|
|
3800
|
+
try {
|
|
3801
|
+
const { approvePendingSkill } = await import('../agent/skill-extractor.js');
|
|
3802
|
+
const result = approvePendingSkill(req.params.name);
|
|
3803
|
+
if (!result.ok) {
|
|
3804
|
+
res.status(404).json(result);
|
|
3805
|
+
return;
|
|
3806
|
+
}
|
|
3807
|
+
res.json(result);
|
|
3808
|
+
}
|
|
3809
|
+
catch (err) {
|
|
3810
|
+
res.status(500).json({ error: String(err) });
|
|
3811
|
+
}
|
|
3812
|
+
});
|
|
3813
|
+
app.post('/api/skills/pending/:name/reject', async (req, res) => {
|
|
3814
|
+
try {
|
|
3815
|
+
const { rejectPendingSkill } = await import('../agent/skill-extractor.js');
|
|
3816
|
+
const result = rejectPendingSkill(req.params.name);
|
|
3817
|
+
if (!result.ok) {
|
|
3818
|
+
res.status(404).json(result);
|
|
3819
|
+
return;
|
|
3820
|
+
}
|
|
3821
|
+
res.json(result);
|
|
3822
|
+
}
|
|
3823
|
+
catch (err) {
|
|
3824
|
+
res.status(500).json({ error: String(err) });
|
|
3825
|
+
}
|
|
3826
|
+
});
|
|
3827
|
+
app.get('/api/skills', async (_req, res) => {
|
|
3747
3828
|
try {
|
|
3748
3829
|
const skillsDir = path.join(VAULT_DIR, '00-System', 'skills');
|
|
3749
3830
|
if (!existsSync(skillsDir)) {
|
|
3750
3831
|
res.json({ skills: [] });
|
|
3751
3832
|
return;
|
|
3752
3833
|
}
|
|
3834
|
+
// Aggregate last-7-day retrieval stats from skill_usage table (best-effort).
|
|
3835
|
+
const usageStats = new Map();
|
|
3836
|
+
if (existsSync(MEMORY_DB_PATH)) {
|
|
3837
|
+
try {
|
|
3838
|
+
const Database = (await import('better-sqlite3')).default;
|
|
3839
|
+
const db = new Database(MEMORY_DB_PATH, { readonly: true });
|
|
3840
|
+
try {
|
|
3841
|
+
const rows = db.prepare(`SELECT skill_name,
|
|
3842
|
+
COUNT(*) AS retrievals,
|
|
3843
|
+
MAX(retrieved_at) AS last_retrieved_at,
|
|
3844
|
+
AVG(score) AS avg_score
|
|
3845
|
+
FROM skill_usage
|
|
3846
|
+
WHERE retrieved_at >= datetime('now', '-7 days')
|
|
3847
|
+
GROUP BY skill_name`).all();
|
|
3848
|
+
for (const r of rows) {
|
|
3849
|
+
usageStats.set(r.skill_name, {
|
|
3850
|
+
retrievals7d: r.retrievals,
|
|
3851
|
+
lastRetrievedAt: r.last_retrieved_at,
|
|
3852
|
+
avgScore: r.avg_score,
|
|
3853
|
+
});
|
|
3854
|
+
}
|
|
3855
|
+
}
|
|
3856
|
+
catch { /* skill_usage may not exist on older DBs */ }
|
|
3857
|
+
db.close();
|
|
3858
|
+
}
|
|
3859
|
+
catch { /* non-fatal */ }
|
|
3860
|
+
}
|
|
3753
3861
|
const files = readdirSync(skillsDir).filter(f => f.endsWith('.md'));
|
|
3754
3862
|
const skills = files.map(f => {
|
|
3755
3863
|
try {
|
|
3756
3864
|
const parsed = matter(readFileSync(path.join(skillsDir, f), 'utf-8'));
|
|
3865
|
+
const name = f.replace('.md', '');
|
|
3866
|
+
const stats = usageStats.get(name);
|
|
3757
3867
|
return {
|
|
3758
|
-
name
|
|
3868
|
+
name,
|
|
3759
3869
|
title: parsed.data.title ?? f,
|
|
3760
3870
|
description: parsed.data.description ?? '',
|
|
3761
3871
|
source: parsed.data.source ?? 'unknown',
|
|
@@ -3766,6 +3876,9 @@ export async function cmdDashboard(opts) {
|
|
|
3766
3876
|
lastUsed: parsed.data.lastUsed ?? null,
|
|
3767
3877
|
createdAt: parsed.data.createdAt ?? '',
|
|
3768
3878
|
updatedAt: parsed.data.updatedAt ?? '',
|
|
3879
|
+
retrievals7d: stats?.retrievals7d ?? 0,
|
|
3880
|
+
lastRetrievedAt: stats?.lastRetrievedAt ?? null,
|
|
3881
|
+
avgScore: stats?.avgScore ?? null,
|
|
3769
3882
|
};
|
|
3770
3883
|
}
|
|
3771
3884
|
catch {
|
|
@@ -8972,15 +9085,25 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
8972
9085
|
<div class="page-title">Scheduled Tasks</div>
|
|
8973
9086
|
<div class="tab-bar" id="automations-tabs">
|
|
8974
9087
|
<button class="active" onclick="switchTab('automations','scheduled')">Scheduled Tasks</button>
|
|
9088
|
+
<button onclick="switchTab('automations','broken')">Broken Jobs <span class="tab-badge" id="tab-broken-count" title="repeatedly failing" style="display:none;background:#ef4444;color:#fff">0</span></button>
|
|
8975
9089
|
<button onclick="switchTab('automations','timers')">Timers <span class="tab-badge" id="tab-timer-count" style="display:none">0</span></button>
|
|
8976
9090
|
<button onclick="switchTab('automations','self-improve')">Self-Improve <span class="tab-badge" id="tab-si-pending" style="display:none">0</span></button>
|
|
8977
|
-
<button onclick="switchTab('automations','skills')">Skills <span class="tab-badge" id="tab-skill-count" style="display:none">0</span></button>
|
|
9091
|
+
<button onclick="switchTab('automations','skills')">Skills <span class="tab-badge" id="tab-skill-count" style="display:none">0</span><span class="tab-badge" id="tab-pending-skill-count" title="pending approval" style="display:none;background:#f59e0b;color:#000">0</span></button>
|
|
8978
9092
|
<button onclick="switchTab('automations','analytics')">Execution Analytics</button>
|
|
8979
9093
|
</div>
|
|
8980
9094
|
<div id="automations-tab-content">
|
|
8981
9095
|
<div class="tab-pane active" id="tab-automations-scheduled">
|
|
8982
9096
|
<div id="panel-cron"><div class="empty-state">Loading...</div></div>
|
|
8983
9097
|
</div>
|
|
9098
|
+
<div class="tab-pane" id="tab-automations-broken">
|
|
9099
|
+
<div class="card">
|
|
9100
|
+
<div class="card-header" style="display:flex;align-items:center;justify-content:space-between">
|
|
9101
|
+
<span>Repeatedly Failing Jobs (last 48h)</span>
|
|
9102
|
+
<span class="badge badge-gray" id="broken-count-badge" style="font-size:10px">0 jobs</span>
|
|
9103
|
+
</div>
|
|
9104
|
+
<div class="card-body" id="panel-broken-jobs"><div class="empty-state">Loading...</div></div>
|
|
9105
|
+
</div>
|
|
9106
|
+
</div>
|
|
8984
9107
|
<div class="tab-pane" id="tab-automations-timers">
|
|
8985
9108
|
<div class="card">
|
|
8986
9109
|
<div class="card-body" id="panel-timers"><div class="empty-state">Loading...</div></div>
|
|
@@ -9032,6 +9155,13 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
9032
9155
|
</div>
|
|
9033
9156
|
</div>
|
|
9034
9157
|
</div>
|
|
9158
|
+
<div class="card" id="pending-skills-card" style="display:none">
|
|
9159
|
+
<div class="card-header" style="display:flex;align-items:center;justify-content:space-between">
|
|
9160
|
+
<span>Pending Approval</span>
|
|
9161
|
+
<span class="badge badge-orange" id="pending-skills-count-badge" style="font-size:10px">0 pending</span>
|
|
9162
|
+
</div>
|
|
9163
|
+
<div class="card-body" id="panel-pending-skills"></div>
|
|
9164
|
+
</div>
|
|
9035
9165
|
<div class="card">
|
|
9036
9166
|
<div class="card-header" style="display:flex;align-items:center;justify-content:space-between">
|
|
9037
9167
|
<span>Learned Skills</span>
|
|
@@ -10197,7 +10327,7 @@ function navigateTo(page, opts) {
|
|
|
10197
10327
|
updateBuilderMode();
|
|
10198
10328
|
document.getElementById('builder-input').focus();
|
|
10199
10329
|
}
|
|
10200
|
-
if (page === 'automations') { refreshCron(); refreshTimers(); refreshSelfImprove(); refreshSkills(); }
|
|
10330
|
+
if (page === 'automations') { refreshCron(); refreshTimers(); refreshSelfImprove(); refreshSkills(); refreshBrokenJobs(); }
|
|
10201
10331
|
if (page === 'intelligence') { refreshMemory(); }
|
|
10202
10332
|
if (page === 'settings') { refreshSettings(); refreshRemoteAccess(); refreshSalesforce(); refreshClaudeIntegrations(); refreshMcpServers(); }
|
|
10203
10333
|
if (page === 'logs') refreshLogs();
|
|
@@ -10238,6 +10368,7 @@ function switchTab(group, tab) {
|
|
|
10238
10368
|
// Tab-specific refresh
|
|
10239
10369
|
if (group === 'automations') {
|
|
10240
10370
|
if (tab === 'scheduled') refreshCron();
|
|
10371
|
+
if (tab === 'broken') refreshBrokenJobs();
|
|
10241
10372
|
if (tab === 'timers') refreshTimers();
|
|
10242
10373
|
if (tab === 'self-improve') refreshSelfImprove();
|
|
10243
10374
|
if (tab === 'workflows') refreshWorkflows();
|
|
@@ -16031,7 +16162,139 @@ async function expandSkill(name) {
|
|
|
16031
16162
|
} catch(e) { toast('Failed to load skill', 'error'); }
|
|
16032
16163
|
}
|
|
16033
16164
|
|
|
16165
|
+
async function refreshBrokenJobs() {
|
|
16166
|
+
try {
|
|
16167
|
+
var r = await apiFetch('/api/cron/broken-jobs');
|
|
16168
|
+
var d = await r.json();
|
|
16169
|
+
var jobs = d.jobs || [];
|
|
16170
|
+
var tabBadge = document.getElementById('tab-broken-count');
|
|
16171
|
+
if (tabBadge) {
|
|
16172
|
+
tabBadge.textContent = String(jobs.length);
|
|
16173
|
+
tabBadge.style.display = jobs.length > 0 ? '' : 'none';
|
|
16174
|
+
}
|
|
16175
|
+
var countBadge = document.getElementById('broken-count-badge');
|
|
16176
|
+
if (countBadge) countBadge.textContent = jobs.length + ' job' + (jobs.length !== 1 ? 's' : '');
|
|
16177
|
+
var container = document.getElementById('panel-broken-jobs');
|
|
16178
|
+
if (!container) return;
|
|
16179
|
+
if (jobs.length === 0) {
|
|
16180
|
+
container.innerHTML = '<div class="empty-state">All jobs healthy in the last 48h.</div>';
|
|
16181
|
+
return;
|
|
16182
|
+
}
|
|
16183
|
+
var html = '<div style="display:flex;flex-direction:column;gap:12px">';
|
|
16184
|
+
for (var j of jobs) {
|
|
16185
|
+
var breaker = j.circuitBreakerEngagedAt
|
|
16186
|
+
? '<span class="badge" style="background:rgba(239,68,68,0.15);color:#ef4444;font-size:10px">circuit broken</span>'
|
|
16187
|
+
: '';
|
|
16188
|
+
var lastErrorAt = j.lastErrorAt ? timeAgo(j.lastErrorAt) : 'unknown';
|
|
16189
|
+
var failureRatio = j.errorCount48h + '/' + j.totalRuns48h;
|
|
16190
|
+
var advisorLine = j.lastAdvisorOpinion
|
|
16191
|
+
? '<div style="font-size:11px;color:var(--text-muted);margin-top:6px"><strong>Advisor:</strong> ' + esc(j.lastAdvisorOpinion) + '</div>'
|
|
16192
|
+
: '';
|
|
16193
|
+
var errorsHtml = '';
|
|
16194
|
+
if (j.lastErrors && j.lastErrors.length > 0) {
|
|
16195
|
+
errorsHtml = '<div style="margin-top:8px;display:flex;flex-direction:column;gap:4px">';
|
|
16196
|
+
for (var e of j.lastErrors) {
|
|
16197
|
+
errorsHtml += '<pre style="font-size:11px;color:var(--text-secondary);background:var(--bg-tertiary);padding:6px 8px;border-radius:4px;white-space:pre-wrap;word-break:break-word;margin:0;max-height:120px;overflow-y:auto">' + esc(e) + '</pre>';
|
|
16198
|
+
}
|
|
16199
|
+
errorsHtml += '</div>';
|
|
16200
|
+
}
|
|
16201
|
+
var agentTag = j.agentSlug
|
|
16202
|
+
? '<span class="badge badge-blue" style="font-size:10px">' + esc(j.agentSlug) + '</span>'
|
|
16203
|
+
: '';
|
|
16204
|
+
html += '<div style="padding:12px;border:1px solid var(--border);border-radius:8px;background:var(--bg-secondary)">'
|
|
16205
|
+
+ '<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">'
|
|
16206
|
+
+ '<strong>' + esc(j.jobName) + '</strong> ' + agentTag + ' ' + breaker
|
|
16207
|
+
+ '<span style="margin-left:auto;font-size:11px;color:var(--text-muted)">' + failureRatio + ' failed \\u00b7 last error ' + lastErrorAt + '</span>'
|
|
16208
|
+
+ '</div>'
|
|
16209
|
+
+ errorsHtml
|
|
16210
|
+
+ advisorLine
|
|
16211
|
+
+ '</div>';
|
|
16212
|
+
}
|
|
16213
|
+
html += '</div>';
|
|
16214
|
+
container.innerHTML = html;
|
|
16215
|
+
} catch(e) {
|
|
16216
|
+
var c = document.getElementById('panel-broken-jobs');
|
|
16217
|
+
if (c) c.innerHTML = '<div class="empty-state" style="color:var(--red)">Failed to load broken jobs</div>';
|
|
16218
|
+
}
|
|
16219
|
+
}
|
|
16220
|
+
|
|
16221
|
+
async function refreshPendingSkills() {
|
|
16222
|
+
try {
|
|
16223
|
+
var r = await apiFetch('/api/skills/pending');
|
|
16224
|
+
var d = await r.json();
|
|
16225
|
+
var pending = d.skills || [];
|
|
16226
|
+
var tabBadge = document.getElementById('tab-pending-skill-count');
|
|
16227
|
+
if (tabBadge) {
|
|
16228
|
+
tabBadge.textContent = String(pending.length);
|
|
16229
|
+
tabBadge.style.display = pending.length > 0 ? '' : 'none';
|
|
16230
|
+
}
|
|
16231
|
+
var card = document.getElementById('pending-skills-card');
|
|
16232
|
+
var countBadge = document.getElementById('pending-skills-count-badge');
|
|
16233
|
+
var container = document.getElementById('panel-pending-skills');
|
|
16234
|
+
if (!container) return;
|
|
16235
|
+
if (pending.length === 0) {
|
|
16236
|
+
if (card) card.style.display = 'none';
|
|
16237
|
+
container.innerHTML = '';
|
|
16238
|
+
return;
|
|
16239
|
+
}
|
|
16240
|
+
if (card) card.style.display = '';
|
|
16241
|
+
if (countBadge) countBadge.textContent = pending.length + ' pending';
|
|
16242
|
+
|
|
16243
|
+
var html = '<div style="display:flex;flex-direction:column;gap:10px">';
|
|
16244
|
+
for (var s of pending) {
|
|
16245
|
+
var sourceTag = s.source === 'cron' ? '<span class="badge badge-green" style="font-size:10px">cron</span>'
|
|
16246
|
+
: s.source === 'unleashed' ? '<span class="badge badge-purple" style="font-size:10px">unleashed</span>'
|
|
16247
|
+
: s.source === 'chat' ? '<span class="badge badge-blue" style="font-size:10px">chat</span>'
|
|
16248
|
+
: '<span class="badge badge-gray" style="font-size:10px">' + esc(s.source || 'unknown') + '</span>';
|
|
16249
|
+
var age = s.createdAt ? timeAgo(s.createdAt) : '';
|
|
16250
|
+
var scopeTag = s.agentSlug
|
|
16251
|
+
? '<span style="font-size:10px;color:var(--text-muted)">for ' + esc(s.agentSlug) + '</span>'
|
|
16252
|
+
: '<span style="font-size:10px;color:var(--text-muted)">global</span>';
|
|
16253
|
+
html += '<div style="padding:12px;border:1px solid var(--border);border-radius:8px;background:var(--bg-secondary)">'
|
|
16254
|
+
+ '<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;flex-wrap:wrap">'
|
|
16255
|
+
+ '<strong>' + esc(s.title) + '</strong> ' + sourceTag + ' ' + scopeTag
|
|
16256
|
+
+ (age ? ' <span style="font-size:10px;color:var(--text-muted)">\\u00b7 learned ' + age + '</span>' : '')
|
|
16257
|
+
+ '<span style="margin-left:auto;display:flex;gap:6px">'
|
|
16258
|
+
+ '<button onclick="approvePendingSkill(\\x27' + esc(s.name) + '\\x27)" style="background:var(--accent);border:1px solid var(--accent);border-radius:4px;padding:3px 10px;font-size:11px;color:white;cursor:pointer">Approve</button>'
|
|
16259
|
+
+ '<button onclick="rejectPendingSkill(\\x27' + esc(s.name) + '\\x27)" style="background:none;border:1px solid var(--border);border-radius:4px;padding:3px 10px;font-size:11px;color:var(--red);cursor:pointer">Reject</button>'
|
|
16260
|
+
+ '</span>'
|
|
16261
|
+
+ '</div>'
|
|
16262
|
+
+ '<div style="font-size:12px;color:var(--text-secondary)">' + esc(s.description || '') + '</div>'
|
|
16263
|
+
+ '</div>';
|
|
16264
|
+
}
|
|
16265
|
+
html += '</div>';
|
|
16266
|
+
container.innerHTML = html;
|
|
16267
|
+
} catch(e) { /* non-fatal */ }
|
|
16268
|
+
}
|
|
16269
|
+
|
|
16270
|
+
async function approvePendingSkill(name) {
|
|
16271
|
+
try {
|
|
16272
|
+
var r = await apiJson('POST', '/api/skills/pending/' + encodeURIComponent(name) + '/approve', {});
|
|
16273
|
+
if (r && r.ok) {
|
|
16274
|
+
toast(r.message || 'Skill approved', 'success');
|
|
16275
|
+
refreshPendingSkills();
|
|
16276
|
+
refreshSkills();
|
|
16277
|
+
} else {
|
|
16278
|
+
toast((r && r.message) || 'Failed to approve', 'error');
|
|
16279
|
+
}
|
|
16280
|
+
} catch(e) { toast('Failed to approve skill', 'error'); }
|
|
16281
|
+
}
|
|
16282
|
+
|
|
16283
|
+
async function rejectPendingSkill(name) {
|
|
16284
|
+
if (!confirm('Reject this pending skill? It will be deleted.')) return;
|
|
16285
|
+
try {
|
|
16286
|
+
var r = await apiJson('POST', '/api/skills/pending/' + encodeURIComponent(name) + '/reject', {});
|
|
16287
|
+
if (r && r.ok) {
|
|
16288
|
+
toast(r.message || 'Skill rejected', 'success');
|
|
16289
|
+
refreshPendingSkills();
|
|
16290
|
+
} else {
|
|
16291
|
+
toast((r && r.message) || 'Failed to reject', 'error');
|
|
16292
|
+
}
|
|
16293
|
+
} catch(e) { toast('Failed to reject skill', 'error'); }
|
|
16294
|
+
}
|
|
16295
|
+
|
|
16034
16296
|
async function refreshSkills() {
|
|
16297
|
+
refreshPendingSkills();
|
|
16035
16298
|
try {
|
|
16036
16299
|
var r = await apiFetch('/api/skills');
|
|
16037
16300
|
var d = await r.json();
|
|
@@ -16075,12 +16338,15 @@ async function refreshSkills() {
|
|
|
16075
16338
|
+ (s.toolsUsed.length > 4 ? ' <span style="font-size:10px;color:var(--text-muted)">+' + (s.toolsUsed.length - 4) + '</span>' : '')
|
|
16076
16339
|
+ '</div>';
|
|
16077
16340
|
}
|
|
16341
|
+
var retrieval7d = (typeof s.retrievals7d === 'number' && s.retrievals7d > 0)
|
|
16342
|
+
? ' \\u00b7 ' + s.retrievals7d + ' retrievals (7d)'
|
|
16343
|
+
: '';
|
|
16078
16344
|
html += '<div id="skill-card-' + esc(s.name) + '" style="padding:12px;border:1px solid var(--border);border-radius:8px;background:var(--bg-secondary)">'
|
|
16079
16345
|
+ '<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">'
|
|
16080
16346
|
+ '<strong style="cursor:pointer" onclick="expandSkill(\\x27' + esc(s.name) + '\\x27)">' + esc(s.title) + '</strong> ' + sourceTag
|
|
16081
16347
|
+ (sourceCtx ? ' ' + sourceCtx : '')
|
|
16082
16348
|
+ '<span style="margin-left:auto;display:flex;align-items:center;gap:8px">'
|
|
16083
|
-
+ '<span style="font-size:11px;color:var(--text-muted)">used ' + s.useCount + 'x' + (age ? ' \\u00b7 ' + age : '') + '</span>'
|
|
16349
|
+
+ '<span style="font-size:11px;color:var(--text-muted)">used ' + s.useCount + 'x' + (age ? ' \\u00b7 ' + age : '') + retrieval7d + '</span>'
|
|
16084
16350
|
+ '<button onclick="expandSkill(\\x27' + esc(s.name) + '\\x27)" style="background:none;border:1px solid var(--border);border-radius:4px;padding:2px 8px;font-size:11px;color:var(--text-secondary);cursor:pointer">View</button>'
|
|
16085
16351
|
+ '<button onclick="editSkillInBuilder(\\x27' + esc(s.name) + '\\x27)" style="background:none;border:1px solid var(--border);border-radius:4px;padding:2px 8px;font-size:11px;color:var(--accent);cursor:pointer">Edit</button>'
|
|
16086
16352
|
+ '<button onclick="deleteSkill(\\x27' + esc(s.name) + '\\x27)" style="background:none;border:1px solid var(--border);border-radius:4px;padding:2px 8px;font-size:11px;color:var(--red);cursor:pointer">Delete</button>'
|
|
@@ -17538,6 +17804,34 @@ try {
|
|
|
17538
17804
|
toast('Daemon restarted \u2014 refreshing data...', 'info');
|
|
17539
17805
|
setTimeout(function() { refreshAll(); }, 1500);
|
|
17540
17806
|
}
|
|
17807
|
+
if (evt.type === 'deep_result') {
|
|
17808
|
+
try {
|
|
17809
|
+
var container = document.getElementById('chat-messages');
|
|
17810
|
+
var text = (evt.data && evt.data.text) ? evt.data.text : '';
|
|
17811
|
+
if (container && text) {
|
|
17812
|
+
var emptyState = container.querySelector('.empty-state');
|
|
17813
|
+
if (emptyState) emptyState.remove();
|
|
17814
|
+
var row = document.createElement('div');
|
|
17815
|
+
row.className = 'chat-assistant-row';
|
|
17816
|
+
var av = document.createElement('div');
|
|
17817
|
+
av.className = 'chat-avatar-sm';
|
|
17818
|
+
av.innerHTML = (lastStatusData && lastStatusData.name ? lastStatusData.name : 'C').charAt(0).toUpperCase();
|
|
17819
|
+
row.appendChild(av);
|
|
17820
|
+
var bubble = document.createElement('div');
|
|
17821
|
+
bubble.className = 'chat-bubble assistant';
|
|
17822
|
+
bubble.innerHTML = renderMd(text);
|
|
17823
|
+
var meta = document.createElement('div');
|
|
17824
|
+
meta.className = 'chat-meta';
|
|
17825
|
+
meta.textContent = new Date().toLocaleTimeString() + ' \u00b7 deep task';
|
|
17826
|
+
bubble.appendChild(meta);
|
|
17827
|
+
row.appendChild(bubble);
|
|
17828
|
+
container.appendChild(row);
|
|
17829
|
+
container.scrollTop = container.scrollHeight;
|
|
17830
|
+
} else {
|
|
17831
|
+
toast('Deep task result ready \u2014 open chat to view.', 'info');
|
|
17832
|
+
}
|
|
17833
|
+
} catch(e) { /* non-fatal */ }
|
|
17834
|
+
}
|
|
17541
17835
|
} catch(err) { /* ignore */ }
|
|
17542
17836
|
};
|
|
17543
17837
|
} catch(err) { /* SSE not supported */ }
|
|
@@ -87,6 +87,11 @@ export declare class CronScheduler {
|
|
|
87
87
|
private watchAgentsDir;
|
|
88
88
|
private unwatchAgentsDir;
|
|
89
89
|
reloadJobs(): void;
|
|
90
|
+
/**
|
|
91
|
+
* Wrap runLog.append so every completion also checks whether a fix
|
|
92
|
+
* verification is pending and DMs the verdict if so.
|
|
93
|
+
*/
|
|
94
|
+
private _logRun;
|
|
90
95
|
private runJob;
|
|
91
96
|
/**
|
|
92
97
|
* Log an advisor event to the events JSONL file for dashboard surfacing.
|
|
@@ -491,6 +491,9 @@ export class CronScheduler {
|
|
|
491
491
|
this.watchingAgents = false;
|
|
492
492
|
}
|
|
493
493
|
reloadJobs() {
|
|
494
|
+
// Snapshot the pre-reload job definitions so fix-verification can diff
|
|
495
|
+
// and flag any currently-failing job whose config just changed.
|
|
496
|
+
const oldJobs = this.jobs.map(j => ({ ...j }));
|
|
494
497
|
// Stop existing scheduled tasks (but NOT the file watcher)
|
|
495
498
|
for (const [name, task] of this.scheduledTasks) {
|
|
496
499
|
task.stop();
|
|
@@ -580,6 +583,30 @@ export class CronScheduler {
|
|
|
580
583
|
logger.info(`Cron job '${def.name}' scheduled: ${def.schedule} (${SYSTEM_TIMEZONE})`);
|
|
581
584
|
}
|
|
582
585
|
}
|
|
586
|
+
// Fix-verification: detect any currently-failing job whose definition just
|
|
587
|
+
// changed, and record a pending verification for their next run.
|
|
588
|
+
// Skipped on the first load (oldJobs empty) since there's no edit to verify.
|
|
589
|
+
if (oldJobs.length > 0) {
|
|
590
|
+
import('./fix-verification.js').then(({ recordEditsForFailingJobs }) => {
|
|
591
|
+
try {
|
|
592
|
+
recordEditsForFailingJobs(oldJobs, this.jobs);
|
|
593
|
+
}
|
|
594
|
+
catch (err) {
|
|
595
|
+
logger.warn({ err }, 'Fix-verification capture failed');
|
|
596
|
+
}
|
|
597
|
+
}).catch(err => logger.warn({ err }, 'Fix-verification import failed'));
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Wrap runLog.append so every completion also checks whether a fix
|
|
602
|
+
* verification is pending and DMs the verdict if so.
|
|
603
|
+
*/
|
|
604
|
+
_logRun(entry) {
|
|
605
|
+
this.runLog.append(entry);
|
|
606
|
+
import('./fix-verification.js').then(({ checkAndDeliverVerification }) => {
|
|
607
|
+
checkAndDeliverVerification(entry, (text) => this.dispatcher.send(text, {}))
|
|
608
|
+
.catch(err => logger.warn({ err, job: entry.jobName }, 'Fix verification DM failed'));
|
|
609
|
+
}).catch(err => logger.warn({ err }, 'Fix-verification import failed'));
|
|
583
610
|
}
|
|
584
611
|
async runJob(job) {
|
|
585
612
|
// Agent status check — skip if agent is paused/terminated
|
|
@@ -649,7 +676,7 @@ export class CronScheduler {
|
|
|
649
676
|
// Non-zero exit or timeout → skip the job
|
|
650
677
|
const exitCode = preCheckErr.status ?? 1;
|
|
651
678
|
logger.info({ job: job.name, exitCode }, 'Pre-check failed — skipping job (no work to do)');
|
|
652
|
-
this.
|
|
679
|
+
this._logRun({
|
|
653
680
|
jobName: job.name,
|
|
654
681
|
startedAt: new Date().toISOString(),
|
|
655
682
|
finishedAt: new Date().toISOString(),
|
|
@@ -690,7 +717,7 @@ export class CronScheduler {
|
|
|
690
717
|
});
|
|
691
718
|
if (!approved) {
|
|
692
719
|
logger.info({ job: job.name }, 'Cron job skipped by owner');
|
|
693
|
-
this.
|
|
720
|
+
this._logRun({
|
|
694
721
|
jobName: job.name,
|
|
695
722
|
startedAt: new Date().toISOString(),
|
|
696
723
|
finishedAt: new Date().toISOString(),
|
|
@@ -709,7 +736,7 @@ export class CronScheduler {
|
|
|
709
736
|
const advice = getExecutionAdvice(job.name, job);
|
|
710
737
|
if (advice.shouldSkip) {
|
|
711
738
|
logger.info({ job: job.name, reason: advice.skipReason }, 'Execution advisor: circuit breaker — skipping job');
|
|
712
|
-
this.
|
|
739
|
+
this._logRun({
|
|
713
740
|
jobName: job.name,
|
|
714
741
|
startedAt: new Date().toISOString(),
|
|
715
742
|
finishedAt: new Date().toISOString(),
|
|
@@ -876,7 +903,7 @@ export class CronScheduler {
|
|
|
876
903
|
this.gateway.injectContext(`discord:user:${DISCORD_OWNER_ID}`, `[Scheduled cron: ${job.name}]`, response);
|
|
877
904
|
}
|
|
878
905
|
}
|
|
879
|
-
this.
|
|
906
|
+
this._logRun(entry);
|
|
880
907
|
// Fire-and-forget: extract procedural skill from successful long-running cron jobs
|
|
881
908
|
if (entry.status === 'ok' && entry.durationMs > 30_000 && response && response.length > 500) {
|
|
882
909
|
this.gateway.extractCronSkill(job.name, job.prompt, response, entry.durationMs, job.agentSlug)
|
|
@@ -902,7 +929,7 @@ export class CronScheduler {
|
|
|
902
929
|
const errorType = errTerminalReason
|
|
903
930
|
? classifyTerminalReason(errTerminalReason)
|
|
904
931
|
: classifyError(err);
|
|
905
|
-
this.
|
|
932
|
+
this._logRun({
|
|
906
933
|
jobName: job.name,
|
|
907
934
|
startedAt: startedAt.toISOString(),
|
|
908
935
|
finishedAt: finishedAt.toISOString(),
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clementine TypeScript — Cron failure monitor.
|
|
3
|
+
*
|
|
4
|
+
* Surfaces cron jobs that have been failing repeatedly so they don't sit
|
|
5
|
+
* silently broken (which is what happened to ross-the-sdr:reply-detection —
|
|
6
|
+
* the existing circuit breaker fired ONCE at consErrors=5 and then went
|
|
7
|
+
* quiet for days).
|
|
8
|
+
*
|
|
9
|
+
* Threshold: a job is "broken" if either
|
|
10
|
+
* - it has >= 3 error/retried entries in the last 48h, OR
|
|
11
|
+
* - the circuit breaker engaged for it within the last 48h.
|
|
12
|
+
*
|
|
13
|
+
* Per-job 24h cooldown prevents re-spamming the owner with the same news.
|
|
14
|
+
*
|
|
15
|
+
* Read-only with respect to the cron run logs and advisor events; mutates
|
|
16
|
+
* only its own state file (cron/failure-monitor.json).
|
|
17
|
+
*/
|
|
18
|
+
export interface BrokenJob {
|
|
19
|
+
jobName: string;
|
|
20
|
+
agentSlug?: string;
|
|
21
|
+
errorCount48h: number;
|
|
22
|
+
totalRuns48h: number;
|
|
23
|
+
lastErrorAt: string | null;
|
|
24
|
+
lastErrors: string[];
|
|
25
|
+
circuitBreakerEngagedAt: string | null;
|
|
26
|
+
lastAdvisorOpinion: string | null;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Compute the current set of broken jobs by scanning all run logs.
|
|
30
|
+
* Pure function (state-free) — used both by the monitor sweep and the dashboard endpoint.
|
|
31
|
+
*/
|
|
32
|
+
export declare function computeBrokenJobs(now?: number): BrokenJob[];
|
|
33
|
+
/**
|
|
34
|
+
* Run a sweep: identify currently-broken jobs, pick the ones we haven't
|
|
35
|
+
* notified about recently, and dispatch one consolidated DM.
|
|
36
|
+
*
|
|
37
|
+
* Returns the jobs that triggered a fresh notification (mostly for tests/logs).
|
|
38
|
+
*/
|
|
39
|
+
export declare function runFailureSweep(send: (text: string) => Promise<unknown>, now?: number): Promise<BrokenJob[]>;
|
|
40
|
+
//# sourceMappingURL=failure-monitor.d.ts.map
|