clementine-agent 1.18.84 → 1.18.86
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/run-agent.d.ts +4 -0
- package/dist/agent/run-agent.js +116 -3
- package/dist/cli/cron.js +6 -0
- package/dist/cli/dashboard.js +221 -3
- package/dist/gateway/cron-scheduler.js +2 -0
- package/dist/gateway/event-log.d.ts +36 -0
- package/dist/gateway/event-log.js +136 -0
- package/dist/gateway/router.d.ts +2 -0
- package/dist/gateway/router.js +1 -0
- package/dist/types.d.ts +48 -0
- package/package.json +1 -1
|
@@ -117,6 +117,10 @@ export interface RunAgentResult {
|
|
|
117
117
|
cache_read_input_tokens?: number;
|
|
118
118
|
cache_creation_input_tokens?: number;
|
|
119
119
|
};
|
|
120
|
+
/** PRD §6 / 1.18.85: stable run UUID. The Event store keys off this id;
|
|
121
|
+
* callers (cron-scheduler, cron CLI) stamp it on CronRunEntry so the
|
|
122
|
+
* Run detail page can join run row → events. */
|
|
123
|
+
runId: string;
|
|
120
124
|
}
|
|
121
125
|
/**
|
|
122
126
|
* Run a single agent invocation via the canonical SDK pattern.
|
package/dist/agent/run-agent.js
CHANGED
|
@@ -22,8 +22,10 @@
|
|
|
22
22
|
* long-task preflight, NO mode=unleashed wrapper.
|
|
23
23
|
*/
|
|
24
24
|
import path from 'node:path';
|
|
25
|
+
import { randomUUID } from 'node:crypto';
|
|
25
26
|
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
26
27
|
import pino from 'pino';
|
|
28
|
+
import { EventLog } from '../gateway/event-log.js';
|
|
27
29
|
/**
|
|
28
30
|
* Module-level cache of MCP server statuses from the most recent SDK
|
|
29
31
|
* init message. Populated by every runAgent / PersonalAssistant query
|
|
@@ -58,6 +60,27 @@ export function recordMcpStatusFromSystemInit(rawMcpServers) {
|
|
|
58
60
|
}
|
|
59
61
|
_lastMcpStatusSnapshot = { servers, updatedAt: new Date().toISOString() };
|
|
60
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Truncate a value for the Event store so a single huge tool input/output
|
|
65
|
+
* doesn't blow out the JSONL line. Object/array shapes are JSON-stringified
|
|
66
|
+
* and capped; primitive strings are sliced; everything else is returned as-is.
|
|
67
|
+
*/
|
|
68
|
+
function truncateForLog(value, maxBytes) {
|
|
69
|
+
if (value == null)
|
|
70
|
+
return value;
|
|
71
|
+
if (typeof value === 'string') {
|
|
72
|
+
return value.length > maxBytes ? value.slice(0, maxBytes) + '...[truncated]' : value;
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const json = JSON.stringify(value);
|
|
76
|
+
if (json.length <= maxBytes)
|
|
77
|
+
return value;
|
|
78
|
+
return { _truncated: true, preview: json.slice(0, maxBytes) + '...[truncated]', _bytes: json.length };
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return { _unstringifiable: true };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
61
84
|
/** Drop one server from the cache so the next query repopulates it. */
|
|
62
85
|
export function invalidateMcpStatusEntry(name) {
|
|
63
86
|
const before = _lastMcpStatusSnapshot.servers.length;
|
|
@@ -261,6 +284,23 @@ export async function runAgent(prompt, opts) {
|
|
|
261
284
|
agentCount: Object.keys(agents).length,
|
|
262
285
|
allowedToolCount: allowedTools.length,
|
|
263
286
|
}, 'runAgent: starting query');
|
|
287
|
+
// PRD §6 / 1.18.85: path A in-process tap. One Event row per significant
|
|
288
|
+
// SDK message, written to ~/.clementine/events/<runId>.jsonl. The Run
|
|
289
|
+
// detail page (Phase 4b) reads from this file. EventLog.append never
|
|
290
|
+
// throws back to the caller — telemetry is best-effort.
|
|
291
|
+
const runId = randomUUID();
|
|
292
|
+
const eventLog = new EventLog();
|
|
293
|
+
let eventSeq = 0;
|
|
294
|
+
const writeEvent = (e) => {
|
|
295
|
+
try {
|
|
296
|
+
eventLog.append({
|
|
297
|
+
...e,
|
|
298
|
+
runId,
|
|
299
|
+
seq: eventSeq++,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
catch { /* never block */ }
|
|
303
|
+
};
|
|
264
304
|
let finalText = '';
|
|
265
305
|
let sessionId = '';
|
|
266
306
|
let totalCostUsd = 0;
|
|
@@ -283,15 +323,27 @@ export async function runAgent(prompt, opts) {
|
|
|
283
323
|
}
|
|
284
324
|
catch { /* non-fatal */ }
|
|
285
325
|
}
|
|
286
|
-
|
|
326
|
+
// PRD Phase 4a / 1.18.85: write the session_start Event row.
|
|
327
|
+
writeEvent({ kind: 'session_start', ts: new Date().toISOString(), sessionId });
|
|
328
|
+
logger.debug({ sessionKey: opts.sessionKey, sdkSessionId: sessionId, runId }, 'runAgent: SDK session initialized');
|
|
287
329
|
continue;
|
|
288
330
|
}
|
|
289
331
|
if (message.type === 'assistant') {
|
|
290
332
|
const am = message;
|
|
333
|
+
// SDK content blocks include text, tool_use, and (when extended-thinking
|
|
334
|
+
// is enabled) thinking. We tap each kind into the Event store.
|
|
291
335
|
const blocks = (am.message?.content ?? []);
|
|
292
336
|
for (const block of blocks) {
|
|
293
337
|
if (block.type === 'text' && typeof block.text === 'string') {
|
|
294
338
|
finalText += block.text;
|
|
339
|
+
// PRD Phase 4a / 1.18.85: llm_text Event. Truncate at 8KB to keep
|
|
340
|
+
// the JSONL light — full text is reachable via the SDK transcript.
|
|
341
|
+
writeEvent({
|
|
342
|
+
kind: 'llm_text',
|
|
343
|
+
ts: new Date().toISOString(),
|
|
344
|
+
sessionId,
|
|
345
|
+
text: block.text.slice(0, 8000),
|
|
346
|
+
});
|
|
295
347
|
if (opts.onText) {
|
|
296
348
|
try {
|
|
297
349
|
await opts.onText(block.text);
|
|
@@ -299,7 +351,29 @@ export async function runAgent(prompt, opts) {
|
|
|
299
351
|
catch { /* streaming is best-effort */ }
|
|
300
352
|
}
|
|
301
353
|
}
|
|
354
|
+
else if (block.type === 'thinking' && typeof block.thinking === 'string') {
|
|
355
|
+
// Extended-thinking block — captured separately so the Run detail
|
|
356
|
+
// page can render thinking distinctly from final text.
|
|
357
|
+
writeEvent({
|
|
358
|
+
kind: 'thinking',
|
|
359
|
+
ts: new Date().toISOString(),
|
|
360
|
+
sessionId,
|
|
361
|
+
thinking: block.thinking.slice(0, 8000),
|
|
362
|
+
});
|
|
363
|
+
}
|
|
302
364
|
else if (block.type === 'tool_use' && typeof block.name === 'string') {
|
|
365
|
+
// PRD Phase 4a: tool_call Event. The tool_use id pairs with the
|
|
366
|
+
// tool_result Event written when the SDK reports back. Inputs
|
|
367
|
+
// truncated at 8KB; the dashboard can fetch the full transcript
|
|
368
|
+
// if a deeper drill-down is needed.
|
|
369
|
+
writeEvent({
|
|
370
|
+
kind: 'tool_call',
|
|
371
|
+
ts: new Date().toISOString(),
|
|
372
|
+
sessionId,
|
|
373
|
+
toolName: block.name,
|
|
374
|
+
toolUseId: block.id,
|
|
375
|
+
toolInput: truncateForLog(block.input ?? {}, 8000),
|
|
376
|
+
});
|
|
303
377
|
if (opts.onToolActivity) {
|
|
304
378
|
try {
|
|
305
379
|
await opts.onToolActivity({ tool: block.name, input: block.input ?? {} });
|
|
@@ -310,6 +384,26 @@ export async function runAgent(prompt, opts) {
|
|
|
310
384
|
}
|
|
311
385
|
continue;
|
|
312
386
|
}
|
|
387
|
+
// SDK user messages carry tool_result blocks back from tool execution.
|
|
388
|
+
// We pair them with the earlier tool_call Event via toolUseId so the
|
|
389
|
+
// Run detail waterfall renders call → result side by side.
|
|
390
|
+
if (message.type === 'user') {
|
|
391
|
+
const um = message;
|
|
392
|
+
const blocks = um.message?.content ?? [];
|
|
393
|
+
for (const block of blocks) {
|
|
394
|
+
if (block.type === 'tool_result') {
|
|
395
|
+
writeEvent({
|
|
396
|
+
kind: 'tool_result',
|
|
397
|
+
ts: new Date().toISOString(),
|
|
398
|
+
sessionId,
|
|
399
|
+
toolUseId: block.tool_use_id,
|
|
400
|
+
toolResult: truncateForLog(block.content, 16000),
|
|
401
|
+
...(block.is_error ? { toolError: 'tool reported is_error' } : {}),
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
313
407
|
if (message.type === 'result') {
|
|
314
408
|
const result = message;
|
|
315
409
|
sessionId = sessionId || (result.session_id ?? '');
|
|
@@ -325,6 +419,15 @@ export async function runAgent(prompt, opts) {
|
|
|
325
419
|
if (r)
|
|
326
420
|
finalText = r;
|
|
327
421
|
}
|
|
422
|
+
// PRD Phase 4a / 1.18.85: session_end Event — closes the run in the
|
|
423
|
+
// event store and stamps the cost + stop reason for the Run detail page.
|
|
424
|
+
writeEvent({
|
|
425
|
+
kind: 'session_end',
|
|
426
|
+
ts: new Date().toISOString(),
|
|
427
|
+
sessionId,
|
|
428
|
+
costUsd: totalCostUsd,
|
|
429
|
+
stopReason: subtype,
|
|
430
|
+
});
|
|
328
431
|
// Mirror cost to usage_log. Same shape as the existing
|
|
329
432
|
// logQueryResult, but standalone so we don't depend on
|
|
330
433
|
// PersonalAssistant's instance state.
|
|
@@ -353,13 +456,22 @@ export async function runAgent(prompt, opts) {
|
|
|
353
456
|
}
|
|
354
457
|
}
|
|
355
458
|
catch (err) {
|
|
459
|
+
// PRD Phase 4a / 1.18.85: error Event closes the run if the SDK throws.
|
|
460
|
+
// Lets the Run detail page render an explicit failure span instead of a
|
|
461
|
+
// run that just trails off after the last successful tool_call.
|
|
462
|
+
const errMsg = String(err?.message ?? err).slice(0, 1000);
|
|
463
|
+
writeEvent({
|
|
464
|
+
kind: 'error',
|
|
465
|
+
ts: new Date().toISOString(),
|
|
466
|
+
sessionId,
|
|
467
|
+
toolError: errMsg,
|
|
468
|
+
});
|
|
356
469
|
// Translate the SDK's budget-exhaustion throw into a message that
|
|
357
470
|
// tells the user (a) what cap tripped and (b) how to raise it.
|
|
358
471
|
// The raw SDK string ("Claude Code returned an error result:
|
|
359
472
|
// Reached maximum budget ($0.5)") leaks through the channel layer
|
|
360
473
|
// as a generic "Something went wrong:" with no actionable hint.
|
|
361
|
-
|
|
362
|
-
if (/Reached maximum budget|error_max_budget_usd/i.test(msg)) {
|
|
474
|
+
if (/Reached maximum budget|error_max_budget_usd/i.test(errMsg)) {
|
|
363
475
|
const cap = maxBudgetUsd?.toFixed(2) ?? '?';
|
|
364
476
|
const envKey = `BUDGET_${source.toUpperCase().replace(/-/g, '_')}_USD`;
|
|
365
477
|
throw new Error(`Hit the $${cap} ${source} budget cap before finishing. ` +
|
|
@@ -384,6 +496,7 @@ export async function runAgent(prompt, opts) {
|
|
|
384
496
|
sessionId,
|
|
385
497
|
subtype,
|
|
386
498
|
...(usage ? { usage } : {}),
|
|
499
|
+
runId,
|
|
387
500
|
};
|
|
388
501
|
}
|
|
389
502
|
//# sourceMappingURL=run-agent.js.map
|
package/dist/cli/cron.js
CHANGED
|
@@ -144,6 +144,11 @@ export async function cmdCronRun(jobName) {
|
|
|
144
144
|
// by the dashboard's manual-run endpoint. Defaults to 'scheduled' for
|
|
145
145
|
// any other invocation path (CLI, daemon-internal callsites).
|
|
146
146
|
const trigger = process.env.CRON_RUN_TRIGGER || 'scheduled';
|
|
147
|
+
// 1.18.85: pull the run UUID from the gateway's per-run metadata
|
|
148
|
+
// side-channel. runAgent stamps it; consume here to link CronRunEntry
|
|
149
|
+
// to the Event store.
|
|
150
|
+
const sideChannel = gateway.consumeLastCronRunMetadata?.();
|
|
151
|
+
const runIdFromGateway = sideChannel?.runId;
|
|
147
152
|
const entry = {
|
|
148
153
|
jobName: job.name,
|
|
149
154
|
startedAt: startedAt.toISOString(),
|
|
@@ -153,6 +158,7 @@ export async function cmdCronRun(jobName) {
|
|
|
153
158
|
attempt: 1,
|
|
154
159
|
outputPreview: response ? response.slice(0, 200) : undefined,
|
|
155
160
|
trigger,
|
|
161
|
+
...(runIdFromGateway ? { id: runIdFromGateway } : {}),
|
|
156
162
|
};
|
|
157
163
|
// PRD Phase 1.1: goal-orientation evaluator (mirrors the daemon path).
|
|
158
164
|
if (job.successSchema || (job.successCriteriaText && job.successCriteriaText.trim())) {
|
package/dist/cli/dashboard.js
CHANGED
|
@@ -5670,6 +5670,26 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
5670
5670
|
res.status(500).json({ error: String(err) });
|
|
5671
5671
|
}
|
|
5672
5672
|
});
|
|
5673
|
+
// ── PRD Phase 4a / 1.18.85: per-run Event store reader ─────────
|
|
5674
|
+
// Returns every event captured by path A (in-process tap in runAgent)
|
|
5675
|
+
// for one run. Used by the new Run detail page (Phase 4b).
|
|
5676
|
+
app.get('/api/runs/:runId/events', async (req, res) => {
|
|
5677
|
+
try {
|
|
5678
|
+
const rawId = req.params.runId;
|
|
5679
|
+
const runId = Array.isArray(rawId) ? rawId[0] : rawId;
|
|
5680
|
+
if (!runId || typeof runId !== 'string') {
|
|
5681
|
+
res.status(400).json({ ok: false, error: 'runId required' });
|
|
5682
|
+
return;
|
|
5683
|
+
}
|
|
5684
|
+
const { EventLog } = await import('../gateway/event-log.js');
|
|
5685
|
+
const log = new EventLog();
|
|
5686
|
+
const events = log.readByRun(runId);
|
|
5687
|
+
res.json({ ok: true, runId, events });
|
|
5688
|
+
}
|
|
5689
|
+
catch (err) {
|
|
5690
|
+
res.status(500).json({ ok: false, error: String(err) });
|
|
5691
|
+
}
|
|
5692
|
+
});
|
|
5673
5693
|
// ── Recent runs across ALL cron jobs ───────────────────────────
|
|
5674
5694
|
// Powers the "Recent History" zone on the Tasks page. Returns the most
|
|
5675
5695
|
// recent N CronRunEntry rows merged from every per-job .jsonl, sorted
|
|
@@ -6676,6 +6696,24 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
6676
6696
|
if (purge) {
|
|
6677
6697
|
const safe = bareJobName.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
6678
6698
|
const runLog = path.join(BASE_DIR, 'cron', 'runs', `${safe}.jsonl`);
|
|
6699
|
+
// Read run UUIDs BEFORE we drop the run log — the Event store is
|
|
6700
|
+
// keyed by run UUID, not by job name, so the only way to know which
|
|
6701
|
+
// event files to delete is by reading the JSONL first.
|
|
6702
|
+
const runIdsToPurge = [];
|
|
6703
|
+
try {
|
|
6704
|
+
if (existsSync(runLog)) {
|
|
6705
|
+
const lines = readFileSync(runLog, 'utf-8').trim().split('\n').filter(Boolean);
|
|
6706
|
+
for (const l of lines) {
|
|
6707
|
+
try {
|
|
6708
|
+
const entry = JSON.parse(l);
|
|
6709
|
+
if (entry && typeof entry.id === 'string' && entry.id)
|
|
6710
|
+
runIdsToPurge.push(entry.id);
|
|
6711
|
+
}
|
|
6712
|
+
catch { /* skip malformed */ }
|
|
6713
|
+
}
|
|
6714
|
+
}
|
|
6715
|
+
}
|
|
6716
|
+
catch { /* non-fatal */ }
|
|
6679
6717
|
try {
|
|
6680
6718
|
if (existsSync(runLog)) {
|
|
6681
6719
|
unlinkSync(runLog);
|
|
@@ -6698,6 +6736,30 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
6698
6736
|
}
|
|
6699
6737
|
}
|
|
6700
6738
|
catch { /* non-fatal */ }
|
|
6739
|
+
// PRD Phase 4a / 1.18.85: drop the new Event store entries linked
|
|
6740
|
+
// to this job's runs. One file per run UUID.
|
|
6741
|
+
try {
|
|
6742
|
+
if (runIdsToPurge.length > 0) {
|
|
6743
|
+
const eventsDir = path.join(BASE_DIR, 'events');
|
|
6744
|
+
if (existsSync(eventsDir)) {
|
|
6745
|
+
let dropped = 0;
|
|
6746
|
+
for (const rid of runIdsToPurge) {
|
|
6747
|
+
const safeRid = String(rid).replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 128);
|
|
6748
|
+
const evtFile = path.join(eventsDir, `${safeRid}.jsonl`);
|
|
6749
|
+
try {
|
|
6750
|
+
if (existsSync(evtFile)) {
|
|
6751
|
+
unlinkSync(evtFile);
|
|
6752
|
+
dropped++;
|
|
6753
|
+
}
|
|
6754
|
+
}
|
|
6755
|
+
catch { /* skip */ }
|
|
6756
|
+
}
|
|
6757
|
+
if (dropped > 0)
|
|
6758
|
+
purged.push(`${dropped} event log${dropped === 1 ? '' : 's'}`);
|
|
6759
|
+
}
|
|
6760
|
+
}
|
|
6761
|
+
}
|
|
6762
|
+
catch { /* non-fatal */ }
|
|
6701
6763
|
try {
|
|
6702
6764
|
const uploadsDir = path.join(BASE_DIR, 'uploads', `cron-${safe}`);
|
|
6703
6765
|
if (existsSync(uploadsDir)) {
|
|
@@ -23638,7 +23700,7 @@ function renderRecentHistoryList(runs) {
|
|
|
23638
23700
|
+ '</div>'
|
|
23639
23701
|
+ '<div style="font-size:12px;color:var(--text-secondary);line-height:18px">' + esc(startedLabel) + '</div>'
|
|
23640
23702
|
+ '<div style="font-size:12px;color:var(--text-muted);line-height:18px">' + esc(durationLabel) + '</div>'
|
|
23641
|
-
+ '<div style="display:flex;gap:6px;align-items:center"><button class="btn-sm" onclick="event.stopPropagation();
|
|
23703
|
+
+ '<div style="display:flex;gap:6px;align-items:center"><button class="btn-sm" onclick="event.stopPropagation();openRunOrTrace(\\x27' + safeName + '\\x27,' + (entry.id ? '\\x27' + jsStr(entry.id) + '\\x27' : 'null') + ')" style="font-size:11px;padding:3px 8px">' + (entry.id ? 'Open run' : 'Trace') + '</button></div>'
|
|
23642
23704
|
+ '</div>';
|
|
23643
23705
|
}
|
|
23644
23706
|
return '<div class="history-list" style="background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius)">'
|
|
@@ -23844,7 +23906,7 @@ function renderRunListBody(allRuns) {
|
|
|
23844
23906
|
+ '<div style="font-size:11px;color:' + triggerColor + ';line-height:18px">' + esc(triggerLabel) + '</div>'
|
|
23845
23907
|
+ '<div style="font-size:12px;color:var(--text-secondary);line-height:18px">' + esc(startedLabel) + '</div>'
|
|
23846
23908
|
+ '<div style="font-size:12px;color:var(--text-muted);line-height:18px">' + esc(durationLabel) + '</div>'
|
|
23847
|
-
+ '<div style="display:flex;gap:6px;align-items:center"><button class="btn-sm" onclick="event.stopPropagation();
|
|
23909
|
+
+ '<div style="display:flex;gap:6px;align-items:center"><button class="btn-sm" onclick="event.stopPropagation();openRunOrTrace(\\x27' + safeName + '\\x27,' + (entry.id ? '\\x27' + jsStr(entry.id) + '\\x27' : 'null') + ')" style="font-size:11px;padding:3px 8px">' + (entry.id ? 'Open run' : 'Trace') + '</button></div>'
|
|
23848
23910
|
+ '</div>';
|
|
23849
23911
|
}
|
|
23850
23912
|
html += '</div>';
|
|
@@ -24322,6 +24384,162 @@ async function refreshCron() {
|
|
|
24322
24384
|
|
|
24323
24385
|
var traceData = [];
|
|
24324
24386
|
|
|
24387
|
+
// PRD Phase 4b / 1.18.86: smart router. If the run entry has a stable
|
|
24388
|
+
// runId (1.18.85+ runs), open the new Run detail viewer reading from the
|
|
24389
|
+
// Event store; otherwise fall back to the legacy trace viewer (which now
|
|
24390
|
+
// just renders the friendly empty state explaining where to find the
|
|
24391
|
+
// real error). Both viewers share the same modal shell.
|
|
24392
|
+
function openRunOrTrace(jobName, runId) {
|
|
24393
|
+
if (runId && typeof runId === 'string') {
|
|
24394
|
+
return openRunDetail(runId, jobName);
|
|
24395
|
+
}
|
|
24396
|
+
return openTraceViewer(jobName);
|
|
24397
|
+
}
|
|
24398
|
+
|
|
24399
|
+
// PRD Phase 4b / 1.18.86: Run detail viewer. Renders a waterfall of
|
|
24400
|
+
// RunEvent rows from /api/runs/:runId/events. Color-coded by kind, paired
|
|
24401
|
+
// tool_call→tool_result by toolUseId, with expandable per-span content.
|
|
24402
|
+
async function openRunDetail(runId, jobName) {
|
|
24403
|
+
document.getElementById('trace-modal-title').textContent = 'Run detail · ' + (jobName || runId);
|
|
24404
|
+
document.getElementById('trace-run-selector').innerHTML = '';
|
|
24405
|
+
document.getElementById('trace-content').innerHTML = '<div style="padding:20px;color:var(--text-muted)">Loading run events…</div>';
|
|
24406
|
+
document.getElementById('trace-modal').classList.add('show');
|
|
24407
|
+
try {
|
|
24408
|
+
var r = await apiFetch('/api/runs/' + encodeURIComponent(runId) + '/events');
|
|
24409
|
+
var d = await r.json();
|
|
24410
|
+
if (!r.ok || d.ok === false) {
|
|
24411
|
+
document.getElementById('trace-content').innerHTML = '<div style="padding:20px;color:var(--red)">Failed to load run: ' + esc(d.error || 'unknown') + '</div>';
|
|
24412
|
+
return;
|
|
24413
|
+
}
|
|
24414
|
+
var events = (d && d.events) || [];
|
|
24415
|
+
if (events.length === 0) {
|
|
24416
|
+
document.getElementById('trace-content').innerHTML = '<div style="padding:24px;color:var(--text-muted);line-height:1.6"><div style="font-weight:500;color:var(--text-secondary);margin-bottom:8px">No events captured for this run</div><div style="font-size:12px">Either the run pre-dates 1.18.85 (when the Event store was added) or the SDK errored before any message landed.<br/>The Recent history row carries the high-level status, error message, and goal verdict.</div></div>';
|
|
24417
|
+
return;
|
|
24418
|
+
}
|
|
24419
|
+
document.getElementById('trace-content').innerHTML = renderRunDetailWaterfall(events, runId, jobName);
|
|
24420
|
+
} catch (e) {
|
|
24421
|
+
document.getElementById('trace-content').innerHTML = '<div style="padding:20px;color:var(--red)">Failed to load run: ' + esc(String(e)) + '</div>';
|
|
24422
|
+
}
|
|
24423
|
+
}
|
|
24424
|
+
|
|
24425
|
+
// Renders the waterfall. Each event becomes a row with:
|
|
24426
|
+
// color border (by kind) · kind badge · time offset · brief preview · expand link
|
|
24427
|
+
// tool_call rows pair with their tool_result by toolUseId so the duration
|
|
24428
|
+
// is computed and shown alongside the call.
|
|
24429
|
+
function renderRunDetailWaterfall(events, runId, jobName) {
|
|
24430
|
+
if (!events.length) return '';
|
|
24431
|
+
var firstTs = events[0].ts ? new Date(events[0].ts).getTime() : Date.now();
|
|
24432
|
+
var lastTs = events[events.length - 1].ts ? new Date(events[events.length - 1].ts).getTime() : firstTs;
|
|
24433
|
+
var totalMs = Math.max(1, lastTs - firstTs);
|
|
24434
|
+
|
|
24435
|
+
// Pair tool_call with its tool_result for duration.
|
|
24436
|
+
var resultByToolUseId = {};
|
|
24437
|
+
for (var i = 0; i < events.length; i++) {
|
|
24438
|
+
var e = events[i];
|
|
24439
|
+
if (e.kind === 'tool_result' && e.toolUseId) {
|
|
24440
|
+
resultByToolUseId[e.toolUseId] = e;
|
|
24441
|
+
}
|
|
24442
|
+
}
|
|
24443
|
+
|
|
24444
|
+
// Per-event color + label
|
|
24445
|
+
function kindColor(k) {
|
|
24446
|
+
if (k === 'session_start' || k === 'session_end') return 'var(--text-muted)';
|
|
24447
|
+
if (k === 'llm_text') return 'var(--accent)';
|
|
24448
|
+
if (k === 'thinking') return 'var(--purple)';
|
|
24449
|
+
if (k === 'tool_call') return '#22c55e';
|
|
24450
|
+
if (k === 'tool_result') return '#22c55e';
|
|
24451
|
+
if (k === 'subagent_start' || k === 'subagent_stop') return '#a855f7';
|
|
24452
|
+
if (k === 'rate_limit') return 'var(--yellow)';
|
|
24453
|
+
if (k === 'hook') return 'var(--blue)';
|
|
24454
|
+
if (k === 'error') return 'var(--red)';
|
|
24455
|
+
return 'var(--text-muted)';
|
|
24456
|
+
}
|
|
24457
|
+
function kindLabel(k) {
|
|
24458
|
+
return (k || 'event').toUpperCase().replace(/_/g, ' ');
|
|
24459
|
+
}
|
|
24460
|
+
|
|
24461
|
+
// Header strip with summary
|
|
24462
|
+
var startLabel = events[0].ts ? new Date(events[0].ts).toLocaleString() : '—';
|
|
24463
|
+
var endEvent = events.find(function(e) { return e.kind === 'session_end'; });
|
|
24464
|
+
var costStr = endEvent && endEvent.costUsd != null ? '$' + endEvent.costUsd.toFixed(4) : '—';
|
|
24465
|
+
var stopReason = endEvent && endEvent.stopReason ? endEvent.stopReason : '—';
|
|
24466
|
+
var html = '<div style="padding:16px 20px;border-bottom:1px solid var(--border);background:var(--bg-secondary);position:sticky;top:0;z-index:1">'
|
|
24467
|
+
+ '<div style="display:flex;align-items:center;gap:14px;font-size:11px;color:var(--text-muted);flex-wrap:wrap">'
|
|
24468
|
+
+ '<span><strong style="color:var(--text-primary)">' + esc(events.length) + '</strong> events</span>'
|
|
24469
|
+
+ '<span>·</span><span>started ' + esc(startLabel) + '</span>'
|
|
24470
|
+
+ '<span>·</span><span>duration <strong style="color:var(--text-primary)">' + esc(formatDurationMs(totalMs)) + '</strong></span>'
|
|
24471
|
+
+ '<span>·</span><span>cost <strong style="color:var(--text-primary)">' + esc(costStr) + '</strong></span>'
|
|
24472
|
+
+ '<span>·</span><span>stop reason <strong style="color:var(--text-primary)">' + esc(stopReason) + '</strong></span>'
|
|
24473
|
+
+ '<span style="flex:1"></span>'
|
|
24474
|
+
+ '<code style="font-size:10px;color:var(--text-muted)">runId ' + esc(String(runId).slice(0, 12)) + '…</code>'
|
|
24475
|
+
+ '</div>'
|
|
24476
|
+
+ '</div>';
|
|
24477
|
+
|
|
24478
|
+
// Waterfall rows
|
|
24479
|
+
html += '<div style="padding:0">';
|
|
24480
|
+
for (var j = 0; j < events.length; j++) {
|
|
24481
|
+
var ev = events[j];
|
|
24482
|
+
var color = kindColor(ev.kind);
|
|
24483
|
+
var label = kindLabel(ev.kind);
|
|
24484
|
+
var tsMs = ev.ts ? new Date(ev.ts).getTime() : firstTs;
|
|
24485
|
+
var offsetMs = tsMs - firstTs;
|
|
24486
|
+
var offsetLabel = offsetMs === 0 ? '+0ms' : '+' + formatDurationMs(offsetMs);
|
|
24487
|
+
var widthPct = Math.max(2, Math.min(100, (offsetMs / totalMs) * 100));
|
|
24488
|
+
// For tool_call, compute duration to its paired tool_result.
|
|
24489
|
+
var pairedDuration = '';
|
|
24490
|
+
if (ev.kind === 'tool_call' && ev.toolUseId && resultByToolUseId[ev.toolUseId]) {
|
|
24491
|
+
var resultTs = new Date(resultByToolUseId[ev.toolUseId].ts).getTime();
|
|
24492
|
+
pairedDuration = ' · ran ' + formatDurationMs(Math.max(0, resultTs - tsMs));
|
|
24493
|
+
}
|
|
24494
|
+
|
|
24495
|
+
// Brief preview: text for llm_text, thinking for thinking, tool name + first arg for tool_call/result, error for error.
|
|
24496
|
+
var preview = '';
|
|
24497
|
+
var fullContent = '';
|
|
24498
|
+
if (ev.kind === 'llm_text' && ev.text) {
|
|
24499
|
+
preview = String(ev.text).slice(0, 160).replace(/\\s+/g, ' ');
|
|
24500
|
+
fullContent = String(ev.text);
|
|
24501
|
+
} else if (ev.kind === 'thinking' && ev.thinking) {
|
|
24502
|
+
preview = String(ev.thinking).slice(0, 160).replace(/\\s+/g, ' ');
|
|
24503
|
+
fullContent = String(ev.thinking);
|
|
24504
|
+
} else if (ev.kind === 'tool_call') {
|
|
24505
|
+
preview = (ev.toolName || 'tool') + (ev.toolInput ? ' · ' + JSON.stringify(ev.toolInput).slice(0, 120) : '');
|
|
24506
|
+
fullContent = ev.toolInput ? JSON.stringify(ev.toolInput, null, 2) : '';
|
|
24507
|
+
} else if (ev.kind === 'tool_result') {
|
|
24508
|
+
preview = ev.toolError ? '✗ ' + ev.toolError : (typeof ev.toolResult === 'string' ? ev.toolResult.slice(0, 160) : (ev.toolResult ? JSON.stringify(ev.toolResult).slice(0, 160) : ''));
|
|
24509
|
+
fullContent = typeof ev.toolResult === 'string' ? ev.toolResult : JSON.stringify(ev.toolResult, null, 2);
|
|
24510
|
+
} else if (ev.kind === 'error') {
|
|
24511
|
+
preview = ev.toolError || '';
|
|
24512
|
+
fullContent = ev.toolError || '';
|
|
24513
|
+
} else if (ev.kind === 'session_start') {
|
|
24514
|
+
preview = ev.sessionId ? 'session ' + String(ev.sessionId).slice(0, 8) + '…' : '';
|
|
24515
|
+
} else if (ev.kind === 'session_end') {
|
|
24516
|
+
preview = '$' + (ev.costUsd != null ? ev.costUsd.toFixed(4) : '?') + ' · ' + (ev.stopReason || '?');
|
|
24517
|
+
}
|
|
24518
|
+
|
|
24519
|
+
var rowId = 'run-evt-' + j;
|
|
24520
|
+
var canExpand = !!fullContent && fullContent.length > preview.length;
|
|
24521
|
+
html += '<div style="display:grid;grid-template-columns:90px 110px 1fr;gap:14px;padding:10px 20px;border-bottom:1px solid var(--border);align-items:start">';
|
|
24522
|
+
html += '<div style="font-size:10px;color:var(--text-muted);font-family:\\x27JetBrains Mono\\x27,monospace;line-height:18px">' + esc(offsetLabel) + '</div>';
|
|
24523
|
+
html += '<div><span style="display:inline-block;background:' + color + '20;color:' + color + ';padding:2px 8px;border-radius:4px;font-size:10px;font-weight:600;letter-spacing:0.04em">' + esc(label) + '</span></div>';
|
|
24524
|
+
html += '<div style="min-width:0">';
|
|
24525
|
+
html += '<div style="font-size:12px;color:var(--text-primary);line-height:1.45;word-break:break-word">'
|
|
24526
|
+
+ esc(preview)
|
|
24527
|
+
+ (pairedDuration ? '<span style="color:var(--text-muted);font-size:11px"> ' + esc(pairedDuration) + '</span>' : '')
|
|
24528
|
+
+ '</div>';
|
|
24529
|
+
if (canExpand) {
|
|
24530
|
+
html += '<button class="btn-sm" onclick="document.getElementById(\\x27' + rowId + '\\x27).style.display=document.getElementById(\\x27' + rowId + '\\x27).style.display===\\x27none\\x27?\\x27block\\x27:\\x27none\\x27" style="margin-top:6px;font-size:10px;padding:2px 8px">Expand</button>';
|
|
24531
|
+
html += '<pre id="' + rowId + '" style="display:none;margin-top:8px;font-size:11px;font-family:\\x27JetBrains Mono\\x27,monospace;background:var(--bg-secondary);border:1px solid var(--border);padding:10px;border-radius:6px;white-space:pre-wrap;word-break:break-word;max-height:400px;overflow-y:auto">' + esc(fullContent) + '</pre>';
|
|
24532
|
+
}
|
|
24533
|
+
// Show toolUseId hint when present so user can correlate with logs.
|
|
24534
|
+
if (ev.toolUseId) {
|
|
24535
|
+
html += '<div style="font-size:10px;color:var(--text-muted);font-family:\\x27JetBrains Mono\\x27,monospace;margin-top:4px">use_id ' + esc(String(ev.toolUseId).slice(0, 12)) + '…</div>';
|
|
24536
|
+
}
|
|
24537
|
+
html += '</div></div>';
|
|
24538
|
+
}
|
|
24539
|
+
html += '</div>';
|
|
24540
|
+
return html;
|
|
24541
|
+
}
|
|
24542
|
+
|
|
24325
24543
|
async function openTraceViewer(jobName) {
|
|
24326
24544
|
document.getElementById('trace-modal-title').textContent = 'Trace: ' + jobName;
|
|
24327
24545
|
document.getElementById('trace-content').innerHTML = '<div style="padding:20px;color:var(--text-muted)">Loading...</div>';
|
|
@@ -25646,7 +25864,7 @@ function renderCronRunDetails(lr) {
|
|
|
25646
25864
|
if (Array.isArray(lr.mcpServersApplied) && lr.mcpServersApplied.length) {
|
|
25647
25865
|
html += '<div style="font-size:11px;color:var(--text-muted);margin-bottom:6px">MCP servers: ' + esc(lr.mcpServersApplied.join(', ')) + '</div>';
|
|
25648
25866
|
}
|
|
25649
|
-
html += '<div style="margin-top:14px;display:flex;gap:8px"><button class="btn-sm" onclick="
|
|
25867
|
+
html += '<div style="margin-top:14px;display:flex;gap:8px"><button class="btn-sm" onclick="openRunOrTrace(\\x27' + jsStr(lr.jobName || editingCronJob || '') + '\\x27,' + (lr.id ? '\\x27' + jsStr(lr.id) + '\\x27' : 'null') + ')" style="font-size:11px">' + (lr.id ? 'Open run' : 'Open trace') + '</button></div>';
|
|
25650
25868
|
html += '</div>';
|
|
25651
25869
|
return html;
|
|
25652
25870
|
}
|
|
@@ -1208,6 +1208,8 @@ export class CronScheduler {
|
|
|
1208
1208
|
terminalReason,
|
|
1209
1209
|
// 1.18.84: persist the actual trigger source for the Run list filter.
|
|
1210
1210
|
trigger,
|
|
1211
|
+
// 1.18.85: stable UUID linking this run to its Event store entries.
|
|
1212
|
+
...(cronMetadata?.runId ? { id: cronMetadata.runId } : {}),
|
|
1211
1213
|
// Trick capability metadata — surfaced by the dashboard's
|
|
1212
1214
|
// "ran with: …" line. Omit empty arrays to keep the JSONL light.
|
|
1213
1215
|
...(cronMetadata?.skillsApplied?.length ? { skillsApplied: cronMetadata.skillsApplied } : {}),
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-run event log — PRD §6 / Phase 4a.
|
|
3
|
+
*
|
|
4
|
+
* Stores RunEvent rows as JSONL at ~/.clementine/events/<runId>.jsonl,
|
|
5
|
+
* mirroring the existing CronRunLog pattern (auto-prune at 2MB / 2000 lines).
|
|
6
|
+
* One file per run keeps reads cheap (no scanning unrelated runs) and lets
|
|
7
|
+
* the Run detail page tail a single file for live updates.
|
|
8
|
+
*
|
|
9
|
+
* Writers:
|
|
10
|
+
* - In-process tap in runAgent (path A) writes session_start, llm_text,
|
|
11
|
+
* thinking, tool_call, tool_result, session_end during the SDK stream.
|
|
12
|
+
* - Hook side-channel (path B, Phase 4d) writes hook events.
|
|
13
|
+
* - Subagent backfill (path C, Phase 4e) synthesizes tool_call/tool_result
|
|
14
|
+
* for inner SDK calls that don't fire parent-level hooks.
|
|
15
|
+
*
|
|
16
|
+
* Reader: dashboard's Run detail page via /api/runs/:run_id/events.
|
|
17
|
+
*/
|
|
18
|
+
import type { RunEvent } from '../types.js';
|
|
19
|
+
export declare class EventLog {
|
|
20
|
+
private readonly dir;
|
|
21
|
+
private static readonly MAX_BYTES;
|
|
22
|
+
private static readonly MAX_LINES;
|
|
23
|
+
constructor(baseDir?: string);
|
|
24
|
+
private logPath;
|
|
25
|
+
append(event: RunEvent): void;
|
|
26
|
+
/** Read every event for one run, in seq order. */
|
|
27
|
+
readByRun(runId: string): RunEvent[];
|
|
28
|
+
/** Returns true if any events were captured for the run. Cheap existence check. */
|
|
29
|
+
hasEventsForRun(runId: string): boolean;
|
|
30
|
+
/** Drop one run's entire log. Called from cron-delete cascade cleanup. */
|
|
31
|
+
removeRun(runId: string): boolean;
|
|
32
|
+
/** Total disk size of all event logs in bytes. For diagnostics. */
|
|
33
|
+
totalBytes(): number;
|
|
34
|
+
private maybePrune;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=event-log.d.ts.map
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-run event log — PRD §6 / Phase 4a.
|
|
3
|
+
*
|
|
4
|
+
* Stores RunEvent rows as JSONL at ~/.clementine/events/<runId>.jsonl,
|
|
5
|
+
* mirroring the existing CronRunLog pattern (auto-prune at 2MB / 2000 lines).
|
|
6
|
+
* One file per run keeps reads cheap (no scanning unrelated runs) and lets
|
|
7
|
+
* the Run detail page tail a single file for live updates.
|
|
8
|
+
*
|
|
9
|
+
* Writers:
|
|
10
|
+
* - In-process tap in runAgent (path A) writes session_start, llm_text,
|
|
11
|
+
* thinking, tool_call, tool_result, session_end during the SDK stream.
|
|
12
|
+
* - Hook side-channel (path B, Phase 4d) writes hook events.
|
|
13
|
+
* - Subagent backfill (path C, Phase 4e) synthesizes tool_call/tool_result
|
|
14
|
+
* for inner SDK calls that don't fire parent-level hooks.
|
|
15
|
+
*
|
|
16
|
+
* Reader: dashboard's Run detail page via /api/runs/:run_id/events.
|
|
17
|
+
*/
|
|
18
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
import pino from 'pino';
|
|
21
|
+
import { BASE_DIR } from '../config.js';
|
|
22
|
+
const logger = pino({ name: 'clementine.event-log', level: process.env.LOG_LEVEL ?? 'info' });
|
|
23
|
+
export class EventLog {
|
|
24
|
+
dir;
|
|
25
|
+
static MAX_BYTES = 2_000_000;
|
|
26
|
+
static MAX_LINES = 2000;
|
|
27
|
+
constructor(baseDir) {
|
|
28
|
+
this.dir = path.join(baseDir ?? BASE_DIR, 'events');
|
|
29
|
+
if (!existsSync(this.dir)) {
|
|
30
|
+
mkdirSync(this.dir, { recursive: true });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
logPath(runId) {
|
|
34
|
+
// Sanitize runId for filesystem — UUIDs are safe but we defend against
|
|
35
|
+
// accidental non-UUID values flowing in from older callsites.
|
|
36
|
+
const safe = String(runId).replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 128);
|
|
37
|
+
return path.join(this.dir, `${safe}.jsonl`);
|
|
38
|
+
}
|
|
39
|
+
append(event) {
|
|
40
|
+
if (!event.runId) {
|
|
41
|
+
logger.warn({ event: { kind: event.kind, ts: event.ts } }, 'Event missing runId — dropped');
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const filePath = this.logPath(event.runId);
|
|
45
|
+
const line = JSON.stringify(event) + '\n';
|
|
46
|
+
try {
|
|
47
|
+
appendFileSync(filePath, line);
|
|
48
|
+
// Prune asynchronously so the SDK stream loop never blocks on disk IO.
|
|
49
|
+
setImmediate(() => this.maybePrune(filePath));
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
// Never throw to the caller — telemetry must not break runs.
|
|
53
|
+
logger.warn({ err, runId: event.runId }, 'Failed to write event log');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** Read every event for one run, in seq order. */
|
|
57
|
+
readByRun(runId) {
|
|
58
|
+
const filePath = this.logPath(runId);
|
|
59
|
+
if (!existsSync(filePath))
|
|
60
|
+
return [];
|
|
61
|
+
try {
|
|
62
|
+
const lines = readFileSync(filePath, 'utf-8').trim().split('\n').filter(Boolean);
|
|
63
|
+
const events = lines
|
|
64
|
+
.map((l) => {
|
|
65
|
+
try {
|
|
66
|
+
return JSON.parse(l);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
.filter((e) => e !== null);
|
|
73
|
+
// Sort by seq so events with the same ts (sub-millisecond bursts) order
|
|
74
|
+
// deterministically.
|
|
75
|
+
events.sort((a, b) => (a.seq ?? 0) - (b.seq ?? 0));
|
|
76
|
+
return events;
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/** Returns true if any events were captured for the run. Cheap existence check. */
|
|
83
|
+
hasEventsForRun(runId) {
|
|
84
|
+
return existsSync(this.logPath(runId));
|
|
85
|
+
}
|
|
86
|
+
/** Drop one run's entire log. Called from cron-delete cascade cleanup. */
|
|
87
|
+
removeRun(runId) {
|
|
88
|
+
const filePath = this.logPath(runId);
|
|
89
|
+
if (!existsSync(filePath))
|
|
90
|
+
return false;
|
|
91
|
+
try {
|
|
92
|
+
writeFileSync(filePath, ''); // truncate, then unlink — symmetric with cron-runs delete pattern
|
|
93
|
+
const fs = require('node:fs');
|
|
94
|
+
fs.unlinkSync(filePath);
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
logger.warn({ err, runId }, 'Failed to remove event log');
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/** Total disk size of all event logs in bytes. For diagnostics. */
|
|
103
|
+
totalBytes() {
|
|
104
|
+
if (!existsSync(this.dir))
|
|
105
|
+
return 0;
|
|
106
|
+
let total = 0;
|
|
107
|
+
try {
|
|
108
|
+
for (const f of readdirSync(this.dir)) {
|
|
109
|
+
if (!f.endsWith('.jsonl'))
|
|
110
|
+
continue;
|
|
111
|
+
try {
|
|
112
|
+
total += statSync(path.join(this.dir, f)).size;
|
|
113
|
+
}
|
|
114
|
+
catch { /* skip */ }
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch { /* skip */ }
|
|
118
|
+
return total;
|
|
119
|
+
}
|
|
120
|
+
maybePrune(filePath) {
|
|
121
|
+
try {
|
|
122
|
+
const { size } = statSync(filePath);
|
|
123
|
+
if (size <= EventLog.MAX_BYTES)
|
|
124
|
+
return;
|
|
125
|
+
const lines = readFileSync(filePath, 'utf-8').trim().split('\n');
|
|
126
|
+
if (lines.length <= EventLog.MAX_LINES)
|
|
127
|
+
return;
|
|
128
|
+
const trimmed = lines.slice(-EventLog.MAX_LINES);
|
|
129
|
+
writeFileSync(filePath, trimmed.join('\n') + '\n');
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// non-critical
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=event-log.js.map
|
package/dist/gateway/router.d.ts
CHANGED
|
@@ -237,6 +237,8 @@ export declare class Gateway {
|
|
|
237
237
|
skillsMissing: string[];
|
|
238
238
|
allowedToolsApplied?: string[];
|
|
239
239
|
mcpServersApplied: string[];
|
|
240
|
+
/** PRD §6 / 1.18.85: run UUID from runAgent. */
|
|
241
|
+
runId?: string;
|
|
240
242
|
} | undefined;
|
|
241
243
|
requestApproval(descriptionOrId: string, explicitId?: string): Promise<boolean | string>;
|
|
242
244
|
resolveApproval(requestId: string, result: boolean | string): void;
|
package/dist/gateway/router.js
CHANGED
|
@@ -2023,6 +2023,7 @@ export class Gateway {
|
|
|
2023
2023
|
skillsMissing: cronResult.skillsMissing,
|
|
2024
2024
|
allowedToolsApplied: cronResult.allowedToolsApplied,
|
|
2025
2025
|
mcpServersApplied: cronResult.mcpServersApplied,
|
|
2026
|
+
runId: cronResult.runId,
|
|
2026
2027
|
};
|
|
2027
2028
|
logger.info({
|
|
2028
2029
|
jobName,
|
package/dist/types.d.ts
CHANGED
|
@@ -404,7 +404,55 @@ export interface LongTaskPreflightSnapshot {
|
|
|
404
404
|
reasons: string[];
|
|
405
405
|
}
|
|
406
406
|
export type TerminalReason = 'blocking_limit' | 'rapid_refill_breaker' | 'prompt_too_long' | 'image_error' | 'model_error' | 'aborted_streaming' | 'aborted_tools' | 'stop_hook_prevented' | 'hook_stopped' | 'tool_deferred' | 'max_turns' | 'completed';
|
|
407
|
+
/**
|
|
408
|
+
* PRD §6 Event entity — one row per significant SDK message during a Run.
|
|
409
|
+
* Stored as JSONL at ~/.clementine/events/<run_id>.jsonl. Powers the new
|
|
410
|
+
* Run detail waterfall (Phase 4b) and the metrics dashboards (Phase 6).
|
|
411
|
+
*
|
|
412
|
+
* Designed to fit the SDK's typed message stream + the 12 hook events. Most
|
|
413
|
+
* fields are optional because each event kind populates a different subset.
|
|
414
|
+
*/
|
|
415
|
+
export interface RunEvent {
|
|
416
|
+
/** Run this event belongs to — links back to CronRunEntry.id. */
|
|
417
|
+
runId: string;
|
|
418
|
+
/** SDK session id once known (system/init lands first; everything else carries it). */
|
|
419
|
+
sessionId?: string;
|
|
420
|
+
/** Monotonic sequence within the run. Used to order events that share a ts. */
|
|
421
|
+
seq: number;
|
|
422
|
+
/** ISO timestamp when the event was captured. */
|
|
423
|
+
ts: string;
|
|
424
|
+
/** Event kind — semantic grouping for the dashboard's span types. */
|
|
425
|
+
kind: 'session_start' | 'session_end' | 'llm_text' | 'thinking' | 'tool_call' | 'tool_result' | 'subagent_start' | 'subagent_stop' | 'rate_limit' | 'hook' | 'error';
|
|
426
|
+
/** Hook event name when kind='hook' (PreToolUse / PostToolUse / etc.). */
|
|
427
|
+
hookEventName?: string;
|
|
428
|
+
/** Tool name when kind='tool_call' or 'tool_result'. Includes mcp__ prefix. */
|
|
429
|
+
toolName?: string;
|
|
430
|
+
/** SDK-assigned tool_use id — pairs tool_call with its tool_result. */
|
|
431
|
+
toolUseId?: string;
|
|
432
|
+
/** For nested tool calls (parallel sub-spawning). */
|
|
433
|
+
parentToolUseId?: string;
|
|
434
|
+
/** Tool input as JSON, truncated at 8KB. */
|
|
435
|
+
toolInput?: unknown;
|
|
436
|
+
/** Tool result as JSON or string, truncated at 16KB. */
|
|
437
|
+
toolResult?: unknown;
|
|
438
|
+
/** Tool error message when result.is_error or PostToolUseFailure. */
|
|
439
|
+
toolError?: string;
|
|
440
|
+
/** Assistant text block content when kind='llm_text'. */
|
|
441
|
+
text?: string;
|
|
442
|
+
/** ThinkingBlock content when kind='thinking'. */
|
|
443
|
+
thinking?: string;
|
|
444
|
+
/** Cost so far for this run when kind='session_end'. */
|
|
445
|
+
costUsd?: number;
|
|
446
|
+
/** Stop reason from ResultMessage when kind='session_end'. */
|
|
447
|
+
stopReason?: string;
|
|
448
|
+
/** Subagent id when kind='subagent_*'. */
|
|
449
|
+
agentId?: string;
|
|
450
|
+
}
|
|
407
451
|
export interface CronRunEntry {
|
|
452
|
+
/** PRD §6 / 1.18.85: stable run UUID. Optional only because pre-1.18.85
|
|
453
|
+
* entries don't have it; new entries always do. The Event store keys
|
|
454
|
+
* off this id. */
|
|
455
|
+
id?: string;
|
|
408
456
|
jobName: string;
|
|
409
457
|
startedAt: string;
|
|
410
458
|
/** Optional: in-progress runs are appended with status='running' before the
|