@exaudeus/workrail 3.80.0 → 3.81.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/worktrain-diagnose.d.ts +114 -0
- package/dist/cli/commands/worktrain-diagnose.js +628 -0
- package/dist/cli-worktrain.js +82 -4
- package/dist/console-ui/assets/{index-2NrQPYdF.js → index-BgMh_c_3.js} +1 -1
- package/dist/console-ui/index.html +1 -1
- package/dist/manifest.json +13 -5
- package/docs/ideas/backlog.md +20 -0
- package/package.json +1 -1
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.parseDaemonEvents = parseDaemonEvents;
|
|
7
|
+
exports.formatDiagnosticCard = formatDiagnosticCard;
|
|
8
|
+
exports.formatDiagnosticJson = formatDiagnosticJson;
|
|
9
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
10
|
+
function parseDaemonEvents(sessionIdQuery, eventsDir, daysBack, readFile) {
|
|
11
|
+
const filePaths = buildFilePaths(eventsDir, daysBack);
|
|
12
|
+
const sessionEvents = new Map();
|
|
13
|
+
for (const filePath of filePaths) {
|
|
14
|
+
const content = readFile(filePath);
|
|
15
|
+
if (content === null)
|
|
16
|
+
continue;
|
|
17
|
+
for (const line of content.split('\n')) {
|
|
18
|
+
const trimmed = line.trim();
|
|
19
|
+
if (!trimmed)
|
|
20
|
+
continue;
|
|
21
|
+
let obj;
|
|
22
|
+
try {
|
|
23
|
+
obj = JSON.parse(trimmed);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const eventSessionId = extractSessionId(obj);
|
|
29
|
+
if (!eventSessionId)
|
|
30
|
+
continue;
|
|
31
|
+
if (!matchesQuery(eventSessionId, sessionIdQuery))
|
|
32
|
+
continue;
|
|
33
|
+
let acc = sessionEvents.get(eventSessionId);
|
|
34
|
+
if (!acc) {
|
|
35
|
+
acc = createAccumulator(eventSessionId);
|
|
36
|
+
sessionEvents.set(eventSessionId, acc);
|
|
37
|
+
}
|
|
38
|
+
accumulateEvent(acc, obj, trimmed);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (sessionEvents.size === 0) {
|
|
42
|
+
return { kind: 'NOT_FOUND', sessionIdQuery, daysBack };
|
|
43
|
+
}
|
|
44
|
+
if (sessionEvents.size > 1) {
|
|
45
|
+
return {
|
|
46
|
+
kind: 'AMBIGUOUS',
|
|
47
|
+
sessionIdQuery,
|
|
48
|
+
candidates: Array.from(sessionEvents.keys()).sort(),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const acc = sessionEvents.values().next().value;
|
|
52
|
+
return classify(acc);
|
|
53
|
+
}
|
|
54
|
+
function classify(acc) {
|
|
55
|
+
const metrics = buildMetrics(acc);
|
|
56
|
+
const steps = buildSteps(acc);
|
|
57
|
+
const durationMs = acc.startedAt !== null && acc.lastTs !== null
|
|
58
|
+
? acc.lastTs - acc.startedAt
|
|
59
|
+
: 0;
|
|
60
|
+
if (acc.completedEvent?.outcome === 'success') {
|
|
61
|
+
return {
|
|
62
|
+
kind: 'SUCCESS',
|
|
63
|
+
sessionId: acc.sessionId,
|
|
64
|
+
workflowId: acc.workflowId,
|
|
65
|
+
startedAt: acc.startedAt,
|
|
66
|
+
durationMs,
|
|
67
|
+
metrics,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
const processState = determineProcessState(acc);
|
|
71
|
+
if (acc.abortedEvent !== null) {
|
|
72
|
+
const infraReason = acc.abortedEvent.reason === 'daemon_shutdown' ? 'daemon_shutdown'
|
|
73
|
+
: acc.abortedEvent.reason === 'daemon_killed' ? 'daemon_killed'
|
|
74
|
+
: 'unknown';
|
|
75
|
+
return {
|
|
76
|
+
kind: 'INFRA_ERROR',
|
|
77
|
+
sessionId: acc.sessionId,
|
|
78
|
+
workflowId: acc.workflowId,
|
|
79
|
+
startedAt: acc.startedAt,
|
|
80
|
+
durationMs,
|
|
81
|
+
infraReason,
|
|
82
|
+
detail: `Session interrupted: ${acc.abortedEvent.reason}`,
|
|
83
|
+
metrics,
|
|
84
|
+
steps,
|
|
85
|
+
processState,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
if (acc.completedEvent === null) {
|
|
89
|
+
return {
|
|
90
|
+
kind: 'ORPHANED',
|
|
91
|
+
sessionId: acc.sessionId,
|
|
92
|
+
workflowId: acc.workflowId,
|
|
93
|
+
startedAt: acc.startedAt,
|
|
94
|
+
durationMs,
|
|
95
|
+
lastEventKind: acc.lastEventKind,
|
|
96
|
+
lastEventTs: acc.lastTs,
|
|
97
|
+
metrics,
|
|
98
|
+
steps,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const { outcome, detail, rawLine } = acc.completedEvent;
|
|
102
|
+
const detailTruncated = detail.length >= 198;
|
|
103
|
+
if (outcome === 'stuck') {
|
|
104
|
+
return {
|
|
105
|
+
kind: 'WORKFLOW_STUCK',
|
|
106
|
+
sessionId: acc.sessionId,
|
|
107
|
+
workflowId: acc.workflowId,
|
|
108
|
+
startedAt: acc.startedAt,
|
|
109
|
+
durationMs,
|
|
110
|
+
stuckReason: acc.stuckReason ?? 'no_progress',
|
|
111
|
+
stuckDetail: acc.stuckDetail ?? detail,
|
|
112
|
+
toolName: acc.stuckToolName ?? undefined,
|
|
113
|
+
argsSummary: acc.stuckArgsSummary ?? undefined,
|
|
114
|
+
metrics,
|
|
115
|
+
steps,
|
|
116
|
+
processState,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
if (outcome === 'timeout') {
|
|
120
|
+
const timeoutReason = detail.includes('wall_clock') ? 'wall_clock'
|
|
121
|
+
: detail.includes('max_turns') ? 'max_turns'
|
|
122
|
+
: 'unknown';
|
|
123
|
+
return {
|
|
124
|
+
kind: 'WORKFLOW_TIMEOUT',
|
|
125
|
+
sessionId: acc.sessionId,
|
|
126
|
+
workflowId: acc.workflowId,
|
|
127
|
+
startedAt: acc.startedAt,
|
|
128
|
+
durationMs,
|
|
129
|
+
timeoutReason,
|
|
130
|
+
stepAdvances: acc.stepAdvances,
|
|
131
|
+
metrics,
|
|
132
|
+
steps,
|
|
133
|
+
processState,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
if (outcome === 'error' && (/model identifier|invalid model|api.*key|authentication/i.test(detail) || /\b400\b.*model|\bmodel\b.*\b400\b/i.test(detail))) {
|
|
137
|
+
return {
|
|
138
|
+
kind: 'CONFIG_ERROR',
|
|
139
|
+
sessionId: acc.sessionId,
|
|
140
|
+
workflowId: acc.workflowId,
|
|
141
|
+
startedAt: acc.startedAt,
|
|
142
|
+
durationMs,
|
|
143
|
+
detail,
|
|
144
|
+
detailTruncated,
|
|
145
|
+
metrics,
|
|
146
|
+
steps,
|
|
147
|
+
processState,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
if (outcome === 'error' && /aborted|SIGKILL|SIGTERM|network|ECONNRESET|ENOTFOUND/i.test(detail)) {
|
|
151
|
+
return {
|
|
152
|
+
kind: 'INFRA_ERROR',
|
|
153
|
+
sessionId: acc.sessionId,
|
|
154
|
+
workflowId: acc.workflowId,
|
|
155
|
+
startedAt: acc.startedAt,
|
|
156
|
+
durationMs,
|
|
157
|
+
infraReason: /aborted/i.test(detail) ? 'aborted' : 'network',
|
|
158
|
+
detail,
|
|
159
|
+
metrics,
|
|
160
|
+
steps,
|
|
161
|
+
processState,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
kind: 'DEFAULT',
|
|
166
|
+
sessionId: acc.sessionId,
|
|
167
|
+
workflowId: acc.workflowId,
|
|
168
|
+
startedAt: acc.startedAt,
|
|
169
|
+
durationMs,
|
|
170
|
+
outcome,
|
|
171
|
+
detail,
|
|
172
|
+
rawEventLine: rawLine,
|
|
173
|
+
metrics,
|
|
174
|
+
steps,
|
|
175
|
+
processState,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function accumulateEvent(acc, obj, rawLine) {
|
|
179
|
+
const kind = typeof obj['kind'] === 'string' ? obj['kind'] : null;
|
|
180
|
+
const ts = typeof obj['ts'] === 'number' ? obj['ts'] : null;
|
|
181
|
+
if (ts !== null) {
|
|
182
|
+
if (acc.startedAt === null || ts < acc.startedAt) {
|
|
183
|
+
acc.startedAt = ts;
|
|
184
|
+
}
|
|
185
|
+
if (acc.lastTs === null || ts > acc.lastTs) {
|
|
186
|
+
acc.lastTs = ts;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (kind !== null)
|
|
190
|
+
acc.lastEventKind = kind;
|
|
191
|
+
if (!kind)
|
|
192
|
+
return;
|
|
193
|
+
switch (kind) {
|
|
194
|
+
case 'session_started': {
|
|
195
|
+
if (typeof obj['workflowId'] === 'string')
|
|
196
|
+
acc.workflowId = obj['workflowId'];
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
case 'session_completed': {
|
|
200
|
+
const outcome = typeof obj['outcome'] === 'string' ? obj['outcome'] : 'unknown';
|
|
201
|
+
const detail = typeof obj['detail'] === 'string' ? obj['detail'] : '';
|
|
202
|
+
acc.completedEvent = { outcome, detail, rawLine };
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
case 'session_aborted': {
|
|
206
|
+
const reason = typeof obj['reason'] === 'string' ? obj['reason'] : 'unknown';
|
|
207
|
+
acc.abortedEvent = { reason };
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
case 'agent_stuck': {
|
|
211
|
+
const reason = typeof obj['reason'] === 'string' ? obj['reason'] : null;
|
|
212
|
+
if (reason === 'repeated_tool_call' || reason === 'no_progress' || reason === 'stall') {
|
|
213
|
+
acc.stuckReason = reason;
|
|
214
|
+
}
|
|
215
|
+
acc.stuckDetail = typeof obj['detail'] === 'string' ? obj['detail'] : null;
|
|
216
|
+
acc.stuckToolName = typeof obj['toolName'] === 'string' ? obj['toolName'] : null;
|
|
217
|
+
acc.stuckArgsSummary = typeof obj['argsSummary'] === 'string' ? obj['argsSummary'] : null;
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
case 'llm_turn_completed': {
|
|
221
|
+
acc.llmTurns++;
|
|
222
|
+
const inputTokens = typeof obj['inputTokens'] === 'number' ? obj['inputTokens'] : 0;
|
|
223
|
+
const outputTokens = typeof obj['outputTokens'] === 'number' ? obj['outputTokens'] : 0;
|
|
224
|
+
acc.inputTokens += inputTokens;
|
|
225
|
+
acc.outputTokens += outputTokens;
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
case 'step_advanced': {
|
|
229
|
+
const turnsForStep = acc.llmTurns - acc.turnsAtLastStep;
|
|
230
|
+
acc.stepTurnCounts.push(turnsForStep);
|
|
231
|
+
acc.turnsAtLastStep = acc.llmTurns;
|
|
232
|
+
acc.stepAdvances++;
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
case 'tool_call_started': {
|
|
236
|
+
acc.toolCallsTotal++;
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
case 'tool_call_failed': {
|
|
240
|
+
acc.toolCallsFailed++;
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
case 'tool_called': {
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
function buildFilePaths(eventsDir, daysBack) {
|
|
249
|
+
const paths = [];
|
|
250
|
+
const now = new Date();
|
|
251
|
+
for (let i = 0; i < daysBack; i++) {
|
|
252
|
+
const d = new Date(now);
|
|
253
|
+
d.setDate(d.getDate() - i);
|
|
254
|
+
const dateStr = d.toISOString().slice(0, 10);
|
|
255
|
+
paths.push(`${eventsDir}/${dateStr}.jsonl`);
|
|
256
|
+
}
|
|
257
|
+
return paths;
|
|
258
|
+
}
|
|
259
|
+
function extractSessionId(obj) {
|
|
260
|
+
const sid = typeof obj['sessionId'] === 'string' ? obj['sessionId'] : null;
|
|
261
|
+
const wrid = typeof obj['workrailSessionId'] === 'string' ? obj['workrailSessionId'] : null;
|
|
262
|
+
return sid ?? wrid;
|
|
263
|
+
}
|
|
264
|
+
function matchesQuery(sessionId, query) {
|
|
265
|
+
return sessionId === query || sessionId.startsWith(query);
|
|
266
|
+
}
|
|
267
|
+
function createAccumulator(sessionId) {
|
|
268
|
+
return {
|
|
269
|
+
sessionId,
|
|
270
|
+
workflowId: 'unknown',
|
|
271
|
+
startedAt: null,
|
|
272
|
+
lastTs: null,
|
|
273
|
+
llmTurns: 0,
|
|
274
|
+
stepAdvances: 0,
|
|
275
|
+
toolCallsTotal: 0,
|
|
276
|
+
toolCallsFailed: 0,
|
|
277
|
+
inputTokens: 0,
|
|
278
|
+
outputTokens: 0,
|
|
279
|
+
stuckReason: null,
|
|
280
|
+
stuckDetail: null,
|
|
281
|
+
stuckToolName: null,
|
|
282
|
+
stuckArgsSummary: null,
|
|
283
|
+
completedEvent: null,
|
|
284
|
+
abortedEvent: null,
|
|
285
|
+
lastEventKind: null,
|
|
286
|
+
turnsAtLastStep: 0,
|
|
287
|
+
stepTurnCounts: [],
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
function buildMetrics(acc) {
|
|
291
|
+
return {
|
|
292
|
+
llmTurns: acc.llmTurns,
|
|
293
|
+
stepAdvances: acc.stepAdvances,
|
|
294
|
+
toolCallsTotal: acc.toolCallsTotal,
|
|
295
|
+
toolCallsFailed: acc.toolCallsFailed,
|
|
296
|
+
inputTokens: acc.inputTokens,
|
|
297
|
+
outputTokens: acc.outputTokens,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
function buildSteps(acc) {
|
|
301
|
+
const steps = [];
|
|
302
|
+
for (let i = 0; i < acc.stepTurnCounts.length; i++) {
|
|
303
|
+
steps.push({ index: i + 1, status: 'completed', turns: acc.stepTurnCounts[i] ?? 0 });
|
|
304
|
+
}
|
|
305
|
+
if (acc.stepAdvances > 0 || acc.llmTurns > 0) {
|
|
306
|
+
const turnsOnTerminalStep = acc.llmTurns - acc.turnsAtLastStep;
|
|
307
|
+
steps.push({
|
|
308
|
+
index: steps.length + 1,
|
|
309
|
+
status: acc.completedEvent?.outcome === 'success' ? 'completed' : 'terminal',
|
|
310
|
+
turns: turnsOnTerminalStep,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
return steps;
|
|
314
|
+
}
|
|
315
|
+
function determineProcessState(acc) {
|
|
316
|
+
if (acc.completedEvent !== null || acc.abortedEvent !== null)
|
|
317
|
+
return 'STOPPED';
|
|
318
|
+
return 'UNKNOWN';
|
|
319
|
+
}
|
|
320
|
+
const GLYPHS = {
|
|
321
|
+
done: '✓',
|
|
322
|
+
terminal: '→',
|
|
323
|
+
pending: '·',
|
|
324
|
+
};
|
|
325
|
+
const ASCII_GLYPHS = {
|
|
326
|
+
done: '[ok]',
|
|
327
|
+
terminal: '[->]',
|
|
328
|
+
pending: '[ ]',
|
|
329
|
+
};
|
|
330
|
+
const ZONE3_CAP = 8;
|
|
331
|
+
function getGlyph(key, opts) {
|
|
332
|
+
return opts.ascii ? ASCII_GLYPHS[key] : GLYPHS[key];
|
|
333
|
+
}
|
|
334
|
+
function applyChalk(text, fn, opts) {
|
|
335
|
+
return opts.noColor ? text : fn(text);
|
|
336
|
+
}
|
|
337
|
+
function formatDiagnosticCard(result, opts = {}) {
|
|
338
|
+
switch (result.kind) {
|
|
339
|
+
case 'NOT_FOUND':
|
|
340
|
+
return formatNotFound(result, opts);
|
|
341
|
+
case 'AMBIGUOUS':
|
|
342
|
+
return formatAmbiguous(result, opts);
|
|
343
|
+
case 'SUCCESS':
|
|
344
|
+
return formatSuccess(result, opts);
|
|
345
|
+
case 'CONFIG_ERROR':
|
|
346
|
+
return formatConfigError(result, opts);
|
|
347
|
+
case 'WORKFLOW_STUCK':
|
|
348
|
+
return formatWorkflowStuck(result, opts);
|
|
349
|
+
case 'WORKFLOW_TIMEOUT':
|
|
350
|
+
return formatWorkflowTimeout(result, opts);
|
|
351
|
+
case 'INFRA_ERROR':
|
|
352
|
+
return formatInfraError(result, opts);
|
|
353
|
+
case 'ORPHANED':
|
|
354
|
+
return formatOrphaned(result, opts);
|
|
355
|
+
case 'DEFAULT':
|
|
356
|
+
return formatDefault(result, opts);
|
|
357
|
+
default: {
|
|
358
|
+
const _exhaustive = result;
|
|
359
|
+
return `Unknown diagnostic kind: ${JSON.stringify(_exhaustive)}`;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
function formatHeader(categoryBadge, sessionId, workflowId, startedAt, durationMs, processState) {
|
|
364
|
+
const startedStr = startedAt !== null
|
|
365
|
+
? `Started: ${formatStartedAt(startedAt)}`
|
|
366
|
+
: '';
|
|
367
|
+
const durationStr = formatDuration(durationMs);
|
|
368
|
+
const stateStr = processState !== null ? ` [${processState}]` : '';
|
|
369
|
+
const parts = [categoryBadge, sessionId, workflowId, startedStr, durationStr].filter(Boolean);
|
|
370
|
+
return parts.join(' ') + stateStr;
|
|
371
|
+
}
|
|
372
|
+
function formatStartedAt(ts) {
|
|
373
|
+
const d = new Date(ts);
|
|
374
|
+
const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
375
|
+
const day = days[d.getDay()] ?? '';
|
|
376
|
+
const hh = String(d.getHours()).padStart(2, '0');
|
|
377
|
+
const mm = String(d.getMinutes()).padStart(2, '0');
|
|
378
|
+
return `${day} ${hh}:${mm}`;
|
|
379
|
+
}
|
|
380
|
+
function formatDuration(ms) {
|
|
381
|
+
const totalSec = Math.floor(ms / 1000);
|
|
382
|
+
const min = Math.floor(totalSec / 60);
|
|
383
|
+
const sec = totalSec % 60;
|
|
384
|
+
if (min > 0)
|
|
385
|
+
return `${min}m ${sec}s`;
|
|
386
|
+
return `${totalSec}s`;
|
|
387
|
+
}
|
|
388
|
+
function formatMetricsLine(m) {
|
|
389
|
+
const failRate = m.toolCallsTotal > 0
|
|
390
|
+
? ` (${m.toolCallsFailed} failed, ${Math.round((m.toolCallsFailed / m.toolCallsTotal) * 100)}%)`
|
|
391
|
+
: '';
|
|
392
|
+
const tokensIn = m.inputTokens > 0 ? `${Math.round(m.inputTokens / 1000)}k in` : null;
|
|
393
|
+
const tokensOut = m.outputTokens > 0 ? `${Math.round(m.outputTokens / 1000)}k out` : null;
|
|
394
|
+
const tokensStr = [tokensIn, tokensOut].filter(Boolean).join(' / ');
|
|
395
|
+
const parts = [
|
|
396
|
+
`Turns: ${m.llmTurns}`,
|
|
397
|
+
`Steps: ${m.stepAdvances}`,
|
|
398
|
+
`Tool calls: ${m.toolCallsTotal}${failRate}`,
|
|
399
|
+
tokensStr ? `Tokens: ${tokensStr}` : null,
|
|
400
|
+
].filter(Boolean);
|
|
401
|
+
return parts.join(' | ');
|
|
402
|
+
}
|
|
403
|
+
function formatStepTimeline(steps, opts) {
|
|
404
|
+
if (steps.length === 0) {
|
|
405
|
+
return 'Step timeline:\n (session terminated before first step)';
|
|
406
|
+
}
|
|
407
|
+
let displaySteps = steps;
|
|
408
|
+
let ellipsisLine = null;
|
|
409
|
+
if (steps.length > ZONE3_CAP) {
|
|
410
|
+
const firstTwo = steps.slice(0, 2);
|
|
411
|
+
const lastThree = steps.slice(-3);
|
|
412
|
+
const omitted = steps.length - 5;
|
|
413
|
+
ellipsisLine = ` ... (${omitted} steps omitted, use --verbose to see all) ...`;
|
|
414
|
+
displaySteps = [...firstTwo, ...lastThree];
|
|
415
|
+
}
|
|
416
|
+
const lines = ['Step timeline:'];
|
|
417
|
+
let ellipsisInserted = false;
|
|
418
|
+
for (const step of displaySteps) {
|
|
419
|
+
if (ellipsisLine && !ellipsisInserted && step === steps[steps.length - 3] && steps.length > ZONE3_CAP) {
|
|
420
|
+
lines.push(ellipsisLine);
|
|
421
|
+
ellipsisInserted = true;
|
|
422
|
+
}
|
|
423
|
+
const glyph = step.status === 'completed' ? getGlyph('done', opts)
|
|
424
|
+
: step.status === 'terminal' ? getGlyph('terminal', opts)
|
|
425
|
+
: getGlyph('pending', opts);
|
|
426
|
+
const turnsStr = step.turns > 0 ? `${step.turns} turns` : '';
|
|
427
|
+
const statusLabel = step.status === 'terminal' ? ' [STOPPED]' : '';
|
|
428
|
+
const stepLine = step.status === 'terminal'
|
|
429
|
+
? ` ${applyChalk(glyph, chalk_1.default.bold, opts)} step ${step.index}${turnsStr ? ` ${turnsStr}` : ''}${statusLabel}`
|
|
430
|
+
: ` ${glyph} step ${step.index}${turnsStr ? ` ${turnsStr}` : ''}`;
|
|
431
|
+
lines.push(stepLine);
|
|
432
|
+
}
|
|
433
|
+
if (ellipsisLine && !ellipsisInserted) {
|
|
434
|
+
lines.push(ellipsisLine);
|
|
435
|
+
}
|
|
436
|
+
return lines.join('\n');
|
|
437
|
+
}
|
|
438
|
+
function formatNotFound(result, _opts) {
|
|
439
|
+
return [
|
|
440
|
+
`Session not found in the last ${result.daysBack} days.`,
|
|
441
|
+
`Query: "${result.sessionIdQuery}"`,
|
|
442
|
+
``,
|
|
443
|
+
`Verify the session ID, or check execution-stats.jsonl for older sessions:`,
|
|
444
|
+
` cat ~/.workrail/data/execution-stats.jsonl | grep "${result.sessionIdQuery.slice(0, 8)}"`,
|
|
445
|
+
].join('\n');
|
|
446
|
+
}
|
|
447
|
+
function formatAmbiguous(result, _opts) {
|
|
448
|
+
return [
|
|
449
|
+
`Multiple sessions match "${result.sessionIdQuery}". Be more specific:`,
|
|
450
|
+
...result.candidates.map(c => ` ${c}`),
|
|
451
|
+
].join('\n');
|
|
452
|
+
}
|
|
453
|
+
function formatSuccess(result, opts) {
|
|
454
|
+
const badge = applyChalk('[SUCCESS]', chalk_1.default.green, opts);
|
|
455
|
+
const header = formatHeader(badge, result.sessionId, result.workflowId, result.startedAt, result.durationMs, null);
|
|
456
|
+
return [
|
|
457
|
+
header,
|
|
458
|
+
``,
|
|
459
|
+
`DIAGNOSIS: SUCCESS -- session completed normally`,
|
|
460
|
+
``,
|
|
461
|
+
` No failure detected.`,
|
|
462
|
+
` Run: worktrain logs --session ${result.sessionId} to see session activity.`,
|
|
463
|
+
``,
|
|
464
|
+
formatMetricsLine(result.metrics),
|
|
465
|
+
].join('\n');
|
|
466
|
+
}
|
|
467
|
+
function formatConfigError(result, opts) {
|
|
468
|
+
const badge = applyChalk('[CONFIG]', chalk_1.default.red, opts);
|
|
469
|
+
const header = formatHeader(badge, result.sessionId, result.workflowId, result.startedAt, result.durationMs, result.processState);
|
|
470
|
+
const truncNote = result.detailTruncated ? `\n (truncated at 200 chars -- see conversation log for full text)` : '';
|
|
471
|
+
return [
|
|
472
|
+
header,
|
|
473
|
+
``,
|
|
474
|
+
applyChalk(`DIAGNOSIS: CONFIG -- invalid model or API configuration`, chalk_1.default.red, opts),
|
|
475
|
+
``,
|
|
476
|
+
` Error: "${result.detail}"${truncNote}`,
|
|
477
|
+
` Fix: Check agentConfig.model in triggers.yml.`,
|
|
478
|
+
` Use format: provider/model-id`,
|
|
479
|
+
` e.g. amazon-bedrock/us.anthropic.claude-sonnet-4-6`,
|
|
480
|
+
``,
|
|
481
|
+
formatMetricsLine(result.metrics),
|
|
482
|
+
``,
|
|
483
|
+
formatStepTimeline(result.steps, opts),
|
|
484
|
+
].join('\n');
|
|
485
|
+
}
|
|
486
|
+
function formatWorkflowStuck(result, opts) {
|
|
487
|
+
const badge = applyChalk('[STUCK]', chalk_1.default.red, opts);
|
|
488
|
+
const header = formatHeader(badge, result.sessionId, result.workflowId, result.startedAt, result.durationMs, result.processState);
|
|
489
|
+
const reasonLabel = result.stuckReason === 'repeated_tool_call' ? 'repeated tool call'
|
|
490
|
+
: result.stuckReason === 'no_progress' ? 'no step progress'
|
|
491
|
+
: 'stalled tool call';
|
|
492
|
+
const toolLine = result.toolName ? ` Tool: ${result.toolName}` : null;
|
|
493
|
+
const argsLine = result.argsSummary ? ` Args: "${result.argsSummary}"` : null;
|
|
494
|
+
let fixLine;
|
|
495
|
+
if (result.stuckReason === 'repeated_tool_call') {
|
|
496
|
+
fixLine = result.toolName
|
|
497
|
+
? ` Fix: ${result.toolName} called with identical args repeatedly. Review step prompt to clarify the observation loop.`
|
|
498
|
+
: ` Fix: Agent called the same tool with identical args repeatedly. Review step prompt.`;
|
|
499
|
+
}
|
|
500
|
+
else if (result.stuckReason === 'no_progress') {
|
|
501
|
+
fixLine = ` Fix: Agent used ${result.metrics.llmTurns} turns with 0 step advances. Step 1 prompt may be unclear or impossible with available tools.`;
|
|
502
|
+
}
|
|
503
|
+
else {
|
|
504
|
+
fixLine = ` Fix: A tool call hung and never completed. Check network connectivity or file lock issues.`;
|
|
505
|
+
}
|
|
506
|
+
return [
|
|
507
|
+
header,
|
|
508
|
+
``,
|
|
509
|
+
applyChalk(`DIAGNOSIS: STUCK -- ${reasonLabel}`, chalk_1.default.red, opts),
|
|
510
|
+
``,
|
|
511
|
+
toolLine,
|
|
512
|
+
argsLine,
|
|
513
|
+
fixLine,
|
|
514
|
+
``,
|
|
515
|
+
formatMetricsLine(result.metrics),
|
|
516
|
+
``,
|
|
517
|
+
formatStepTimeline(result.steps, opts),
|
|
518
|
+
].filter(line => line !== null).join('\n');
|
|
519
|
+
}
|
|
520
|
+
function formatWorkflowTimeout(result, opts) {
|
|
521
|
+
const badge = applyChalk('[TIMEOUT]', chalk_1.default.yellow, opts);
|
|
522
|
+
const header = formatHeader(badge, result.sessionId, result.workflowId, result.startedAt, result.durationMs, result.processState);
|
|
523
|
+
const reasonLabel = result.timeoutReason === 'wall_clock' ? 'wall clock limit reached'
|
|
524
|
+
: result.timeoutReason === 'max_turns' ? 'turn limit reached'
|
|
525
|
+
: 'limit reached';
|
|
526
|
+
let fixLine;
|
|
527
|
+
if (result.timeoutReason === 'wall_clock') {
|
|
528
|
+
fixLine = result.stepAdvances === 0
|
|
529
|
+
? ` Fix: Agent never advanced a step before timeout. The workflow prompt may be unclear.`
|
|
530
|
+
: ` Fix: Increase maxSessionMinutes in triggers.yml agentConfig, or narrow the workflow scope.`;
|
|
531
|
+
}
|
|
532
|
+
else if (result.timeoutReason === 'max_turns') {
|
|
533
|
+
fixLine = ` Fix: Increase maxTurns in triggers.yml agentConfig, or simplify the workflow.`;
|
|
534
|
+
}
|
|
535
|
+
else {
|
|
536
|
+
fixLine = ` Fix: Increase maxSessionMinutes or maxTurns in triggers.yml agentConfig.`;
|
|
537
|
+
}
|
|
538
|
+
return [
|
|
539
|
+
header,
|
|
540
|
+
``,
|
|
541
|
+
applyChalk(`DIAGNOSIS: TIMEOUT -- ${reasonLabel}`, chalk_1.default.yellow, opts),
|
|
542
|
+
``,
|
|
543
|
+
` Steps completed: ${result.stepAdvances}`,
|
|
544
|
+
fixLine,
|
|
545
|
+
``,
|
|
546
|
+
formatMetricsLine(result.metrics),
|
|
547
|
+
``,
|
|
548
|
+
formatStepTimeline(result.steps, opts),
|
|
549
|
+
].join('\n');
|
|
550
|
+
}
|
|
551
|
+
function formatInfraError(result, opts) {
|
|
552
|
+
const badge = applyChalk('[INFRA]', chalk_1.default.yellow, opts);
|
|
553
|
+
const header = formatHeader(badge, result.sessionId, result.workflowId, result.startedAt, result.durationMs, result.processState);
|
|
554
|
+
const reasonLabel = result.infraReason === 'daemon_shutdown' ? 'daemon stopped mid-session'
|
|
555
|
+
: result.infraReason === 'daemon_killed' ? 'daemon killed mid-session'
|
|
556
|
+
: result.infraReason === 'network' ? 'network error'
|
|
557
|
+
: result.infraReason === 'aborted' ? 'session aborted'
|
|
558
|
+
: 'infrastructure error';
|
|
559
|
+
const fixLine = (result.infraReason === 'daemon_shutdown' || result.infraReason === 'daemon_killed')
|
|
560
|
+
? ` Fix: Restart the daemon: worktrain daemon start\n Re-queue the trigger or re-run the session manually.`
|
|
561
|
+
: ` Fix: Check daemon logs: worktrain logs --session ${result.sessionId}`;
|
|
562
|
+
return [
|
|
563
|
+
header,
|
|
564
|
+
``,
|
|
565
|
+
applyChalk(`DIAGNOSIS: INFRA -- ${reasonLabel}`, chalk_1.default.yellow, opts),
|
|
566
|
+
``,
|
|
567
|
+
` Detail: ${result.detail}`,
|
|
568
|
+
fixLine,
|
|
569
|
+
``,
|
|
570
|
+
formatMetricsLine(result.metrics),
|
|
571
|
+
``,
|
|
572
|
+
formatStepTimeline(result.steps, opts),
|
|
573
|
+
].join('\n');
|
|
574
|
+
}
|
|
575
|
+
function formatOrphaned(result, opts) {
|
|
576
|
+
const badge = applyChalk('[ORPHANED]', chalk_1.default.gray ?? chalk_1.default.dim, opts);
|
|
577
|
+
const header = formatHeader(badge, result.sessionId, result.workflowId, result.startedAt, result.durationMs, null);
|
|
578
|
+
const lastEventStr = result.lastEventKind && result.lastEventTs !== null
|
|
579
|
+
? ` Last event: ${result.lastEventKind} (${formatRelativeTime(result.lastEventTs)} ago)`
|
|
580
|
+
: ` Last event: unknown`;
|
|
581
|
+
return [
|
|
582
|
+
header,
|
|
583
|
+
``,
|
|
584
|
+
applyChalk(`DIAGNOSIS: ORPHANED -- session ended without a completion event`, chalk_1.default.dim, opts),
|
|
585
|
+
``,
|
|
586
|
+
lastEventStr,
|
|
587
|
+
` Note: The daemon may have crashed or been killed mid-session.`,
|
|
588
|
+
` Fix: Check daemon process: worktrain daemon --status`,
|
|
589
|
+
` If stopped, restart: worktrain daemon start`,
|
|
590
|
+
``,
|
|
591
|
+
formatMetricsLine(result.metrics),
|
|
592
|
+
``,
|
|
593
|
+
formatStepTimeline(result.steps, opts),
|
|
594
|
+
].join('\n');
|
|
595
|
+
}
|
|
596
|
+
function formatDefault(result, opts) {
|
|
597
|
+
const badge = `[${result.outcome.toUpperCase()}]`;
|
|
598
|
+
const header = formatHeader(badge, result.sessionId, result.workflowId, result.startedAt, result.durationMs, result.processState);
|
|
599
|
+
return [
|
|
600
|
+
header,
|
|
601
|
+
``,
|
|
602
|
+
`DIAGNOSIS: UNKNOWN -- unrecognized failure type`,
|
|
603
|
+
``,
|
|
604
|
+
` Outcome: ${result.outcome}`,
|
|
605
|
+
result.detail ? ` Detail: ${result.detail}` : null,
|
|
606
|
+
` Raw: ${result.rawEventLine.slice(0, 200)}`,
|
|
607
|
+
``,
|
|
608
|
+
` No automated fix suggestion available for this failure type.`,
|
|
609
|
+
` File an issue: https://github.com/EtienneBBeaulac/workrail/issues`,
|
|
610
|
+
``,
|
|
611
|
+
formatMetricsLine(result.metrics),
|
|
612
|
+
``,
|
|
613
|
+
formatStepTimeline(result.steps, opts),
|
|
614
|
+
].filter(line => line !== null).join('\n');
|
|
615
|
+
}
|
|
616
|
+
function formatRelativeTime(ts) {
|
|
617
|
+
const elapsed = Date.now() - ts;
|
|
618
|
+
const min = Math.floor(elapsed / 60000);
|
|
619
|
+
if (min < 1)
|
|
620
|
+
return 'just now';
|
|
621
|
+
if (min < 60)
|
|
622
|
+
return `${min} minutes`;
|
|
623
|
+
const h = Math.floor(min / 60);
|
|
624
|
+
return `${h} hours`;
|
|
625
|
+
}
|
|
626
|
+
function formatDiagnosticJson(result) {
|
|
627
|
+
return JSON.stringify(result, null, 2);
|
|
628
|
+
}
|