clementine-agent 1.0.20 → 1.0.22
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/self-improve.js +74 -29
- package/dist/cli/dashboard.js +217 -0
- package/dist/gateway/claim-tracker.d.ts +75 -0
- package/dist/gateway/claim-tracker.js +495 -0
- package/dist/gateway/failure-monitor.js +108 -5
- package/dist/gateway/heartbeat-scheduler.js +22 -0
- package/dist/gateway/notifications.d.ts +1 -0
- package/dist/gateway/notifications.js +16 -0
- package/dist/gateway/outcome-grader.d.ts +41 -0
- package/dist/gateway/outcome-grader.js +173 -0
- package/dist/memory/store.js +29 -0
- package/package.json +1 -1
|
@@ -611,63 +611,108 @@ export class SelfImproveLoop {
|
|
|
611
611
|
async hypothesize(metrics, history) {
|
|
612
612
|
// Read targeted triggers (written by cron scheduler when jobs fail repeatedly)
|
|
613
613
|
let targetedTriggers = '';
|
|
614
|
+
const triggerBullets = [];
|
|
615
|
+
// Source 1: explicit triggers written by the cron scheduler at 3+
|
|
616
|
+
// consecutive errors (legacy path — we still honor and drain).
|
|
614
617
|
const triggersDir = path.join(SELF_IMPROVE_DIR, 'triggers');
|
|
615
618
|
if (existsSync(triggersDir)) {
|
|
616
619
|
const triggerFiles = readdirSync(triggersDir).filter(f => f.endsWith('.json'));
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
catch {
|
|
626
|
-
return null;
|
|
627
|
-
}
|
|
628
|
-
}).filter(Boolean);
|
|
629
|
-
if (triggers.length > 0) {
|
|
630
|
-
targetedTriggers = `\n\n## PRIORITY: Failing Jobs Needing Attention\n` +
|
|
631
|
-
`These jobs have been failing repeatedly and need prompt/config fixes:\n` +
|
|
632
|
-
triggers.map((t) => `- **${t.jobName}**: ${t.consecutiveErrors} consecutive errors. Recent: ${(t.recentErrors ?? []).join('; ')}`).join('\n') +
|
|
633
|
-
`\n\nFocus your improvement hypothesis on fixing these jobs first.\n`;
|
|
620
|
+
const triggers = triggerFiles.slice(0, 3).map(f => {
|
|
621
|
+
try {
|
|
622
|
+
const t = JSON.parse(readFileSync(path.join(triggersDir, f), 'utf-8'));
|
|
623
|
+
unlinkSync(path.join(triggersDir, f));
|
|
624
|
+
return t;
|
|
625
|
+
}
|
|
626
|
+
catch {
|
|
627
|
+
return null;
|
|
634
628
|
}
|
|
629
|
+
}).filter(Boolean);
|
|
630
|
+
for (const t of triggers) {
|
|
631
|
+
triggerBullets.push(`- **${t.jobName}**: ${t.consecutiveErrors} consecutive errors. Recent: ${(t.recentErrors ?? []).join('; ')}`);
|
|
635
632
|
}
|
|
636
633
|
}
|
|
634
|
+
// Source 2: broken-jobs from the failure monitor. These are jobs the
|
|
635
|
+
// user hasn't applied a fix for yet — real, current gaps the hypothesizer
|
|
636
|
+
// should target. Complements the diversity constraint: even if the area
|
|
637
|
+
// has been over-targeted historically, a specific broken job is a fresh
|
|
638
|
+
// concrete signal.
|
|
639
|
+
try {
|
|
640
|
+
const { computeBrokenJobs } = await import('../gateway/failure-monitor.js');
|
|
641
|
+
const broken = computeBrokenJobs();
|
|
642
|
+
for (const b of broken.slice(0, 3)) {
|
|
643
|
+
const diagHint = b.diagnosis
|
|
644
|
+
? ` Diagnosis: ${b.diagnosis.rootCause.slice(0, 120)}`
|
|
645
|
+
: '';
|
|
646
|
+
triggerBullets.push(`- **${b.jobName}**: ${b.errorCount48h}/${b.totalRuns48h} failed in 48h${b.circuitBreakerEngagedAt ? ' (breaker engaged)' : ''}.${diagHint}`);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
catch { /* failure-monitor module optional */ }
|
|
650
|
+
if (triggerBullets.length > 0) {
|
|
651
|
+
targetedTriggers = `\n\n## PRIORITY: Failing Jobs Needing Attention\n` +
|
|
652
|
+
`These jobs have been failing recently and need prompt/config fixes:\n` +
|
|
653
|
+
triggerBullets.join('\n') +
|
|
654
|
+
`\n\nFocus your improvement hypothesis on fixing these jobs first.\n`;
|
|
655
|
+
}
|
|
637
656
|
// Format experiment history for the prompt
|
|
638
657
|
const historyText = history.slice(-20).map(e => `#${e.iteration} | ${e.area} | "${e.hypothesis.slice(0, 60)}" | ${(e.score * 10).toFixed(1)}/10 ${e.accepted ? '✅' : '❌'}`).join('\n') || '(no prior experiments)';
|
|
639
|
-
// Enforce diversity: count recent proposals per area:target AND per area
|
|
658
|
+
// Enforce diversity: count recent proposals per area:target AND per area.
|
|
659
|
+
// A pair is only "over-targeted" if its MOST RECENT attempt was within
|
|
660
|
+
// the last 30 days — otherwise it's fair game to retry with fresh data.
|
|
661
|
+
// Stops the saturation state where after ~60 experiments the loop has
|
|
662
|
+
// blocked every area:target pair permanently and produces no new
|
|
663
|
+
// hypotheses (the Apr 11-19 plateau).
|
|
664
|
+
const DIVERSITY_WINDOW_MS = 30 * 24 * 60 * 60 * 1000;
|
|
665
|
+
const diversityCutoff = Date.now() - DIVERSITY_WINDOW_MS;
|
|
640
666
|
const recentTargets = new Map();
|
|
641
667
|
const recentAreas = new Map();
|
|
642
|
-
for (const e of history.slice(-
|
|
668
|
+
for (const e of history.slice(-50)) {
|
|
643
669
|
const key = `${e.area}:${e.target}`;
|
|
644
|
-
|
|
645
|
-
|
|
670
|
+
const ts = Date.parse(e.startedAt);
|
|
671
|
+
const tsMs = Number.isFinite(ts) ? ts : 0;
|
|
672
|
+
const cur = recentTargets.get(key);
|
|
673
|
+
recentTargets.set(key, {
|
|
674
|
+
count: (cur?.count ?? 0) + 1,
|
|
675
|
+
newestMs: Math.max(cur?.newestMs ?? 0, tsMs),
|
|
676
|
+
});
|
|
677
|
+
const curA = recentAreas.get(e.area);
|
|
678
|
+
recentAreas.set(e.area, {
|
|
679
|
+
count: (curA?.count ?? 0) + 1,
|
|
680
|
+
newestMs: Math.max(curA?.newestMs ?? 0, tsMs),
|
|
681
|
+
});
|
|
646
682
|
}
|
|
647
683
|
for (const p of this.getPendingChanges()) {
|
|
648
684
|
const key = `${p.area}:${p.target}`;
|
|
649
|
-
|
|
650
|
-
|
|
685
|
+
const now = Date.now();
|
|
686
|
+
const cur = recentTargets.get(key);
|
|
687
|
+
recentTargets.set(key, {
|
|
688
|
+
count: (cur?.count ?? 0) + 1,
|
|
689
|
+
newestMs: Math.max(cur?.newestMs ?? 0, now),
|
|
690
|
+
});
|
|
691
|
+
const curA = recentAreas.get(p.area);
|
|
692
|
+
recentAreas.set(p.area, {
|
|
693
|
+
count: (curA?.count ?? 0) + 1,
|
|
694
|
+
newestMs: Math.max(curA?.newestMs ?? 0, now),
|
|
695
|
+
});
|
|
651
696
|
}
|
|
652
|
-
// Block
|
|
697
|
+
// Block only when both (a) count is high enough AND (b) the last attempt
|
|
698
|
+
// was within the diversity window.
|
|
653
699
|
const overTargeted = [...recentTargets.entries()]
|
|
654
|
-
.filter(([,
|
|
700
|
+
.filter(([, v]) => v.count >= 2 && v.newestMs > diversityCutoff)
|
|
655
701
|
.map(([key]) => key);
|
|
656
|
-
// Block entire areas with >= 3 recent proposals
|
|
657
702
|
const overTargetedAreas = [...recentAreas.entries()]
|
|
658
|
-
.filter(([,
|
|
703
|
+
.filter(([, v]) => v.count >= 3 && v.newestMs > diversityCutoff)
|
|
659
704
|
.map(([area]) => area);
|
|
660
705
|
// Build area coverage stats to nudge the LLM toward unexplored areas
|
|
661
706
|
const allAreas = this.config.areas;
|
|
662
707
|
const areaCoverage = allAreas.map(area => {
|
|
663
|
-
const count = recentAreas.get(area) ?? 0;
|
|
708
|
+
const count = recentAreas.get(area)?.count ?? 0;
|
|
664
709
|
return `- ${area}: ${count} recent proposals`;
|
|
665
710
|
}).join('\n');
|
|
666
711
|
const diversityConstraint = `\n\n## AREA COVERAGE (target under-explored areas)\n${areaCoverage}\n` +
|
|
667
712
|
(overTargeted.length > 0 || overTargetedAreas.length > 0
|
|
668
713
|
? `\n## DIVERSITY CONSTRAINT\n` +
|
|
669
714
|
(overTargetedAreas.length > 0
|
|
670
|
-
? `These AREAS have been over-targeted and MUST NOT be chosen:\n${overTargetedAreas.map(a => `- ${a} (${recentAreas.get(a)} proposals)`).join('\n')}\n`
|
|
715
|
+
? `These AREAS have been over-targeted and MUST NOT be chosen:\n${overTargetedAreas.map(a => `- ${a} (${recentAreas.get(a)?.count ?? 0} proposals)`).join('\n')}\n`
|
|
671
716
|
: '') +
|
|
672
717
|
(overTargeted.length > 0
|
|
673
718
|
? `These specific targets MUST NOT be re-targeted:\n${overTargeted.map(t => `- ${t}`).join('\n')}\n`
|
package/dist/cli/dashboard.js
CHANGED
|
@@ -2075,6 +2075,58 @@ export async function cmdDashboard(opts) {
|
|
|
2075
2075
|
res.status(500).json({ error: String(err) });
|
|
2076
2076
|
}
|
|
2077
2077
|
});
|
|
2078
|
+
// ── Claims + trust score ────────────────────────────────────────
|
|
2079
|
+
app.get('/api/claims', async (req, res) => {
|
|
2080
|
+
try {
|
|
2081
|
+
const { listClaims, trustScore } = await import('../gateway/claim-tracker.js');
|
|
2082
|
+
const status = req.query.status;
|
|
2083
|
+
const limit = Number(req.query.limit ?? 50);
|
|
2084
|
+
const sinceHours = req.query.sinceHours ? Number(req.query.sinceHours) : undefined;
|
|
2085
|
+
const validStatus = ['pending', 'verified', 'failed', 'expired', 'dismissed'];
|
|
2086
|
+
const claims = await listClaims({
|
|
2087
|
+
...(status && validStatus.includes(status) ? { status: status } : {}),
|
|
2088
|
+
limit: Number.isFinite(limit) ? limit : 50,
|
|
2089
|
+
...(sinceHours && Number.isFinite(sinceHours) ? { sinceHours } : {}),
|
|
2090
|
+
});
|
|
2091
|
+
const trust = await trustScore(30);
|
|
2092
|
+
res.json({ claims, trust });
|
|
2093
|
+
}
|
|
2094
|
+
catch (err) {
|
|
2095
|
+
res.status(500).json({ error: String(err) });
|
|
2096
|
+
}
|
|
2097
|
+
});
|
|
2098
|
+
app.post('/api/claims/:id/mark-verified', async (req, res) => {
|
|
2099
|
+
try {
|
|
2100
|
+
const { setClaimStatus } = await import('../gateway/claim-tracker.js');
|
|
2101
|
+
const verdict = typeof req.body?.verdict === 'string' ? req.body.verdict.slice(0, 400) : 'Manually verified by owner';
|
|
2102
|
+
const ok = await setClaimStatus(req.params.id, 'verified', verdict);
|
|
2103
|
+
res.json({ ok });
|
|
2104
|
+
}
|
|
2105
|
+
catch (err) {
|
|
2106
|
+
res.status(500).json({ error: String(err) });
|
|
2107
|
+
}
|
|
2108
|
+
});
|
|
2109
|
+
app.post('/api/claims/:id/mark-failed', async (req, res) => {
|
|
2110
|
+
try {
|
|
2111
|
+
const { setClaimStatus } = await import('../gateway/claim-tracker.js');
|
|
2112
|
+
const verdict = typeof req.body?.verdict === 'string' ? req.body.verdict.slice(0, 400) : 'Manually marked as failed by owner';
|
|
2113
|
+
const ok = await setClaimStatus(req.params.id, 'failed', verdict);
|
|
2114
|
+
res.json({ ok });
|
|
2115
|
+
}
|
|
2116
|
+
catch (err) {
|
|
2117
|
+
res.status(500).json({ error: String(err) });
|
|
2118
|
+
}
|
|
2119
|
+
});
|
|
2120
|
+
app.post('/api/claims/:id/dismiss', async (req, res) => {
|
|
2121
|
+
try {
|
|
2122
|
+
const { setClaimStatus } = await import('../gateway/claim-tracker.js');
|
|
2123
|
+
const ok = await setClaimStatus(req.params.id, 'dismissed', 'Dismissed — not a tracked promise');
|
|
2124
|
+
res.json({ ok });
|
|
2125
|
+
}
|
|
2126
|
+
catch (err) {
|
|
2127
|
+
res.status(500).json({ error: String(err) });
|
|
2128
|
+
}
|
|
2129
|
+
});
|
|
2078
2130
|
// ── Broken jobs (failure monitor) ───────────────────────────────
|
|
2079
2131
|
app.get('/api/cron/broken-jobs', async (_req, res) => {
|
|
2080
2132
|
try {
|
|
@@ -8903,6 +8955,10 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
8903
8955
|
<div class="nav-item" data-page="intelligence">
|
|
8904
8956
|
<span class="nav-icon">🧠</span> Intelligence
|
|
8905
8957
|
</div>
|
|
8958
|
+
<div class="nav-item" data-page="claims">
|
|
8959
|
+
<span class="nav-icon">🔒</span> Trust & Claims
|
|
8960
|
+
<span class="nav-badge" id="nav-trust-score" style="display:none">--</span>
|
|
8961
|
+
</div>
|
|
8906
8962
|
<div class="nav-item" data-page="logs">
|
|
8907
8963
|
<span class="nav-icon">📜</span> Logs
|
|
8908
8964
|
</div>
|
|
@@ -9337,6 +9393,32 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
9337
9393
|
<div id="panel-sessions"><div class="empty-state">Loading...</div></div>
|
|
9338
9394
|
</div>
|
|
9339
9395
|
|
|
9396
|
+
<!-- ═══ Trust & Claims Page ═══ -->
|
|
9397
|
+
<div class="page" id="page-claims">
|
|
9398
|
+
<div class="page-title">Trust & Claims</div>
|
|
9399
|
+
<div class="card" style="margin-bottom:16px">
|
|
9400
|
+
<div class="card-body" style="display:flex;align-items:center;gap:16px;padding:16px">
|
|
9401
|
+
<div style="font-size:36px;font-weight:700" id="trust-score-big">--</div>
|
|
9402
|
+
<div style="flex:1">
|
|
9403
|
+
<div style="font-size:13px;font-weight:600">Clementine's trust score</div>
|
|
9404
|
+
<div style="font-size:11px;color:var(--text-muted)" id="trust-score-detail">
|
|
9405
|
+
Rolling over the last 30 verified or failed claims.
|
|
9406
|
+
</div>
|
|
9407
|
+
</div>
|
|
9408
|
+
<div style="display:flex;gap:6px">
|
|
9409
|
+
<button class="btn-sm" onclick="refreshClaims('all')" id="claims-filter-all" style="padding:4px 10px">All</button>
|
|
9410
|
+
<button class="btn-sm" onclick="refreshClaims('pending')" id="claims-filter-pending" style="padding:4px 10px">Pending</button>
|
|
9411
|
+
<button class="btn-sm" onclick="refreshClaims('verified')" id="claims-filter-verified" style="padding:4px 10px">Verified</button>
|
|
9412
|
+
<button class="btn-sm" onclick="refreshClaims('failed')" id="claims-filter-failed" style="padding:4px 10px">Failed</button>
|
|
9413
|
+
</div>
|
|
9414
|
+
</div>
|
|
9415
|
+
</div>
|
|
9416
|
+
<div class="card">
|
|
9417
|
+
<div class="card-header">Recent claims</div>
|
|
9418
|
+
<div class="card-body" id="panel-claims"><div class="empty-state">Loading...</div></div>
|
|
9419
|
+
</div>
|
|
9420
|
+
</div>
|
|
9421
|
+
|
|
9340
9422
|
<!-- ═══ Logs Page ═══ -->
|
|
9341
9423
|
<div class="page" id="page-logs">
|
|
9342
9424
|
<div class="page-title">Logs</div>
|
|
@@ -10376,6 +10458,7 @@ function navigateTo(page, opts) {
|
|
|
10376
10458
|
document.getElementById('builder-input').focus();
|
|
10377
10459
|
}
|
|
10378
10460
|
if (page === 'automations') { refreshCron(); refreshTimers(); refreshSelfImprove(); refreshSkills(); refreshBrokenJobs(); }
|
|
10461
|
+
if (page === 'claims') { refreshClaims(); }
|
|
10379
10462
|
if (page === 'intelligence') { refreshMemory(); }
|
|
10380
10463
|
if (page === 'settings') { refreshSettings(); refreshRemoteAccess(); refreshSalesforce(); refreshClaudeIntegrations(); refreshMcpServers(); }
|
|
10381
10464
|
if (page === 'logs') refreshLogs();
|
|
@@ -16210,6 +16293,129 @@ async function expandSkill(name) {
|
|
|
16210
16293
|
} catch(e) { toast('Failed to load skill', 'error'); }
|
|
16211
16294
|
}
|
|
16212
16295
|
|
|
16296
|
+
// ── Trust & Claims ────────────────────────
|
|
16297
|
+
var _claimsFilter = 'all';
|
|
16298
|
+
|
|
16299
|
+
function formatTrustScore(trust) {
|
|
16300
|
+
if (!trust) return { big: '--', detail: 'No claims recorded yet.' };
|
|
16301
|
+
if (trust.score === null) {
|
|
16302
|
+
return {
|
|
16303
|
+
big: trust.total === 0 ? '--' : String(trust.total),
|
|
16304
|
+
detail: trust.total === 0
|
|
16305
|
+
? 'No verified or failed claims yet. Score activates after 3+ verdicts.'
|
|
16306
|
+
: 'Only ' + trust.total + ' verdict' + (trust.total === 1 ? '' : 's') + ' so far \u2014 score activates at 3+.',
|
|
16307
|
+
};
|
|
16308
|
+
}
|
|
16309
|
+
var pct = Math.round(trust.score * 100);
|
|
16310
|
+
return {
|
|
16311
|
+
big: pct + '%',
|
|
16312
|
+
detail: trust.verified + ' verified, ' + trust.failed + ' failed over last ' + trust.total + ' judged claims.',
|
|
16313
|
+
};
|
|
16314
|
+
}
|
|
16315
|
+
|
|
16316
|
+
async function refreshClaims(filter) {
|
|
16317
|
+
if (filter && filter !== _claimsFilter) _claimsFilter = filter;
|
|
16318
|
+
try {
|
|
16319
|
+
var url = '/api/claims?limit=100';
|
|
16320
|
+
if (_claimsFilter && _claimsFilter !== 'all') url += '&status=' + encodeURIComponent(_claimsFilter);
|
|
16321
|
+
var r = await apiFetch(url);
|
|
16322
|
+
var d = await r.json();
|
|
16323
|
+
|
|
16324
|
+
// Trust score
|
|
16325
|
+
var t = formatTrustScore(d.trust);
|
|
16326
|
+
var big = document.getElementById('trust-score-big');
|
|
16327
|
+
var detail = document.getElementById('trust-score-detail');
|
|
16328
|
+
if (big) big.textContent = t.big;
|
|
16329
|
+
if (detail) detail.textContent = t.detail;
|
|
16330
|
+
var navBadge = document.getElementById('nav-trust-score');
|
|
16331
|
+
if (navBadge) {
|
|
16332
|
+
if (d.trust && d.trust.score !== null) {
|
|
16333
|
+
navBadge.textContent = Math.round(d.trust.score * 100) + '%';
|
|
16334
|
+
navBadge.style.display = '';
|
|
16335
|
+
} else {
|
|
16336
|
+
navBadge.style.display = 'none';
|
|
16337
|
+
}
|
|
16338
|
+
}
|
|
16339
|
+
|
|
16340
|
+
// Filter-button highlighting
|
|
16341
|
+
['all', 'pending', 'verified', 'failed'].forEach(function(k) {
|
|
16342
|
+
var btn = document.getElementById('claims-filter-' + k);
|
|
16343
|
+
if (!btn) return;
|
|
16344
|
+
if (k === _claimsFilter) {
|
|
16345
|
+
btn.style.background = 'var(--accent)';
|
|
16346
|
+
btn.style.color = 'white';
|
|
16347
|
+
btn.style.border = '1px solid var(--accent)';
|
|
16348
|
+
} else {
|
|
16349
|
+
btn.style.background = 'var(--bg-tertiary)';
|
|
16350
|
+
btn.style.color = 'var(--text-primary)';
|
|
16351
|
+
btn.style.border = '1px solid var(--border)';
|
|
16352
|
+
}
|
|
16353
|
+
});
|
|
16354
|
+
|
|
16355
|
+
var claims = d.claims || [];
|
|
16356
|
+
var container = document.getElementById('panel-claims');
|
|
16357
|
+
if (!container) return;
|
|
16358
|
+
if (claims.length === 0) {
|
|
16359
|
+
container.innerHTML = '<div class="empty-state">No claims in this view.</div>';
|
|
16360
|
+
return;
|
|
16361
|
+
}
|
|
16362
|
+
|
|
16363
|
+
var statusColor = {
|
|
16364
|
+
pending: '#f59e0b',
|
|
16365
|
+
verified: '#22c55e',
|
|
16366
|
+
failed: '#ef4444',
|
|
16367
|
+
expired: '#6b7280',
|
|
16368
|
+
dismissed: '#6b7280',
|
|
16369
|
+
};
|
|
16370
|
+
|
|
16371
|
+
var html = '<div style="display:flex;flex-direction:column;gap:8px">';
|
|
16372
|
+
for (var c of claims) {
|
|
16373
|
+
var color = statusColor[c.status] || '#6b7280';
|
|
16374
|
+
var due = c.dueAt ? ' \u00b7 due ' + timeAgo(c.dueAt) : '';
|
|
16375
|
+
var ver = c.verifiedAt ? ' \u00b7 verdict ' + timeAgo(c.verifiedAt) : '';
|
|
16376
|
+
var actions = c.status === 'pending'
|
|
16377
|
+
? '<span style="margin-left:auto;display:flex;gap:4px">'
|
|
16378
|
+
+ '<button onclick="markClaim(\\x27' + esc(c.id) + '\\x27,\\x27verified\\x27)" style="background:none;border:1px solid var(--border);border-radius:3px;padding:2px 8px;font-size:11px;color:#22c55e;cursor:pointer">Verified</button>'
|
|
16379
|
+
+ '<button onclick="markClaim(\\x27' + esc(c.id) + '\\x27,\\x27failed\\x27)" style="background:none;border:1px solid var(--border);border-radius:3px;padding:2px 8px;font-size:11px;color:#ef4444;cursor:pointer">Failed</button>'
|
|
16380
|
+
+ '<button onclick="markClaim(\\x27' + esc(c.id) + '\\x27,\\x27dismissed\\x27)" style="background:none;border:1px solid var(--border);border-radius:3px;padding:2px 8px;font-size:11px;color:var(--text-muted);cursor:pointer">Dismiss</button>'
|
|
16381
|
+
+ '</span>'
|
|
16382
|
+
: '';
|
|
16383
|
+
var verdictLine = c.verdict ? '<div style="font-size:11px;color:var(--text-muted);margin-top:4px;font-style:italic">\u201c' + esc(c.verdict) + '\u201d</div>' : '';
|
|
16384
|
+
html += '<div style="padding:10px;border:1px solid var(--border);border-left:3px solid ' + color + ';border-radius:6px;background:var(--bg-secondary)">'
|
|
16385
|
+
+ '<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">'
|
|
16386
|
+
+ '<span style="font-size:10px;padding:1px 6px;background:' + color + '33;color:' + color + ';border-radius:3px;text-transform:uppercase;letter-spacing:0.5px">' + esc(c.status) + '</span>'
|
|
16387
|
+
+ '<span style="font-size:10px;color:var(--text-muted)">' + esc(c.claimType) + '</span>'
|
|
16388
|
+
+ '<strong style="font-size:13px">' + esc(c.subject) + '</strong>'
|
|
16389
|
+
+ '<span style="font-size:10px;color:var(--text-muted);margin-left:auto">' + timeAgo(c.extractedAt) + due + ver + '</span>'
|
|
16390
|
+
+ '</div>'
|
|
16391
|
+
+ '<div style="font-size:11px;color:var(--text-secondary);margin-top:6px">' + esc(c.messageSnippet.slice(0, 300)) + '</div>'
|
|
16392
|
+
+ verdictLine
|
|
16393
|
+
+ actions
|
|
16394
|
+
+ '</div>';
|
|
16395
|
+
}
|
|
16396
|
+
html += '</div>';
|
|
16397
|
+
container.innerHTML = html;
|
|
16398
|
+
} catch (e) {
|
|
16399
|
+
var c = document.getElementById('panel-claims');
|
|
16400
|
+
if (c) c.innerHTML = '<div class="empty-state" style="color:var(--red)">Failed to load claims</div>';
|
|
16401
|
+
}
|
|
16402
|
+
}
|
|
16403
|
+
|
|
16404
|
+
async function markClaim(id, status) {
|
|
16405
|
+
var endpoint = status === 'verified' ? 'mark-verified' : status === 'failed' ? 'mark-failed' : 'dismiss';
|
|
16406
|
+
try {
|
|
16407
|
+
var res = await apiJson('POST', '/api/claims/' + encodeURIComponent(id) + '/' + endpoint, {});
|
|
16408
|
+
if (res && res.ok) {
|
|
16409
|
+
toast('Claim marked ' + status, 'success');
|
|
16410
|
+
refreshClaims();
|
|
16411
|
+
} else {
|
|
16412
|
+
toast('Failed to update claim', 'error');
|
|
16413
|
+
}
|
|
16414
|
+
} catch (e) {
|
|
16415
|
+
toast('Error: ' + String(e), 'error');
|
|
16416
|
+
}
|
|
16417
|
+
}
|
|
16418
|
+
|
|
16213
16419
|
async function applyBrokenJobFix(jobName) {
|
|
16214
16420
|
try {
|
|
16215
16421
|
// First: dry-run to get the actual diff to show in the confirm dialog
|
|
@@ -17903,6 +18109,17 @@ async function refreshSalesforce() {
|
|
|
17903
18109
|
refreshAll();
|
|
17904
18110
|
refreshTeamNav();
|
|
17905
18111
|
}
|
|
18112
|
+
// Lightweight trust-score fetch for the nav badge (any page benefits from this)
|
|
18113
|
+
try {
|
|
18114
|
+
var tr = await apiFetch('/api/claims?limit=1');
|
|
18115
|
+
var td = await tr.json();
|
|
18116
|
+
var navBadge = document.getElementById('nav-trust-score');
|
|
18117
|
+
if (navBadge && td && td.trust && td.trust.score !== null) {
|
|
18118
|
+
navBadge.textContent = Math.round(td.trust.score * 100) + '%';
|
|
18119
|
+
navBadge.style.display = '';
|
|
18120
|
+
}
|
|
18121
|
+
} catch(e) { /* non-fatal */ }
|
|
18122
|
+
|
|
17906
18123
|
// Populate agent filter from init data (avoid separate /api/office call)
|
|
17907
18124
|
if (d && d.office) {
|
|
17908
18125
|
try {
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clementine TypeScript — Claim tracking.
|
|
3
|
+
*
|
|
4
|
+
* Every outbound DM passes through the notification dispatcher. This
|
|
5
|
+
* module parses those messages for claims Clementine makes (promises,
|
|
6
|
+
* fixes, scheduled actions) and persists them to a SQLite table so we
|
|
7
|
+
* can verify them later and compute a rolling trust score.
|
|
8
|
+
*
|
|
9
|
+
* The bluntest answer to "you told me you fixed it twice and it wasn't
|
|
10
|
+
* fixed" — every claim is now recorded, some auto-verified, all
|
|
11
|
+
* reviewable in the dashboard.
|
|
12
|
+
*
|
|
13
|
+
* Extraction is regex-only to keep cost at $0 per DM. For nuanced
|
|
14
|
+
* claims the dashboard's manual verify/fail path covers the gap.
|
|
15
|
+
*/
|
|
16
|
+
import type { Gateway } from './router.js';
|
|
17
|
+
export type ClaimType = 'scheduled' | 'fixed' | 'will_do' | 'sent' | 'added' | 'unknown';
|
|
18
|
+
export type VerifyStrategy = 'cron_run_check' | 'config_inspect' | 'manual';
|
|
19
|
+
export type ClaimStatus = 'pending' | 'verified' | 'failed' | 'expired' | 'dismissed';
|
|
20
|
+
export interface Claim {
|
|
21
|
+
id: string;
|
|
22
|
+
sessionKey: string | null;
|
|
23
|
+
messageSnippet: string;
|
|
24
|
+
claimType: ClaimType;
|
|
25
|
+
subject: string;
|
|
26
|
+
dueAt: string | null;
|
|
27
|
+
verifyStrategy: VerifyStrategy;
|
|
28
|
+
status: ClaimStatus;
|
|
29
|
+
verdict: string | null;
|
|
30
|
+
extractedAt: string;
|
|
31
|
+
verifiedAt: string | null;
|
|
32
|
+
agentSlug: string | null;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Extract claims from a message. Returns empty array if nothing matched.
|
|
36
|
+
* Caller supplies sessionKey for traceability. Never throws.
|
|
37
|
+
*/
|
|
38
|
+
export declare function extractClaims(text: string, sessionKey?: string | null, agentSlug?: string | null): Omit<Claim, 'status' | 'extractedAt' | 'verifiedAt' | 'verdict'>[];
|
|
39
|
+
/**
|
|
40
|
+
* Drain the LLM-fallback queue: pick up to N enqueued DMs, ask Haiku
|
|
41
|
+
* to extract claims via the same shape the regex patterns use, persist
|
|
42
|
+
* any found. Best-effort — errors just leave the queue unchanged for
|
|
43
|
+
* the next sweep.
|
|
44
|
+
*/
|
|
45
|
+
export declare function drainLLMFallback(gateway: Gateway, maxPerSweep?: number): Promise<number>;
|
|
46
|
+
export declare function recordClaims(claims: Omit<Claim, 'status' | 'extractedAt' | 'verifiedAt' | 'verdict'>[]): Promise<void>;
|
|
47
|
+
export declare function listClaims(opts?: {
|
|
48
|
+
status?: ClaimStatus;
|
|
49
|
+
limit?: number;
|
|
50
|
+
sinceHours?: number;
|
|
51
|
+
}): Promise<Claim[]>;
|
|
52
|
+
export declare function setClaimStatus(id: string, status: ClaimStatus, verdict?: string): Promise<boolean>;
|
|
53
|
+
/**
|
|
54
|
+
* Rolling trust score over the last N verified-or-failed claims.
|
|
55
|
+
* Ignores 'pending', 'expired', and 'dismissed' — only signal from
|
|
56
|
+
* actual verdicts. Returns null when there's not enough data (<3
|
|
57
|
+
* judged claims) to be meaningful.
|
|
58
|
+
*/
|
|
59
|
+
export declare function trustScore(lastN?: number): Promise<{
|
|
60
|
+
score: number | null;
|
|
61
|
+
verified: number;
|
|
62
|
+
failed: number;
|
|
63
|
+
total: number;
|
|
64
|
+
} | null>;
|
|
65
|
+
/**
|
|
66
|
+
* Sweep pending claims whose due_at has passed and try to verify them.
|
|
67
|
+
* Returns count of claims verified/failed. Safe to call repeatedly —
|
|
68
|
+
* only processes pending claims.
|
|
69
|
+
*/
|
|
70
|
+
export declare function verifyDueClaims(now?: number): Promise<{
|
|
71
|
+
verified: number;
|
|
72
|
+
failed: number;
|
|
73
|
+
expired: number;
|
|
74
|
+
}>;
|
|
75
|
+
//# sourceMappingURL=claim-tracker.d.ts.map
|