clementine-agent 1.6.2 → 1.7.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.
- package/dist/agent/assistant.js +14 -0
- package/dist/agent/hooks.d.ts +2 -0
- package/dist/agent/hooks.js +58 -0
- package/dist/cli/dashboard.js +377 -4
- package/dist/dashboard/builder/serializer.d.ts +16 -10
- package/dist/dashboard/builder/serializer.js +153 -36
- package/dist/gateway/heartbeat-scheduler.d.ts +20 -0
- package/dist/gateway/heartbeat-scheduler.js +92 -0
- package/dist/memory/store.d.ts +110 -0
- package/dist/memory/store.js +290 -4
- package/dist/memory/write-queue.d.ts +1 -0
- package/dist/memory/write-queue.js +1 -0
- package/dist/tools/builder-tools.js +17 -4
- package/dist/tools/memory-tools.js +152 -7
- package/dist/tools/shared.d.ts +9 -0
- package/dist/types.d.ts +3 -0
- package/package.json +1 -1
- package/vendor/browser-harness-mcp/README.md +12 -7
- package/vendor/browser-harness-mcp/__pycache__/server.cpython-314.pyc +0 -0
- package/vendor/browser-harness-mcp/server.py +288 -44
package/dist/agent/assistant.js
CHANGED
|
@@ -411,10 +411,24 @@ Routing rule: if the fact is something the agent should *always know* (not just
|
|
|
411
411
|
## Rules:
|
|
412
412
|
- Only save genuinely NEW facts not already present in the Current Memory above.
|
|
413
413
|
- If updating an existing topic, use memory_write(action="update_memory") to REPLACE the section, not append duplicates.
|
|
414
|
+
- If a stored fact is now wrong (user corrected it, situation changed), use memory_write(action="supersede", supersedes_chunk_id=N, reason="…") instead of appending — the old chunk becomes invisible to retrieval, provenance is preserved.
|
|
414
415
|
- If there's nothing new to save, respond "No new facts." and exit — do NOT call any tools.
|
|
415
416
|
- Use the MCP tools (user_model, memory_write, note_create, task_add, note_take).
|
|
416
417
|
- NEVER respond to ${OWNER}. You are invisible. Just save facts and exit.
|
|
417
418
|
|
|
419
|
+
## Salience hint, confidence, reason (memory_write):
|
|
420
|
+
Every memory_write call may include \`salience_hint\` (0.5–2.0), \`confidence\` (0–1), and \`reason\` (one short sentence). Use them — retrieval prioritizes high-salience, deprioritizes low-confidence, and reasons make the memory system explainable.
|
|
421
|
+
|
|
422
|
+
salience_hint:
|
|
423
|
+
- 0.5 — tentative, single-mention, may not be durable
|
|
424
|
+
- 1.0 — normal (default; equivalent to omitting)
|
|
425
|
+
- 1.5 — durable preference, decision, or strong stated opinion
|
|
426
|
+
- 2.0 — identity-level fact (rare): role, name, foundational stance
|
|
427
|
+
|
|
428
|
+
confidence: 1.0 = certain (default), 0.7 = probable, 0.5 = uncertain or heard secondhand, 0.3 = tentative. Lowers retrieval ranking without hiding.
|
|
429
|
+
|
|
430
|
+
reason: one sentence answering "why is this worth keeping?" — e.g. "user just stated firm preference for plain .env over keychain after being burned by it." Skip routine cases.
|
|
431
|
+
|
|
418
432
|
## Behavioral Correction Detection:
|
|
419
433
|
If ${OWNER} corrects HOW the assistant behaved (not a factual correction), output a JSON block:
|
|
420
434
|
\`\`\`json-behavioral
|
package/dist/agent/hooks.d.ts
CHANGED
|
@@ -48,6 +48,8 @@ export interface AuditEvent {
|
|
|
48
48
|
*/
|
|
49
49
|
export declare function logAuditJsonl(event: AuditEvent): void;
|
|
50
50
|
export declare function setHeartbeatMode(active: boolean, tier2Allowed?: boolean): void;
|
|
51
|
+
export declare function resetBrowserHarnessApproval(): void;
|
|
52
|
+
export declare function isBrowserHarnessApproved(): boolean;
|
|
51
53
|
export declare function setApprovalCallback(cb: ((desc: string) => Promise<boolean>) | null): void;
|
|
52
54
|
export declare function setProfileTier(tier: number | null): void;
|
|
53
55
|
export declare function setProfileAllowedTools(tools: string[] | null): void;
|
package/dist/agent/hooks.js
CHANGED
|
@@ -120,6 +120,16 @@ export function setHeartbeatMode(active, tier2Allowed = false) {
|
|
|
120
120
|
heartbeatActive = active;
|
|
121
121
|
heartbeatTier2Allowed = tier2Allowed;
|
|
122
122
|
}
|
|
123
|
+
// Session-scoped approval for browser harness T3 actions. Once the user
|
|
124
|
+
// approves a session, subsequent T3 calls within that session auto-allow.
|
|
125
|
+
// Resets on daemon restart (in-memory) and on explicit revoke.
|
|
126
|
+
let browserHarnessSessionApproved = false;
|
|
127
|
+
export function resetBrowserHarnessApproval() {
|
|
128
|
+
browserHarnessSessionApproved = false;
|
|
129
|
+
}
|
|
130
|
+
export function isBrowserHarnessApproved() {
|
|
131
|
+
return browserHarnessSessionApproved;
|
|
132
|
+
}
|
|
123
133
|
export function setApprovalCallback(cb) {
|
|
124
134
|
approvalCallback = cb;
|
|
125
135
|
}
|
|
@@ -197,11 +207,23 @@ export function logToolUse(toolName, toolInput) {
|
|
|
197
207
|
// These apply to actual heartbeats and tier-1 cron jobs (read-only).
|
|
198
208
|
// Tier 2+ cron jobs and unleashed tasks bypass these restrictions.
|
|
199
209
|
const HEARTBEAT_DISALLOWED_TIER2 = ['Write', 'Edit', 'Bash'];
|
|
210
|
+
// Browser harness write-class tools — drive the user's real Chrome with their
|
|
211
|
+
// live cookies/sessions. NEVER run these without interactive approval. The
|
|
212
|
+
// MCP server name is "browser-harness" so the SDK exposes them as
|
|
213
|
+
// mcp__browser-harness__<tool>.
|
|
214
|
+
const BROWSER_HARNESS_T3_TOOLS = [
|
|
215
|
+
'mcp__browser-harness__browser_click_xy',
|
|
216
|
+
'mcp__browser-harness__browser_type_text',
|
|
217
|
+
'mcp__browser-harness__browser_press_key',
|
|
218
|
+
'mcp__browser-harness__browser_scroll',
|
|
219
|
+
'mcp__browser-harness__browser_run_python',
|
|
220
|
+
];
|
|
200
221
|
const HEARTBEAT_DISALLOWED_ALWAYS = [
|
|
201
222
|
'Bash', // No raw shell in low-tier autonomous mode
|
|
202
223
|
'Task', // No sub-agents in heartbeats (too short to benefit)
|
|
203
224
|
'Skill', // Skill packs load heavy context and waste turns
|
|
204
225
|
'TodoWrite', // Internal bookkeeping wastes autonomous turns
|
|
226
|
+
...BROWSER_HARNESS_T3_TOOLS, // Browser writes never run unsupervised
|
|
205
227
|
];
|
|
206
228
|
export function getHeartbeatDisallowedTools() {
|
|
207
229
|
const disallowed = [...HEARTBEAT_DISALLOWED_ALWAYS];
|
|
@@ -315,6 +337,42 @@ export async function enforceToolPermissions(toolName, toolInput, sourceOverride
|
|
|
315
337
|
};
|
|
316
338
|
}
|
|
317
339
|
}
|
|
340
|
+
// ── Browser harness T3 — never autonomous, approve once per session ─
|
|
341
|
+
// These tools click/type/scroll/run-python in the user's REAL Chrome
|
|
342
|
+
// with their live cookies. They must never run without explicit consent.
|
|
343
|
+
const effectiveSourceForBrowser = sourceOverride ?? interactionSource;
|
|
344
|
+
if (BROWSER_HARNESS_T3_TOOLS.includes(toolName)) {
|
|
345
|
+
// Hard block during any autonomous context (cron tier-2, unleashed,
|
|
346
|
+
// heartbeat, member-channel sources). Heartbeat block is also handled
|
|
347
|
+
// above via getHeartbeatDisallowedTools, but this catches tier-2 cron
|
|
348
|
+
// and unleashed where heartbeatActive=false.
|
|
349
|
+
if (heartbeatActive || effectiveSourceForBrowser === 'autonomous') {
|
|
350
|
+
appendAuditFile(`[BROWSER-HARNESS] DENIED autonomous: ${toolName}`);
|
|
351
|
+
return {
|
|
352
|
+
behavior: 'deny',
|
|
353
|
+
message: `${toolName} controls your live browser — blocked during autonomous execution. Run interactively instead.`,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
// Interactive: ask once per session. Subsequent T3 calls auto-allow
|
|
357
|
+
// until daemon restart (or explicit revoke via resetBrowserHarnessApproval).
|
|
358
|
+
if (!browserHarnessSessionApproved) {
|
|
359
|
+
if (approvalCallback) {
|
|
360
|
+
const approved = await approvalCallback('Allow Clementine to control your browser this session? Clicks, types, and key presses will run in your real Chrome with your live cookies and logins.');
|
|
361
|
+
if (!approved) {
|
|
362
|
+
return { behavior: 'deny', message: 'Browser control denied by user.' };
|
|
363
|
+
}
|
|
364
|
+
browserHarnessSessionApproved = true;
|
|
365
|
+
appendAuditFile('[BROWSER-HARNESS] Session approval granted');
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
// No approval callback wired — be safe, deny.
|
|
369
|
+
return {
|
|
370
|
+
behavior: 'deny',
|
|
371
|
+
message: 'Browser control requires interactive approval, but no approval callback is set in this context.',
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
318
376
|
// ── Profile tier restrictions (restrict, never elevate) ────────
|
|
319
377
|
if (activeProfileTier !== null) {
|
|
320
378
|
if (activeProfileTier < 2 && ['Bash', 'Write', 'Edit'].includes(toolName)) {
|
package/dist/cli/dashboard.js
CHANGED
|
@@ -3160,6 +3160,7 @@ export async function cmdDashboard(opts) {
|
|
|
3160
3160
|
import('../dashboard/builder/events.js'),
|
|
3161
3161
|
]);
|
|
3162
3162
|
const slug = body.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 64) || 'workflow';
|
|
3163
|
+
const agentSlug = body.agent ? (String(body.agent).trim() || undefined) : undefined;
|
|
3163
3164
|
const wf = {
|
|
3164
3165
|
name: body.name,
|
|
3165
3166
|
description: body.description ?? '',
|
|
@@ -3174,8 +3175,9 @@ export async function cmdDashboard(opts) {
|
|
|
3174
3175
|
maxTurns: 15,
|
|
3175
3176
|
}],
|
|
3176
3177
|
sourceFile: '',
|
|
3178
|
+
agentSlug,
|
|
3177
3179
|
};
|
|
3178
|
-
const id = workflowId(slug);
|
|
3180
|
+
const id = workflowId(slug, agentSlug);
|
|
3179
3181
|
const result = saveWorkflow(id, wf);
|
|
3180
3182
|
if (!result.ok) {
|
|
3181
3183
|
res.status(400).json({ error: result.error });
|
|
@@ -5391,6 +5393,72 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
5391
5393
|
// content out of search results.
|
|
5392
5394
|
// Memory Health snapshot — single endpoint feeding the dashboard tab.
|
|
5393
5395
|
// Read-only aggregate over the existing tables; no caching needed (cheap).
|
|
5396
|
+
// Self-correction stats — supersession provenance. Powers the
|
|
5397
|
+
// "Self-correction (supersedes)" card on Brain → Health.
|
|
5398
|
+
app.get('/api/memory/supersedes', async (_req, res) => {
|
|
5399
|
+
try {
|
|
5400
|
+
const gateway = await getGateway();
|
|
5401
|
+
const store = gateway.assistant?.memoryStore;
|
|
5402
|
+
if (!store?.getSupersedeStats) {
|
|
5403
|
+
res.status(503).json({ error: 'Memory store not available' });
|
|
5404
|
+
return;
|
|
5405
|
+
}
|
|
5406
|
+
res.json({ ok: true, stats: store.getSupersedeStats() });
|
|
5407
|
+
}
|
|
5408
|
+
catch (err) {
|
|
5409
|
+
res.status(500).json({ error: String(err) });
|
|
5410
|
+
}
|
|
5411
|
+
});
|
|
5412
|
+
// Knowledge graph signals — wikilink density + per-match-type recall
|
|
5413
|
+
// contribution. Powers the "Knowledge graph signals" card.
|
|
5414
|
+
app.get('/api/memory/graph-stats', async (_req, res) => {
|
|
5415
|
+
try {
|
|
5416
|
+
const gateway = await getGateway();
|
|
5417
|
+
const store = gateway.assistant?.memoryStore;
|
|
5418
|
+
if (!store?.getGraphStats) {
|
|
5419
|
+
res.status(503).json({ error: 'Memory store not available' });
|
|
5420
|
+
return;
|
|
5421
|
+
}
|
|
5422
|
+
res.json({ ok: true, stats: store.getGraphStats({ topN: 12, lookbackHours: 24 * 7 }) });
|
|
5423
|
+
}
|
|
5424
|
+
catch (err) {
|
|
5425
|
+
res.status(500).json({ error: String(err) });
|
|
5426
|
+
}
|
|
5427
|
+
});
|
|
5428
|
+
// Cross-channel session bridge — recent session summaries grouped by
|
|
5429
|
+
// channel. Powers the "Cross-channel handoff" card.
|
|
5430
|
+
app.get('/api/memory/session-bridge', async (_req, res) => {
|
|
5431
|
+
try {
|
|
5432
|
+
const gateway = await getGateway();
|
|
5433
|
+
const store = gateway.assistant?.memoryStore;
|
|
5434
|
+
if (!store?.getRecentSummariesByChannel) {
|
|
5435
|
+
res.status(503).json({ error: 'Memory store not available' });
|
|
5436
|
+
return;
|
|
5437
|
+
}
|
|
5438
|
+
res.json({ ok: true, grouped: store.getRecentSummariesByChannel(3) });
|
|
5439
|
+
}
|
|
5440
|
+
catch (err) {
|
|
5441
|
+
res.status(500).json({ error: String(err) });
|
|
5442
|
+
}
|
|
5443
|
+
});
|
|
5444
|
+
// Recent writes — what the agent has been capturing, including reason and
|
|
5445
|
+
// salience hint when supplied. Powers the Brain → Health "Recent writes" panel.
|
|
5446
|
+
app.get('/api/memory/writes/recent', async (req, res) => {
|
|
5447
|
+
try {
|
|
5448
|
+
const limit = Math.min(Number(req.query.limit) || 50, 200);
|
|
5449
|
+
const gateway = await getGateway();
|
|
5450
|
+
const store = gateway.assistant?.memoryStore;
|
|
5451
|
+
if (!store?.getRecentWrites) {
|
|
5452
|
+
res.status(503).json({ error: 'Memory store not available' });
|
|
5453
|
+
return;
|
|
5454
|
+
}
|
|
5455
|
+
const writes = store.getRecentWrites(limit);
|
|
5456
|
+
res.json({ ok: true, writes });
|
|
5457
|
+
}
|
|
5458
|
+
catch (err) {
|
|
5459
|
+
res.status(500).json({ error: String(err) });
|
|
5460
|
+
}
|
|
5461
|
+
});
|
|
5394
5462
|
app.get('/api/memory/health', async (_req, res) => {
|
|
5395
5463
|
try {
|
|
5396
5464
|
const gateway = await getGateway();
|
|
@@ -5428,6 +5496,25 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
5428
5496
|
res.json({ ok: true, action, report });
|
|
5429
5497
|
return;
|
|
5430
5498
|
}
|
|
5499
|
+
if (action === 'reembed-dense') {
|
|
5500
|
+
// Run backfill in the background — first call also pays the model
|
|
5501
|
+
// load cost (~440MB download on first ever run). We respond immediately
|
|
5502
|
+
// so the UI doesn't time out; the user re-polls /api/memory/health.
|
|
5503
|
+
const limit = Number(req.body?.limit) > 0 ? Number(req.body.limit) : 200;
|
|
5504
|
+
const embeddings = await import('../memory/embeddings.js');
|
|
5505
|
+
const ready = await embeddings.probeDenseReady();
|
|
5506
|
+
if (!ready) {
|
|
5507
|
+
res.status(503).json({ error: 'Dense embedding model failed to load' });
|
|
5508
|
+
return;
|
|
5509
|
+
}
|
|
5510
|
+
// Fire-and-forget; surface progress via subsequent /api/memory/health polls.
|
|
5511
|
+
store.backfillDenseEmbeddings({ limit }).catch((err) => {
|
|
5512
|
+
// eslint-disable-next-line no-console
|
|
5513
|
+
console.error('[dashboard] reembed-dense failed', err);
|
|
5514
|
+
});
|
|
5515
|
+
res.json({ ok: true, action, started: true, limit });
|
|
5516
|
+
return;
|
|
5517
|
+
}
|
|
5431
5518
|
res.status(400).json({ error: 'Unknown action: ' + action });
|
|
5432
5519
|
}
|
|
5433
5520
|
catch (err) {
|
|
@@ -12315,6 +12402,10 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
12315
12402
|
<option value="agent">agent</option>
|
|
12316
12403
|
<option value="workflow">workflow</option>
|
|
12317
12404
|
</select>
|
|
12405
|
+
<label style="font-size:11px;color:var(--text-muted);font-weight:500;letter-spacing:0.04em;text-transform:uppercase">Owner</label>
|
|
12406
|
+
<select id="builder-owner" onchange="onBuilderOwnerChange()" title="Filter and create scoped to this owner" style="padding:4px 8px;border:1px solid var(--border);border-radius:6px;background:var(--bg-secondary);color:var(--text-primary);font-size:12px;min-width:160px">
|
|
12407
|
+
<option value="">Clementine (global)</option>
|
|
12408
|
+
</select>
|
|
12318
12409
|
<span id="builder-agent-label" style="padding:0;font-size:13px;color:var(--text-secondary);font-weight:500"></span>
|
|
12319
12410
|
<input type="hidden" id="builder-agent" value="">
|
|
12320
12411
|
<span style="flex:1"></span>
|
|
@@ -12771,6 +12862,42 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
12771
12862
|
<div id="memory-health-content">
|
|
12772
12863
|
<div class="skel-block"><div class="skel-row med"></div><div class="skel-row"></div><div class="skel-row short"></div></div>
|
|
12773
12864
|
</div>
|
|
12865
|
+
<div class="card" style="margin-top:18px">
|
|
12866
|
+
<div class="card-header" style="display:flex;align-items:center;justify-content:space-between">
|
|
12867
|
+
<span>Recent writes</span>
|
|
12868
|
+
<span style="font-size:11px;color:var(--text-muted)">What the agent captured, with reason & salience</span>
|
|
12869
|
+
</div>
|
|
12870
|
+
<div class="card-body" id="panel-recent-writes" style="padding:0">
|
|
12871
|
+
<div class="skel-block" style="padding:14px"><div class="skel-row med"></div><div class="skel-row short"></div></div>
|
|
12872
|
+
</div>
|
|
12873
|
+
</div>
|
|
12874
|
+
<div class="card" style="margin-top:18px">
|
|
12875
|
+
<div class="card-header" style="display:flex;align-items:center;justify-content:space-between">
|
|
12876
|
+
<span>Self-correction (supersedes)</span>
|
|
12877
|
+
<span style="font-size:11px;color:var(--text-muted)">Old facts the agent has explicitly replaced</span>
|
|
12878
|
+
</div>
|
|
12879
|
+
<div class="card-body" id="panel-supersedes" style="padding:0">
|
|
12880
|
+
<div class="skel-block" style="padding:14px"><div class="skel-row med"></div><div class="skel-row short"></div></div>
|
|
12881
|
+
</div>
|
|
12882
|
+
</div>
|
|
12883
|
+
<div class="card" style="margin-top:18px">
|
|
12884
|
+
<div class="card-header" style="display:flex;align-items:center;justify-content:space-between">
|
|
12885
|
+
<span>Knowledge graph signals</span>
|
|
12886
|
+
<span style="font-size:11px;color:var(--text-muted)">Wikilink density & which retrieval signal earns its keep (last 7d)</span>
|
|
12887
|
+
</div>
|
|
12888
|
+
<div class="card-body" id="panel-graph-stats" style="padding:0">
|
|
12889
|
+
<div class="skel-block" style="padding:14px"><div class="skel-row med"></div><div class="skel-row short"></div></div>
|
|
12890
|
+
</div>
|
|
12891
|
+
</div>
|
|
12892
|
+
<div class="card" style="margin-top:18px">
|
|
12893
|
+
<div class="card-header" style="display:flex;align-items:center;justify-content:space-between">
|
|
12894
|
+
<span>Cross-channel handoff</span>
|
|
12895
|
+
<span style="font-size:11px;color:var(--text-muted)">Recent session summaries by channel — proves continuity</span>
|
|
12896
|
+
</div>
|
|
12897
|
+
<div class="card-body" id="panel-session-bridge" style="padding:0">
|
|
12898
|
+
<div class="skel-block" style="padding:14px"><div class="skel-row med"></div><div class="skel-row short"></div></div>
|
|
12899
|
+
</div>
|
|
12900
|
+
</div>
|
|
12774
12901
|
<div class="card" style="margin-top:18px">
|
|
12775
12902
|
<div class="card-header" style="display:flex;align-items:center;justify-content:space-between">
|
|
12776
12903
|
<span>Trust & claim verification</span>
|
|
@@ -15050,6 +15177,10 @@ function navigateTo(page, opts) {
|
|
|
15050
15177
|
try { switchTab('intelligence', intelTab); } catch (e) { /* */ }
|
|
15051
15178
|
if (bt === 'health') {
|
|
15052
15179
|
if (typeof refreshMemoryHealth === 'function') refreshMemoryHealth();
|
|
15180
|
+
if (typeof refreshRecentWrites === 'function') refreshRecentWrites();
|
|
15181
|
+
if (typeof refreshSupersedes === 'function') refreshSupersedes();
|
|
15182
|
+
if (typeof refreshGraphStats === 'function') refreshGraphStats();
|
|
15183
|
+
if (typeof refreshSessionBridge === 'function') refreshSessionBridge();
|
|
15053
15184
|
if (typeof refreshClaims === 'function') refreshClaims();
|
|
15054
15185
|
if (typeof refreshRoutingAudit === 'function') refreshRoutingAudit();
|
|
15055
15186
|
}
|
|
@@ -15392,6 +15523,10 @@ function switchTab(group, tab) {
|
|
|
15392
15523
|
if (tab === 'files' && typeof refreshVaultFiles === 'function') refreshVaultFiles();
|
|
15393
15524
|
if (tab === 'health') {
|
|
15394
15525
|
if (typeof refreshMemoryHealth === 'function') refreshMemoryHealth();
|
|
15526
|
+
if (typeof refreshRecentWrites === 'function') refreshRecentWrites();
|
|
15527
|
+
if (typeof refreshSupersedes === 'function') refreshSupersedes();
|
|
15528
|
+
if (typeof refreshGraphStats === 'function') refreshGraphStats();
|
|
15529
|
+
if (typeof refreshSessionBridge === 'function') refreshSessionBridge();
|
|
15395
15530
|
if (typeof refreshClaims === 'function') refreshClaims();
|
|
15396
15531
|
if (typeof refreshRoutingAudit === 'function') refreshRoutingAudit();
|
|
15397
15532
|
}
|
|
@@ -20625,12 +20760,220 @@ function formatBytes(n) {
|
|
|
20625
20760
|
return (n / 1024 / 1024 / 1024).toFixed(2) + ' GB';
|
|
20626
20761
|
}
|
|
20627
20762
|
|
|
20628
|
-
async function
|
|
20629
|
-
var
|
|
20763
|
+
async function refreshSupersedes() {
|
|
20764
|
+
var el = document.getElementById('panel-supersedes');
|
|
20765
|
+
if (!el) return;
|
|
20766
|
+
try {
|
|
20767
|
+
var r = await apiFetch('/api/memory/supersedes');
|
|
20768
|
+
var d = await r.json();
|
|
20769
|
+
if (!d.ok || !d.stats) {
|
|
20770
|
+
el.innerHTML = '<div class="empty-state" style="padding:14px">' + esc(d.error || 'No data') + '</div>';
|
|
20771
|
+
return;
|
|
20772
|
+
}
|
|
20773
|
+
var s = d.stats;
|
|
20774
|
+
if (!s.recent || s.recent.length === 0) {
|
|
20775
|
+
el.innerHTML = '<div class="empty-state" style="padding:14px">No supersedes yet. The agent will record corrections here when it explicitly replaces a stale fact.</div>';
|
|
20776
|
+
return;
|
|
20777
|
+
}
|
|
20778
|
+
var html = '<div style="padding:10px 14px;font-size:12px;color:var(--text-muted)">Total: <b>' + (s.superseded || 0) + '</b> chunks superseded.</div>';
|
|
20779
|
+
html += '<table class="data-table" style="width:100%"><thead><tr>'
|
|
20780
|
+
+ '<th style="width:140px">When</th>'
|
|
20781
|
+
+ '<th style="width:90px">Old → New</th>'
|
|
20782
|
+
+ '<th>Reason</th></tr></thead><tbody>';
|
|
20783
|
+
for (var i = 0; i < s.recent.length; i++) {
|
|
20784
|
+
var sup = s.recent[i];
|
|
20785
|
+
var when = '';
|
|
20786
|
+
try { when = new Date(sup.supersededAt + 'Z').toLocaleString(); } catch { when = sup.supersededAt; }
|
|
20787
|
+
html += '<tr>'
|
|
20788
|
+
+ '<td style="font-size:11px;color:var(--text-muted)">' + esc(when) + '</td>'
|
|
20789
|
+
+ '<td style="font-size:12px"><code>#' + sup.oldId + '</code> → <code>#' + sup.newId + '</code></td>'
|
|
20790
|
+
+ '<td style="font-size:12px">' + (sup.reason ? esc(sup.reason) : '<span style="color:var(--text-muted);opacity:0.6">no reason recorded</span>') + '</td>'
|
|
20791
|
+
+ '</tr>';
|
|
20792
|
+
}
|
|
20793
|
+
html += '</tbody></table>';
|
|
20794
|
+
el.innerHTML = html;
|
|
20795
|
+
} catch (err) {
|
|
20796
|
+
el.innerHTML = '<div class="empty-state" style="padding:14px">Failed to load: ' + esc(String(err)) + '</div>';
|
|
20797
|
+
}
|
|
20798
|
+
}
|
|
20799
|
+
|
|
20800
|
+
async function refreshGraphStats() {
|
|
20801
|
+
var el = document.getElementById('panel-graph-stats');
|
|
20802
|
+
if (!el) return;
|
|
20803
|
+
try {
|
|
20804
|
+
var r = await apiFetch('/api/memory/graph-stats');
|
|
20805
|
+
var d = await r.json();
|
|
20806
|
+
if (!d.ok || !d.stats) {
|
|
20807
|
+
el.innerHTML = '<div class="empty-state" style="padding:14px">' + esc(d.error || 'No data') + '</div>';
|
|
20808
|
+
return;
|
|
20809
|
+
}
|
|
20810
|
+
var s = d.stats;
|
|
20811
|
+
var cb = s.recallContributionByType || {};
|
|
20812
|
+
var totalMatches = 0;
|
|
20813
|
+
for (var k in cb) totalMatches += (cb[k] || 0);
|
|
20814
|
+
|
|
20815
|
+
var html = '<div style="padding:12px 14px;display:grid;grid-template-columns:1fr 1fr;gap:14px">';
|
|
20816
|
+
html += '<div>';
|
|
20817
|
+
html += '<div style="font-size:12px;color:var(--text-muted);margin-bottom:8px">Wikilinks</div>';
|
|
20818
|
+
html += '<div style="font-size:24px;font-weight:700;margin-bottom:8px">' + (s.wikilinkCount || 0).toLocaleString() + '</div>';
|
|
20819
|
+
if (s.topLinkedTargets && s.topLinkedTargets.length > 0) {
|
|
20820
|
+
html += '<div style="font-size:11px;color:var(--text-muted);margin-bottom:4px">Top-linked entities</div>';
|
|
20821
|
+
html += '<table style="width:100%;font-size:12px"><tbody>';
|
|
20822
|
+
for (var i = 0; i < s.topLinkedTargets.length; i++) {
|
|
20823
|
+
var t = s.topLinkedTargets[i];
|
|
20824
|
+
html += '<tr><td style="padding:2px 0">' + esc(t.target) + '</td><td style="text-align:right;color:var(--text-muted);padding:2px 0">' + t.count + '</td></tr>';
|
|
20825
|
+
}
|
|
20826
|
+
html += '</tbody></table>';
|
|
20827
|
+
} else {
|
|
20828
|
+
html += '<div class="empty-state" style="font-size:12px">No wikilinks yet — add [[Name]] refs in vault notes to grow the graph.</div>';
|
|
20829
|
+
}
|
|
20830
|
+
html += '</div>';
|
|
20831
|
+
|
|
20832
|
+
html += '<div>';
|
|
20833
|
+
html += '<div style="font-size:12px;color:var(--text-muted);margin-bottom:8px">Recall contribution (last 7d, ' + (s.tracesAnalyzed || 0) + ' traces)</div>';
|
|
20834
|
+
if (totalMatches === 0) {
|
|
20835
|
+
html += '<div class="empty-state" style="font-size:12px">No recall traces yet. Run a chat to populate.</div>';
|
|
20836
|
+
} else {
|
|
20837
|
+
var order = ['fts', 'vector', 'graph', 'recency'];
|
|
20838
|
+
var labels = { fts: 'Lexical (FTS5)', vector: 'Semantic (dense)', graph: 'Graph (wikilinks)', recency: 'Recency' };
|
|
20839
|
+
var colors = { fts: '#60a5fa', vector: '#10b981', graph: '#f59e0b', recency: '#9ca3af' };
|
|
20840
|
+
html += '<table style="width:100%;font-size:12px"><tbody>';
|
|
20841
|
+
for (var j = 0; j < order.length; j++) {
|
|
20842
|
+
var key = order[j];
|
|
20843
|
+
var cnt = cb[key] || 0;
|
|
20844
|
+
var pct = totalMatches > 0 ? Math.round((cnt / totalMatches) * 100) : 0;
|
|
20845
|
+
html += '<tr><td style="padding:3px 8px 3px 0;width:140px">' + labels[key] + '</td>'
|
|
20846
|
+
+ '<td style="padding:3px 0">'
|
|
20847
|
+
+ '<div style="display:flex;align-items:center;gap:8px">'
|
|
20848
|
+
+ '<div style="flex:1;background:var(--bg-secondary, #1a1a1a);border-radius:3px;overflow:hidden;height:14px">'
|
|
20849
|
+
+ '<div style="width:' + pct + '%;background:' + colors[key] + ';height:100%"></div>'
|
|
20850
|
+
+ '</div>'
|
|
20851
|
+
+ '<span style="font-variant-numeric:tabular-nums;width:50px;text-align:right;color:var(--text-muted)">' + pct + '%</span>'
|
|
20852
|
+
+ '</div>'
|
|
20853
|
+
+ '</td></tr>';
|
|
20854
|
+
}
|
|
20855
|
+
html += '</tbody></table>';
|
|
20856
|
+
}
|
|
20857
|
+
html += '</div></div>';
|
|
20858
|
+
el.innerHTML = html;
|
|
20859
|
+
} catch (err) {
|
|
20860
|
+
el.innerHTML = '<div class="empty-state" style="padding:14px">Failed to load: ' + esc(String(err)) + '</div>';
|
|
20861
|
+
}
|
|
20862
|
+
}
|
|
20863
|
+
|
|
20864
|
+
async function refreshSessionBridge() {
|
|
20865
|
+
var el = document.getElementById('panel-session-bridge');
|
|
20866
|
+
if (!el) return;
|
|
20867
|
+
try {
|
|
20868
|
+
var r = await apiFetch('/api/memory/session-bridge');
|
|
20869
|
+
var d = await r.json();
|
|
20870
|
+
if (!d.ok || !d.grouped) {
|
|
20871
|
+
el.innerHTML = '<div class="empty-state" style="padding:14px">' + esc(d.error || 'No data') + '</div>';
|
|
20872
|
+
return;
|
|
20873
|
+
}
|
|
20874
|
+
var channels = Object.keys(d.grouped).sort();
|
|
20875
|
+
if (channels.length === 0) {
|
|
20876
|
+
el.innerHTML = '<div class="empty-state" style="padding:14px">No session summaries yet. They populate as conversations end.</div>';
|
|
20877
|
+
return;
|
|
20878
|
+
}
|
|
20879
|
+
var html = '<div style="padding:12px 14px;display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:14px">';
|
|
20880
|
+
for (var ci = 0; ci < channels.length; ci++) {
|
|
20881
|
+
var ch = channels[ci];
|
|
20882
|
+
var summaries = d.grouped[ch] || [];
|
|
20883
|
+
html += '<div style="border:1px solid var(--border);border-radius:6px;padding:10px;background:var(--bg-secondary, transparent)">';
|
|
20884
|
+
html += '<div style="font-size:11px;color:var(--text-muted);margin-bottom:6px;text-transform:uppercase;letter-spacing:0.05em">' + esc(ch) + ' · ' + summaries.length + ' recent</div>';
|
|
20885
|
+
for (var si = 0; si < summaries.length; si++) {
|
|
20886
|
+
var ss = summaries[si];
|
|
20887
|
+
var when = '';
|
|
20888
|
+
try { when = new Date(ss.createdAt + 'Z').toLocaleString(); } catch { when = ss.createdAt; }
|
|
20889
|
+
var preview = (ss.summary || '').slice(0, 200);
|
|
20890
|
+
html += '<div style="font-size:12px;margin-bottom:8px;padding-bottom:8px;border-bottom:1px solid var(--border)">'
|
|
20891
|
+
+ '<div style="color:var(--text-muted);font-size:10px;margin-bottom:2px">' + esc(when) + ' · ' + ss.exchangeCount + ' exchanges</div>'
|
|
20892
|
+
+ esc(preview) + (ss.summary && ss.summary.length > 200 ? '…' : '')
|
|
20893
|
+
+ '</div>';
|
|
20894
|
+
}
|
|
20895
|
+
html += '</div>';
|
|
20896
|
+
}
|
|
20897
|
+
html += '</div>';
|
|
20898
|
+
el.innerHTML = html;
|
|
20899
|
+
} catch (err) {
|
|
20900
|
+
el.innerHTML = '<div class="empty-state" style="padding:14px">Failed to load: ' + esc(String(err)) + '</div>';
|
|
20901
|
+
}
|
|
20902
|
+
}
|
|
20903
|
+
|
|
20904
|
+
async function refreshRecentWrites() {
|
|
20905
|
+
var el = document.getElementById('panel-recent-writes');
|
|
20906
|
+
if (!el) return;
|
|
20907
|
+
try {
|
|
20908
|
+
var r = await apiFetch('/api/memory/writes/recent?limit=30');
|
|
20909
|
+
var d = await r.json();
|
|
20910
|
+
if (!d.ok || !Array.isArray(d.writes)) {
|
|
20911
|
+
el.innerHTML = '<div class="empty-state" style="padding:14px">' + esc(d.error || 'No data') + '</div>';
|
|
20912
|
+
return;
|
|
20913
|
+
}
|
|
20914
|
+
if (d.writes.length === 0) {
|
|
20915
|
+
el.innerHTML = '<div class="empty-state" style="padding:14px">No writes captured yet. The agent will start logging memory_write activity here once Phase 2 reaches her.</div>';
|
|
20916
|
+
return;
|
|
20917
|
+
}
|
|
20918
|
+
var html = '<table class="data-table" style="width:100%">';
|
|
20919
|
+
html += '<thead><tr>'
|
|
20920
|
+
+ '<th style="width:120px">When</th>'
|
|
20921
|
+
+ '<th style="width:80px">Agent</th>'
|
|
20922
|
+
+ '<th style="width:120px">Action</th>'
|
|
20923
|
+
+ '<th>Where</th>'
|
|
20924
|
+
+ '<th style="width:60px;text-align:right">Sal</th>'
|
|
20925
|
+
+ '<th>Reason</th>'
|
|
20926
|
+
+ '<th style="width:90px">Status</th>'
|
|
20927
|
+
+ '</tr></thead><tbody>';
|
|
20928
|
+
for (var i = 0; i < d.writes.length; i++) {
|
|
20929
|
+
var w = d.writes[i];
|
|
20930
|
+
var when = '';
|
|
20931
|
+
try { when = new Date(w.extractedAt + 'Z').toLocaleString(); } catch { when = w.extractedAt; }
|
|
20932
|
+
var where = w.section ? esc(w.section) : (w.filePath ? esc(w.filePath) : '—');
|
|
20933
|
+
var sal = (w.salienceHint != null) ? Number(w.salienceHint).toFixed(1) : '—';
|
|
20934
|
+
var salColor = (w.salienceHint != null && w.salienceHint > 1.0) ? 'var(--success, #10b981)'
|
|
20935
|
+
: (w.salienceHint != null && w.salienceHint < 1.0) ? 'var(--text-muted)' : 'var(--text-primary)';
|
|
20936
|
+
var statusBadge = w.status === 'dedup_skipped'
|
|
20937
|
+
? '<span style="color:var(--text-muted);font-size:11px">deduped</span>'
|
|
20938
|
+
: w.status === 'active'
|
|
20939
|
+
? '<span style="color:var(--success, #10b981);font-size:11px">written</span>'
|
|
20940
|
+
: '<span style="font-size:11px">' + esc(w.status) + '</span>';
|
|
20941
|
+
html += '<tr>'
|
|
20942
|
+
+ '<td style="font-size:11px;color:var(--text-muted)">' + esc(when) + '</td>'
|
|
20943
|
+
+ '<td style="font-size:11px">' + esc(w.agentSlug || 'global') + '</td>'
|
|
20944
|
+
+ '<td style="font-size:12px"><code>' + esc(w.action || w.toolName) + '</code></td>'
|
|
20945
|
+
+ '<td style="font-size:12px">' + where + '</td>'
|
|
20946
|
+
+ '<td style="text-align:right;font-weight:600;color:' + salColor + '">' + sal + '</td>'
|
|
20947
|
+
+ '<td style="font-size:12px;color:var(--text-muted)">' + (w.reason ? esc(w.reason) : '<span style="opacity:0.5">no reason given</span>') + '</td>'
|
|
20948
|
+
+ '<td>' + statusBadge + '</td>'
|
|
20949
|
+
+ '</tr>';
|
|
20950
|
+
}
|
|
20951
|
+
html += '</tbody></table>';
|
|
20952
|
+
el.innerHTML = html;
|
|
20953
|
+
} catch (err) {
|
|
20954
|
+
el.innerHTML = '<div class="empty-state" style="padding:14px">Failed to load: ' + esc(String(err)) + '</div>';
|
|
20955
|
+
}
|
|
20956
|
+
}
|
|
20957
|
+
|
|
20958
|
+
async function memoryHealthAction(action, extra) {
|
|
20959
|
+
var labels = { 'janitor': 'cleanup', 'rebuild-fts': 'FTS rebuild', 'fix-orphans': 'orphan fix', 'reembed-dense': 'dense embedding backfill' };
|
|
20630
20960
|
if (!confirm('Run ' + (labels[action] || action) + ' now?')) return;
|
|
20631
20961
|
try {
|
|
20632
|
-
var
|
|
20962
|
+
var body = Object.assign({ action: action }, extra || {});
|
|
20963
|
+
var r = await apiJson('POST', '/api/memory/health/action', body);
|
|
20633
20964
|
if (r.error) { toast('Action failed: ' + r.error, 'error'); return; }
|
|
20965
|
+
if (action === 'reembed-dense' && r.started) {
|
|
20966
|
+
toast('Backfill started in background (' + (r.limit || '?') + ' chunks). Refreshing every 10s…', 'info');
|
|
20967
|
+
// Poll coverage updates so the user sees progress without manually refreshing.
|
|
20968
|
+
var pollCount = 0;
|
|
20969
|
+
var poll = setInterval(function() {
|
|
20970
|
+
refreshMemoryHealth();
|
|
20971
|
+
pollCount++;
|
|
20972
|
+
if (pollCount >= 12) clearInterval(poll); // ~2 minutes
|
|
20973
|
+
}, 10000);
|
|
20974
|
+
refreshMemoryHealth();
|
|
20975
|
+
return;
|
|
20976
|
+
}
|
|
20634
20977
|
var detail = '';
|
|
20635
20978
|
if (r.result) detail = ' — ' + Object.entries(r.result).slice(0, 4).map(function(p) { return p[0] + ':' + p[1]; }).join(', ');
|
|
20636
20979
|
if (r.report) detail = ' — orphans nulled: ' + (r.report.orphanRefsNulled || 0) + ', FTS rebuilds: ' + (r.report.ftsRebuilds || 0);
|
|
@@ -20857,8 +21200,38 @@ async function refreshMemoryHealth() {
|
|
|
20857
21200
|
html += '<div class="metric-hero"><div class="metric-hero-value">' + formatBytes(h.dbSizeBytes)
|
|
20858
21201
|
+ '</div><div class="metric-hero-label">DB File Size</div>'
|
|
20859
21202
|
+ '<div class="metric-hero-sub">last vacuum: ' + esc(h.lastVacuumAt || 'never') + '</div></div>';
|
|
21203
|
+
|
|
21204
|
+
// Dense embedding coverage — the leading indicator for retrieval quality.
|
|
21205
|
+
// <50% means the agent is mostly searching on TF-IDF and missing semantic matches.
|
|
21206
|
+
var de = h.denseEmbeddings || { withDense: 0, total: 0, models: [], currentModel: '', ready: false };
|
|
21207
|
+
var densePct = de.total > 0 ? ((de.withDense / de.total) * 100).toFixed(1) : '0.0';
|
|
21208
|
+
var denseColor = de.total === 0 ? 'var(--text-muted)'
|
|
21209
|
+
: (de.withDense / Math.max(1, de.total)) >= 0.95 ? 'var(--success, #10b981)'
|
|
21210
|
+
: (de.withDense / Math.max(1, de.total)) >= 0.5 ? 'var(--warning, #f59e0b)'
|
|
21211
|
+
: 'var(--danger, #ef4444)';
|
|
21212
|
+
var modelLabel = de.currentModel ? de.currentModel.split('/').pop() : '—';
|
|
21213
|
+
html += '<div class="metric-hero" style="border-left:3px solid ' + denseColor + '">'
|
|
21214
|
+
+ '<div class="metric-hero-value" style="color:' + denseColor + '">' + densePct + '%</div>'
|
|
21215
|
+
+ '<div class="metric-hero-label">Semantic Coverage</div>'
|
|
21216
|
+
+ '<div class="metric-hero-sub">' + (de.withDense || 0) + ' of ' + (de.total || 0)
|
|
21217
|
+
+ ' chunks · ' + esc(modelLabel) + '</div></div>';
|
|
21218
|
+
|
|
20860
21219
|
html += '</div>';
|
|
20861
21220
|
|
|
21221
|
+
// Coverage call-to-action — only render when there's work to do.
|
|
21222
|
+
if (de.total > 0 && de.withDense < de.total) {
|
|
21223
|
+
var missing = de.total - de.withDense;
|
|
21224
|
+
html += '<div class="card" style="margin-bottom:16px;border-left:3px solid ' + denseColor + '">';
|
|
21225
|
+
html += '<div class="card-body" style="padding:14px;display:flex;align-items:center;gap:14px;flex-wrap:wrap">';
|
|
21226
|
+
html += '<div style="flex:1;min-width:240px">';
|
|
21227
|
+
html += '<div style="font-weight:600;margin-bottom:4px">Retrieval running on sparse vectors for ' + missing.toLocaleString() + ' chunks</div>';
|
|
21228
|
+
html += '<div style="font-size:12px;color:var(--text-muted)">Backfill builds 768-dim neural embeddings for semantic search. First run downloads ~440MB.</div>';
|
|
21229
|
+
html += '</div>';
|
|
21230
|
+
html += '<button class="btn-sm" onclick="memoryHealthAction(\'reembed-dense\', { limit: 200 })" title="Embed up to 200 chunks now">Backfill 200</button>';
|
|
21231
|
+
html += '<button class="btn-sm" onclick="memoryHealthAction(\'reembed-dense\', { limit: 2000 })" title="Embed up to 2000 chunks now (slower)">Backfill 2000</button>';
|
|
21232
|
+
html += '</div></div>';
|
|
21233
|
+
}
|
|
21234
|
+
|
|
20862
21235
|
// Two-column layout: categories + table sizes on left, top cited on right.
|
|
20863
21236
|
html += '<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px">';
|
|
20864
21237
|
|
|
@@ -15,15 +15,24 @@
|
|
|
15
15
|
* unless edited through this module, and edits preserve unrelated fields.
|
|
16
16
|
*/
|
|
17
17
|
import type { WorkflowDefinition, CronJobDefinition, BuilderWorkflowSummary, WorkflowOriginKind } from '../../types.js';
|
|
18
|
-
export declare function cronId(name: string): string;
|
|
19
|
-
export declare function workflowId(filename: string): string;
|
|
20
|
-
export
|
|
18
|
+
export declare function cronId(name: string, agentSlug?: string): string;
|
|
19
|
+
export declare function workflowId(filename: string, agentSlug?: string): string;
|
|
20
|
+
export type ParsedBuilderId = {
|
|
21
21
|
origin: WorkflowOriginKind;
|
|
22
|
+
scope: 'global';
|
|
22
23
|
key: string;
|
|
23
|
-
} |
|
|
24
|
+
} | {
|
|
25
|
+
origin: WorkflowOriginKind;
|
|
26
|
+
scope: 'agent';
|
|
27
|
+
agentSlug: string;
|
|
28
|
+
key: string;
|
|
29
|
+
};
|
|
30
|
+
export declare function parseBuilderId(id: string): ParsedBuilderId | null;
|
|
24
31
|
export declare function listAllForBuilder(): BuilderWorkflowSummary[];
|
|
25
32
|
export declare function readWorkflow(id: string): WorkflowDefinition | null;
|
|
26
|
-
export declare function cronJobToWorkflow(job: CronJobDefinition
|
|
33
|
+
export declare function cronJobToWorkflow(job: CronJobDefinition, opts?: {
|
|
34
|
+
sourceFile?: string;
|
|
35
|
+
}): WorkflowDefinition;
|
|
27
36
|
/** True if a workflow is shaped like a CRON.md entry (single prompt step + cron schedule). */
|
|
28
37
|
export declare function isCronShape(wf: WorkflowDefinition): boolean;
|
|
29
38
|
export declare function saveWorkflow(id: string, wf: WorkflowDefinition): {
|
|
@@ -32,11 +41,8 @@ export declare function saveWorkflow(id: string, wf: WorkflowDefinition): {
|
|
|
32
41
|
ok: false;
|
|
33
42
|
error: string;
|
|
34
43
|
};
|
|
35
|
-
/** Resolve the on-disk file path for a builder id
|
|
36
|
-
export declare function sourceFileForId(id: string, parsedHint?:
|
|
37
|
-
origin: WorkflowOriginKind;
|
|
38
|
-
key: string;
|
|
39
|
-
}): string | null;
|
|
44
|
+
/** Resolve the on-disk file path for a builder id. */
|
|
45
|
+
export declare function sourceFileForId(id: string, parsedHint?: ParsedBuilderId): string | null;
|
|
40
46
|
/** Drawflow node shape (subset we use). */
|
|
41
47
|
interface DrawflowNode {
|
|
42
48
|
id: number;
|