clementine-agent 1.18.83 → 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.
- package/dist/agent/assistant.d.ts +0 -2
- package/dist/agent/assistant.js +23 -9
- package/dist/agent/run-agent.d.ts +19 -0
- package/dist/agent/run-agent.js +170 -3
- package/dist/cli/cron.js +13 -0
- package/dist/cli/dashboard.js +79 -8
- package/dist/gateway/cron-scheduler.js +7 -2
- 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 +53 -0
- package/package.json +1 -1
|
@@ -70,8 +70,6 @@ export declare class PersonalAssistant {
|
|
|
70
70
|
private memoryStore;
|
|
71
71
|
private _lastUserMessage?;
|
|
72
72
|
onSkillProposed: ((skill: import('../types.js').SkillDocument) => void) | null;
|
|
73
|
-
private _lastMcpStatus;
|
|
74
|
-
private _lastMcpStatusTime;
|
|
75
73
|
/** Terminal reason from the last SDK query — consumed by cron scheduler for precise error classification. */
|
|
76
74
|
private _lastTerminalReason?;
|
|
77
75
|
/** Per-session stall nudge — set after a query shows stall signals, consumed on the next query. */
|
package/dist/agent/assistant.js
CHANGED
|
@@ -17,6 +17,7 @@ import { BASE_DIR, PKG_DIR, VAULT_DIR, DAILY_NOTES_DIR, SOUL_FILE, AGENTS_FILE,
|
|
|
17
17
|
import { summarizeIntegrationStatus } from '../config/integrations-registry.js';
|
|
18
18
|
import { loadToolPreferences, computeAvailability, buildPromptInstruction, buildComposioStatusBlock, KNOWN_SERVICES, } from '../integrations/tool-preferences.js';
|
|
19
19
|
import { loadClaudeIntegrations } from './mcp-bridge.js';
|
|
20
|
+
import { getLatestMcpStatusSnapshot, recordMcpStatusFromSystemInit, invalidateMcpStatusEntry } from './run-agent.js';
|
|
20
21
|
import { detectFrustrationSignals, detectRepeatedTopics } from './insight-engine.js';
|
|
21
22
|
import { DEFAULT_CHANNEL_CAPABILITIES } from '../types.js';
|
|
22
23
|
import { enforceToolPermissions, getSecurityPrompt, getHeartbeatSecurityPrompt, getCronSecurityPrompt, getHeartbeatDisallowedTools, logToolUse, logAuditJsonl, } from './hooks.js';
|
|
@@ -710,8 +711,10 @@ export class PersonalAssistant {
|
|
|
710
711
|
memoryStore = null; // Typed as any — MemoryStore may not be available yet
|
|
711
712
|
_lastUserMessage;
|
|
712
713
|
onSkillProposed = null;
|
|
713
|
-
|
|
714
|
-
|
|
714
|
+
// PRD Phase 2 / 1.18.84: superseded by the shared module-level cache in
|
|
715
|
+
// run-agent.ts (getLatestMcpStatusSnapshot / recordMcpStatusFromSystemInit).
|
|
716
|
+
// The fields below were declared but never written pre-1.18.84; the
|
|
717
|
+
// module cache populates from every system/init message instead.
|
|
715
718
|
/** Terminal reason from the last SDK query — consumed by cron scheduler for precise error classification. */
|
|
716
719
|
_lastTerminalReason;
|
|
717
720
|
/** Per-session stall nudge — set after a query shows stall signals, consumed on the next query. */
|
|
@@ -808,7 +811,12 @@ export class PersonalAssistant {
|
|
|
808
811
|
this.onSkillProposed = cb;
|
|
809
812
|
}
|
|
810
813
|
getMcpStatus() {
|
|
811
|
-
|
|
814
|
+
// 1.18.84 correctness fix: delegate to the shared module-level cache.
|
|
815
|
+
// Pre-1.18.84 we returned this._lastMcpStatus, which was declared but
|
|
816
|
+
// never written — getMcpStatus() always returned empty. Now run-agent
|
|
817
|
+
// and assistant query streams record into a shared snapshot via
|
|
818
|
+
// recordMcpStatusFromSystemInit when the SDK init message lands.
|
|
819
|
+
return getLatestMcpStatusSnapshot();
|
|
812
820
|
}
|
|
813
821
|
/**
|
|
814
822
|
* PRD Phase 2.1: clear the cached status for one server so the next query
|
|
@@ -818,12 +826,9 @@ export class PersonalAssistant {
|
|
|
818
826
|
* stale error/auth state. Returns the post-clear cached snapshot.
|
|
819
827
|
*/
|
|
820
828
|
invalidateMcpStatus(serverName) {
|
|
821
|
-
const
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
if (cleared)
|
|
825
|
-
this._lastMcpStatusTime = new Date().toISOString();
|
|
826
|
-
return { servers: this._lastMcpStatus, updatedAt: this._lastMcpStatusTime, cleared };
|
|
829
|
+
const result = invalidateMcpStatusEntry(serverName);
|
|
830
|
+
const snapshot = getLatestMcpStatusSnapshot();
|
|
831
|
+
return { servers: snapshot.servers, updatedAt: snapshot.updatedAt, cleared: result.cleared };
|
|
827
832
|
}
|
|
828
833
|
/** Inject a background work result into the session as silent follow-up context. */
|
|
829
834
|
injectPendingContext(sessionKey, userPrompt, result) {
|
|
@@ -2999,6 +3004,15 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
2999
3004
|
const trace = [];
|
|
3000
3005
|
const stream = query({ prompt, options: sdkOptions });
|
|
3001
3006
|
for await (const message of stream) {
|
|
3007
|
+
// 1.18.84 correctness: capture MCP server status from SDK init.
|
|
3008
|
+
if (message.type === 'system' && message.subtype === 'init') {
|
|
3009
|
+
const mcpServersRaw = message.mcp_servers;
|
|
3010
|
+
if (mcpServersRaw)
|
|
3011
|
+
try {
|
|
3012
|
+
recordMcpStatusFromSystemInit(mcpServersRaw);
|
|
3013
|
+
}
|
|
3014
|
+
catch { /* non-fatal */ }
|
|
3015
|
+
}
|
|
3002
3016
|
if (message.type === 'assistant') {
|
|
3003
3017
|
const blocks = getContentBlocks(message);
|
|
3004
3018
|
for (const block of blocks) {
|
|
@@ -22,6 +22,21 @@
|
|
|
22
22
|
* long-task preflight, NO mode=unleashed wrapper.
|
|
23
23
|
*/
|
|
24
24
|
import { type AgentDefinition } from '@anthropic-ai/claude-agent-sdk';
|
|
25
|
+
/** Read the latest MCP status snapshot. Safe to call from any module. */
|
|
26
|
+
export declare function getLatestMcpStatusSnapshot(): {
|
|
27
|
+
servers: Array<{
|
|
28
|
+
name: string;
|
|
29
|
+
status: string;
|
|
30
|
+
}>;
|
|
31
|
+
updatedAt: string;
|
|
32
|
+
};
|
|
33
|
+
/** Write a fresh snapshot. Called from system/init handlers. */
|
|
34
|
+
export declare function recordMcpStatusFromSystemInit(rawMcpServers: unknown): void;
|
|
35
|
+
/** Drop one server from the cache so the next query repopulates it. */
|
|
36
|
+
export declare function invalidateMcpStatusEntry(name: string): {
|
|
37
|
+
cleared: boolean;
|
|
38
|
+
updatedAt: string;
|
|
39
|
+
};
|
|
25
40
|
import type { AgentProfile } from '../types.js';
|
|
26
41
|
import type { AgentManager } from './agent-manager.js';
|
|
27
42
|
import type { MemoryStore } from '../memory/store.js';
|
|
@@ -102,6 +117,10 @@ export interface RunAgentResult {
|
|
|
102
117
|
cache_read_input_tokens?: number;
|
|
103
118
|
cache_creation_input_tokens?: number;
|
|
104
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;
|
|
105
124
|
}
|
|
106
125
|
/**
|
|
107
126
|
* Run a single agent invocation via the canonical SDK pattern.
|
package/dist/agent/run-agent.js
CHANGED
|
@@ -22,8 +22,74 @@
|
|
|
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';
|
|
29
|
+
/**
|
|
30
|
+
* Module-level cache of MCP server statuses from the most recent SDK
|
|
31
|
+
* init message. Populated by every runAgent / PersonalAssistant query
|
|
32
|
+
* stream that captures `system/init`. Read by Assistant.getMcpStatus()
|
|
33
|
+
* and the dashboard's Tools & MCP catalog page.
|
|
34
|
+
*
|
|
35
|
+
* Pre-1.18.84 the assistant declared a private _lastMcpStatus but no
|
|
36
|
+
* code wrote to it — getMcpStatus() always returned empty, making the
|
|
37
|
+
* catalog status pills misleading. The shared module cache fixes that
|
|
38
|
+
* without coupling assistant.ts to runAgent's stream loop.
|
|
39
|
+
*/
|
|
40
|
+
let _lastMcpStatusSnapshot = {
|
|
41
|
+
servers: [],
|
|
42
|
+
updatedAt: '',
|
|
43
|
+
};
|
|
44
|
+
/** Read the latest MCP status snapshot. Safe to call from any module. */
|
|
45
|
+
export function getLatestMcpStatusSnapshot() {
|
|
46
|
+
return { servers: [..._lastMcpStatusSnapshot.servers], updatedAt: _lastMcpStatusSnapshot.updatedAt };
|
|
47
|
+
}
|
|
48
|
+
/** Write a fresh snapshot. Called from system/init handlers. */
|
|
49
|
+
export function recordMcpStatusFromSystemInit(rawMcpServers) {
|
|
50
|
+
if (!Array.isArray(rawMcpServers))
|
|
51
|
+
return;
|
|
52
|
+
const servers = [];
|
|
53
|
+
for (const entry of rawMcpServers) {
|
|
54
|
+
if (!entry || typeof entry !== 'object')
|
|
55
|
+
continue;
|
|
56
|
+
const e = entry;
|
|
57
|
+
if (typeof e.name !== 'string' || !e.name)
|
|
58
|
+
continue;
|
|
59
|
+
servers.push({ name: e.name, status: typeof e.status === 'string' ? e.status : 'unknown' });
|
|
60
|
+
}
|
|
61
|
+
_lastMcpStatusSnapshot = { servers, updatedAt: new Date().toISOString() };
|
|
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
|
+
}
|
|
84
|
+
/** Drop one server from the cache so the next query repopulates it. */
|
|
85
|
+
export function invalidateMcpStatusEntry(name) {
|
|
86
|
+
const before = _lastMcpStatusSnapshot.servers.length;
|
|
87
|
+
_lastMcpStatusSnapshot = {
|
|
88
|
+
servers: _lastMcpStatusSnapshot.servers.filter((s) => s.name !== name),
|
|
89
|
+
updatedAt: new Date().toISOString(),
|
|
90
|
+
};
|
|
91
|
+
return { cleared: _lastMcpStatusSnapshot.servers.length < before, updatedAt: _lastMcpStatusSnapshot.updatedAt };
|
|
92
|
+
}
|
|
27
93
|
import { BASE_DIR, PKG_DIR, CLAUDE_CODE_OAUTH_TOKEN, ANTHROPIC_API_KEY as CONFIG_ANTHROPIC_API_KEY, normalizeClaudeSdkOptionsForOneMillionContext, } from '../config.js';
|
|
28
94
|
import { buildAgentMap } from './agent-definitions.js';
|
|
29
95
|
const MCP_SERVER_SCRIPT = path.join(PKG_DIR, 'dist', 'tools', 'mcp-server.js');
|
|
@@ -218,6 +284,23 @@ export async function runAgent(prompt, opts) {
|
|
|
218
284
|
agentCount: Object.keys(agents).length,
|
|
219
285
|
allowedToolCount: allowedTools.length,
|
|
220
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
|
+
};
|
|
221
304
|
let finalText = '';
|
|
222
305
|
let sessionId = '';
|
|
223
306
|
let totalCostUsd = 0;
|
|
@@ -229,15 +312,38 @@ export async function runAgent(prompt, opts) {
|
|
|
229
312
|
for await (const message of stream) {
|
|
230
313
|
if (message.type === 'system' && message.subtype === 'init') {
|
|
231
314
|
sessionId = message.session_id ?? '';
|
|
232
|
-
|
|
315
|
+
// PRD Phase 2 / 1.18.84 correctness: capture the SDK-reported MCP
|
|
316
|
+
// server status so getMcpStatus() (and the dashboard's Tools & MCP
|
|
317
|
+
// catalog) actually has data. The init message includes
|
|
318
|
+
// mcp_servers: Array<{ name, status }> per the SDK protocol.
|
|
319
|
+
const mcpServersRaw = message.mcp_servers;
|
|
320
|
+
if (mcpServersRaw) {
|
|
321
|
+
try {
|
|
322
|
+
recordMcpStatusFromSystemInit(mcpServersRaw);
|
|
323
|
+
}
|
|
324
|
+
catch { /* non-fatal */ }
|
|
325
|
+
}
|
|
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');
|
|
233
329
|
continue;
|
|
234
330
|
}
|
|
235
331
|
if (message.type === 'assistant') {
|
|
236
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.
|
|
237
335
|
const blocks = (am.message?.content ?? []);
|
|
238
336
|
for (const block of blocks) {
|
|
239
337
|
if (block.type === 'text' && typeof block.text === 'string') {
|
|
240
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
|
+
});
|
|
241
347
|
if (opts.onText) {
|
|
242
348
|
try {
|
|
243
349
|
await opts.onText(block.text);
|
|
@@ -245,7 +351,29 @@ export async function runAgent(prompt, opts) {
|
|
|
245
351
|
catch { /* streaming is best-effort */ }
|
|
246
352
|
}
|
|
247
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
|
+
}
|
|
248
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
|
+
});
|
|
249
377
|
if (opts.onToolActivity) {
|
|
250
378
|
try {
|
|
251
379
|
await opts.onToolActivity({ tool: block.name, input: block.input ?? {} });
|
|
@@ -256,6 +384,26 @@ export async function runAgent(prompt, opts) {
|
|
|
256
384
|
}
|
|
257
385
|
continue;
|
|
258
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
|
+
}
|
|
259
407
|
if (message.type === 'result') {
|
|
260
408
|
const result = message;
|
|
261
409
|
sessionId = sessionId || (result.session_id ?? '');
|
|
@@ -271,6 +419,15 @@ export async function runAgent(prompt, opts) {
|
|
|
271
419
|
if (r)
|
|
272
420
|
finalText = r;
|
|
273
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
|
+
});
|
|
274
431
|
// Mirror cost to usage_log. Same shape as the existing
|
|
275
432
|
// logQueryResult, but standalone so we don't depend on
|
|
276
433
|
// PersonalAssistant's instance state.
|
|
@@ -299,13 +456,22 @@ export async function runAgent(prompt, opts) {
|
|
|
299
456
|
}
|
|
300
457
|
}
|
|
301
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
|
+
});
|
|
302
469
|
// Translate the SDK's budget-exhaustion throw into a message that
|
|
303
470
|
// tells the user (a) what cap tripped and (b) how to raise it.
|
|
304
471
|
// The raw SDK string ("Claude Code returned an error result:
|
|
305
472
|
// Reached maximum budget ($0.5)") leaks through the channel layer
|
|
306
473
|
// as a generic "Something went wrong:" with no actionable hint.
|
|
307
|
-
|
|
308
|
-
if (/Reached maximum budget|error_max_budget_usd/i.test(msg)) {
|
|
474
|
+
if (/Reached maximum budget|error_max_budget_usd/i.test(errMsg)) {
|
|
309
475
|
const cap = maxBudgetUsd?.toFixed(2) ?? '?';
|
|
310
476
|
const envKey = `BUDGET_${source.toUpperCase().replace(/-/g, '_')}_USD`;
|
|
311
477
|
throw new Error(`Hit the $${cap} ${source} budget cap before finishing. ` +
|
|
@@ -330,6 +496,7 @@ export async function runAgent(prompt, opts) {
|
|
|
330
496
|
sessionId,
|
|
331
497
|
subtype,
|
|
332
498
|
...(usage ? { usage } : {}),
|
|
499
|
+
runId,
|
|
333
500
|
};
|
|
334
501
|
}
|
|
335
502
|
//# sourceMappingURL=run-agent.js.map
|
package/dist/cli/cron.js
CHANGED
|
@@ -140,6 +140,15 @@ export async function cmdCronRun(jobName) {
|
|
|
140
140
|
try {
|
|
141
141
|
const response = await gateway.handleCronJob(job.name, job.prompt, job.tier, job.maxTurns, job.model, job.workDir, job.mode, job.maxHours);
|
|
142
142
|
const finishedAt = new Date();
|
|
143
|
+
// 1.18.84: trigger source comes from the CRON_RUN_TRIGGER env var set
|
|
144
|
+
// by the dashboard's manual-run endpoint. Defaults to 'scheduled' for
|
|
145
|
+
// any other invocation path (CLI, daemon-internal callsites).
|
|
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;
|
|
143
152
|
const entry = {
|
|
144
153
|
jobName: job.name,
|
|
145
154
|
startedAt: startedAt.toISOString(),
|
|
@@ -148,6 +157,8 @@ export async function cmdCronRun(jobName) {
|
|
|
148
157
|
durationMs: finishedAt.getTime() - startedAt.getTime(),
|
|
149
158
|
attempt: 1,
|
|
150
159
|
outputPreview: response ? response.slice(0, 200) : undefined,
|
|
160
|
+
trigger,
|
|
161
|
+
...(runIdFromGateway ? { id: runIdFromGateway } : {}),
|
|
151
162
|
};
|
|
152
163
|
// PRD Phase 1.1: goal-orientation evaluator (mirrors the daemon path).
|
|
153
164
|
if (job.successSchema || (job.successCriteriaText && job.successCriteriaText.trim())) {
|
|
@@ -170,6 +181,7 @@ export async function cmdCronRun(jobName) {
|
|
|
170
181
|
}
|
|
171
182
|
catch (err) {
|
|
172
183
|
const finishedAt = new Date();
|
|
184
|
+
const trigger = process.env.CRON_RUN_TRIGGER || 'scheduled';
|
|
173
185
|
runLog.append({
|
|
174
186
|
jobName: job.name,
|
|
175
187
|
startedAt: startedAt.toISOString(),
|
|
@@ -179,6 +191,7 @@ export async function cmdCronRun(jobName) {
|
|
|
179
191
|
error: String(err).slice(0, 500),
|
|
180
192
|
errorType: classifyError(err),
|
|
181
193
|
attempt: 1,
|
|
194
|
+
trigger,
|
|
182
195
|
});
|
|
183
196
|
console.error(`Error: ${err}`);
|
|
184
197
|
process.exit(1);
|
package/dist/cli/dashboard.js
CHANGED
|
@@ -4613,7 +4613,8 @@ export async function cmdDashboard(opts) {
|
|
|
4613
4613
|
detached: true,
|
|
4614
4614
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
4615
4615
|
cwd: BASE_DIR,
|
|
4616
|
-
|
|
4616
|
+
// 1.18.84: pass the trigger source so cron.ts stamps it on the run entry.
|
|
4617
|
+
env: { ...process.env, CLEMENTINE_HOME: BASE_DIR, CRON_RUN_TRIGGER: 'manual' },
|
|
4617
4618
|
});
|
|
4618
4619
|
// Capture stderr for error reporting
|
|
4619
4620
|
let stderr = '';
|
|
@@ -5669,6 +5670,26 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
5669
5670
|
res.status(500).json({ error: String(err) });
|
|
5670
5671
|
}
|
|
5671
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
|
+
});
|
|
5672
5693
|
// ── Recent runs across ALL cron jobs ───────────────────────────
|
|
5673
5694
|
// Powers the "Recent History" zone on the Tasks page. Returns the most
|
|
5674
5695
|
// recent N CronRunEntry rows merged from every per-job .jsonl, sorted
|
|
@@ -6675,6 +6696,24 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
6675
6696
|
if (purge) {
|
|
6676
6697
|
const safe = bareJobName.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
6677
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 */ }
|
|
6678
6717
|
try {
|
|
6679
6718
|
if (existsSync(runLog)) {
|
|
6680
6719
|
unlinkSync(runLog);
|
|
@@ -6697,6 +6736,30 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
6697
6736
|
}
|
|
6698
6737
|
}
|
|
6699
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 */ }
|
|
6700
6763
|
try {
|
|
6701
6764
|
const uploadsDir = path.join(BASE_DIR, 'uploads', `cron-${safe}`);
|
|
6702
6765
|
if (existsSync(uploadsDir)) {
|
|
@@ -23811,12 +23874,13 @@ function renderRunListBody(allRuns) {
|
|
|
23811
23874
|
var startedAt = entry.startedAt ? new Date(entry.startedAt) : null;
|
|
23812
23875
|
var startedLabel = startedAt ? startedAt.toLocaleString() : '—';
|
|
23813
23876
|
var durationLabel = entry.durationMs != null ? formatDurationMs(entry.durationMs) : '—';
|
|
23814
|
-
//
|
|
23815
|
-
//
|
|
23816
|
-
|
|
23817
|
-
|
|
23818
|
-
|
|
23819
|
-
|
|
23877
|
+
// 1.18.84: real persisted trigger field. Falls back to a heuristic for
|
|
23878
|
+
// pre-1.18.84 run entries that don't have the field set.
|
|
23879
|
+
var triggerLabel = entry.trigger || (entry.attempt > 1 ? 'retry' : 'scheduled');
|
|
23880
|
+
var triggerColor = entry.trigger === 'manual' ? 'var(--accent)'
|
|
23881
|
+
: entry.trigger === 'after' ? 'var(--purple)'
|
|
23882
|
+
: entry.trigger === 'discord' ? 'var(--blue)'
|
|
23883
|
+
: 'var(--text-muted)';
|
|
23820
23884
|
// Goal cell
|
|
23821
23885
|
var goalCell = '<div></div>';
|
|
23822
23886
|
if (entry.goalCheck) {
|
|
@@ -23932,7 +23996,14 @@ async function refreshToolsMcpCatalog() {
|
|
|
23932
23996
|
try {
|
|
23933
23997
|
var sR = await apiFetch('/api/mcp-status');
|
|
23934
23998
|
var statusJson = await sR.json();
|
|
23935
|
-
|
|
23999
|
+
// /api/mcp-status returns { servers: [{name, status}], updatedAt }.
|
|
24000
|
+
// Build a name → entry lookup so renderMcpCatalogCard can probe by name.
|
|
24001
|
+
if (statusJson && Array.isArray(statusJson.servers)) {
|
|
24002
|
+
for (var si = 0; si < statusJson.servers.length; si++) {
|
|
24003
|
+
var entry = statusJson.servers[si];
|
|
24004
|
+
if (entry && entry.name) statusMap[entry.name] = entry;
|
|
24005
|
+
}
|
|
24006
|
+
}
|
|
23936
24007
|
} catch (e) { /* status is optional — servers still render without it */ }
|
|
23937
24008
|
try {
|
|
23938
24009
|
var lR = await apiFetch('/api/mcp-servers');
|
|
@@ -888,7 +888,7 @@ export class CronScheduler {
|
|
|
888
888
|
ctx.agentSlug = wf.agentSlug;
|
|
889
889
|
return ctx;
|
|
890
890
|
}
|
|
891
|
-
async runJob(job) {
|
|
891
|
+
async runJob(job, trigger = 'scheduled') {
|
|
892
892
|
const creditBlock = getBackgroundCreditBlock();
|
|
893
893
|
if (creditBlock) {
|
|
894
894
|
logger.warn({ job: job.name, until: creditBlock.until }, 'Cron job skipped — Claude credit block active');
|
|
@@ -1206,6 +1206,10 @@ export class CronScheduler {
|
|
|
1206
1206
|
outputPreview: response ? response.slice(0, 200) : undefined,
|
|
1207
1207
|
advisorApplied,
|
|
1208
1208
|
terminalReason,
|
|
1209
|
+
// 1.18.84: persist the actual trigger source for the Run list filter.
|
|
1210
|
+
trigger,
|
|
1211
|
+
// 1.18.85: stable UUID linking this run to its Event store entries.
|
|
1212
|
+
...(cronMetadata?.runId ? { id: cronMetadata.runId } : {}),
|
|
1209
1213
|
// Trick capability metadata — surfaced by the dashboard's
|
|
1210
1214
|
// "ran with: …" line. Omit empty arrays to keep the JSONL light.
|
|
1211
1215
|
...(cronMetadata?.skillsApplied?.length ? { skillsApplied: cronMetadata.skillsApplied } : {}),
|
|
@@ -1268,7 +1272,8 @@ export class CronScheduler {
|
|
|
1268
1272
|
const dependents = this.jobs.filter(j => j.after === job.name && j.enabled && !this.disabledJobs.has(j.name));
|
|
1269
1273
|
for (const dep of dependents) {
|
|
1270
1274
|
logger.info(`Chain: '${job.name}' succeeded — triggering '${dep.name}'`);
|
|
1271
|
-
|
|
1275
|
+
// 1.18.84: chained-after triggers carry trigger='after' for the Run list filter.
|
|
1276
|
+
this.runJob(dep, 'after').catch((err) => {
|
|
1272
1277
|
logger.error({ err, job: dep.name }, `Chained job '${dep.name}' failed`);
|
|
1273
1278
|
});
|
|
1274
1279
|
}
|
|
@@ -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
|
|
@@ -447,6 +495,11 @@ export interface CronRunEntry {
|
|
|
447
495
|
allowedToolsApplied?: string[];
|
|
448
496
|
/** MCP servers live for this run (post profile + trick allowlist intersection). */
|
|
449
497
|
mcpServersApplied?: string[];
|
|
498
|
+
/** PRD §6 / 1.18.84: how this run was triggered. Persisted by the
|
|
499
|
+
* scheduler (cron tick / chained 'after' / manual-run endpoint /
|
|
500
|
+
* Discord) so the Run list can filter by source instead of guessing
|
|
501
|
+
* via heuristics on attempt count. */
|
|
502
|
+
trigger?: 'manual' | 'scheduled' | 'webhook' | 'api' | 'fork' | 'resume' | 'discord' | 'after';
|
|
450
503
|
/** PRD Phase 1: did the run accomplish what it was supposed to?
|
|
451
504
|
* Computed at run-end when the Task has successSchema or successCriteriaText.
|
|
452
505
|
* - status='pass' both configured checks passed (or the only one configured did)
|