clementine-agent 1.18.84 → 1.18.85

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.
@@ -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.
@@ -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
- logger.debug({ sessionKey: opts.sessionKey, sdkSessionId: sessionId }, 'runAgent: SDK session initialized');
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
- const msg = String(err?.message ?? err);
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())) {
@@ -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)) {
@@ -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
@@ -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;
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.18.84",
3
+ "version": "1.18.85",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",