taskplane 0.5.12 β 0.6.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/README.md +1 -1
- package/bin/rpc-wrapper.mjs +777 -0
- package/dashboard/public/app.js +45 -6
- package/dashboard/public/style.css +31 -0
- package/dashboard/server.cjs +326 -1
- package/extensions/task-runner.ts +1111 -30
- package/extensions/taskplane/config-loader.ts +31 -0
- package/extensions/taskplane/config-schema.ts +88 -0
- package/extensions/taskplane/diagnostic-reports.ts +463 -0
- package/extensions/taskplane/diagnostics.ts +323 -0
- package/extensions/taskplane/engine.ts +407 -62
- package/extensions/taskplane/extension.ts +259 -8
- package/extensions/taskplane/index.ts +1 -0
- package/extensions/taskplane/merge.ts +786 -66
- package/extensions/taskplane/messages.ts +594 -2
- package/extensions/taskplane/persistence.ts +342 -19
- package/extensions/taskplane/quality-gate.ts +1033 -0
- package/extensions/taskplane/resume.ts +505 -36
- package/extensions/taskplane/supervisor-primer.md +664 -0
- package/extensions/taskplane/types.ts +534 -6
- package/extensions/taskplane/verification.ts +537 -0
- package/extensions/taskplane/worktree.ts +329 -1
- package/package.json +1 -1
- package/skills/create-taskplane-task/references/prompt-template.md +0 -2
- package/templates/agents/task-reviewer.md +54 -2
- package/templates/agents/task-worker.md +8 -4
package/dashboard/public/app.js
CHANGED
|
@@ -73,6 +73,26 @@ function tokenSummaryFromLaneState(ls) {
|
|
|
73
73
|
return s;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
/** Build compact telemetry badge HTML for retry/compaction indicators.
|
|
77
|
+
* Only shows badges when telemetry data has meaningful values.
|
|
78
|
+
* @param {object|null} tel - Telemetry data for a lane (from currentData.telemetry[prefix])
|
|
79
|
+
* @returns {string} HTML string with badges, or "" if nothing to show
|
|
80
|
+
*/
|
|
81
|
+
function telemetryBadgesHtml(tel) {
|
|
82
|
+
if (!tel) return "";
|
|
83
|
+
let badges = "";
|
|
84
|
+
if (tel.retryActive) {
|
|
85
|
+
const err = tel.lastRetryError ? ` β ${tel.lastRetryError}` : "";
|
|
86
|
+
badges += `<span class="telem-badge telem-retry-active" title="Retry in progress${escapeHtml(err)}">π retrying</span>`;
|
|
87
|
+
} else if (tel.retries > 0) {
|
|
88
|
+
badges += `<span class="telem-badge telem-retry" title="${tel.retries} auto-retry event(s)">π ${tel.retries}</span>`;
|
|
89
|
+
}
|
|
90
|
+
if (tel.compactions > 0) {
|
|
91
|
+
badges += `<span class="telem-badge telem-compaction" title="${tel.compactions} context compaction(s)">π ${tel.compactions}</span>`;
|
|
92
|
+
}
|
|
93
|
+
return badges;
|
|
94
|
+
}
|
|
95
|
+
|
|
76
96
|
// βββ Copy to Clipboard ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
77
97
|
|
|
78
98
|
let toastEl = null;
|
|
@@ -342,14 +362,19 @@ function renderSummary(batch) {
|
|
|
342
362
|
|
|
343
363
|
// Aggregate tokens across all active lane states
|
|
344
364
|
const laneStates = currentData?.laneStates || {};
|
|
345
|
-
let batchInput = 0, batchOutput = 0, batchCacheRead = 0, batchCacheWrite = 0,
|
|
365
|
+
let batchInput = 0, batchOutput = 0, batchCacheRead = 0, batchCacheWrite = 0, batchCostFromLanes = 0;
|
|
346
366
|
for (const ls of Object.values(laneStates)) {
|
|
347
367
|
batchInput += ls.workerInputTokens || 0;
|
|
348
368
|
batchOutput += ls.workerOutputTokens || 0;
|
|
349
369
|
batchCacheRead += ls.workerCacheReadTokens || 0;
|
|
350
370
|
batchCacheWrite += ls.workerCacheWriteTokens || 0;
|
|
351
|
-
|
|
371
|
+
batchCostFromLanes += ls.workerCostUsd || 0;
|
|
352
372
|
}
|
|
373
|
+
// Use server-computed batchTotalCost (includes telemetry for uncovered lanes);
|
|
374
|
+
// fallback to lane-state-only sum for backward compatibility (pre-telemetry server)
|
|
375
|
+
const batchCost = (currentData?.batchTotalCost != null && currentData.batchTotalCost > 0)
|
|
376
|
+
? currentData.batchTotalCost
|
|
377
|
+
: batchCostFromLanes;
|
|
353
378
|
const batchTotalIn = batchInput + batchCacheRead;
|
|
354
379
|
if (batchTotalIn > 0 || batchOutput > 0) {
|
|
355
380
|
let tokenStr = ` Β· tokens: β${formatTokens(batchTotalIn)} β${formatTokens(batchOutput)}`;
|
|
@@ -386,6 +411,7 @@ function renderLanesTasks(batch, tmuxSessions) {
|
|
|
386
411
|
const tasks = batch.tasks || [];
|
|
387
412
|
const tmuxSet = new Set(tmuxSessions || []);
|
|
388
413
|
const laneStates = currentData?.laneStates || {};
|
|
414
|
+
const telemetry = currentData?.telemetry || {};
|
|
389
415
|
const showRepos = knownRepos.length >= 2;
|
|
390
416
|
let html = "";
|
|
391
417
|
|
|
@@ -432,8 +458,9 @@ function renderLanesTasks(batch, tmuxSessions) {
|
|
|
432
458
|
html += `<div class="task-row"><span class="task-icon"></span><span style="color:var(--text-faint);grid-column:2/-1;">No tasks assigned</span></div>`;
|
|
433
459
|
}
|
|
434
460
|
|
|
435
|
-
// Get lane state for worker stats
|
|
461
|
+
// Get lane state and telemetry for worker stats
|
|
436
462
|
const ls = laneStates[lane.tmuxSessionName] || null;
|
|
463
|
+
const tel = telemetry[lane.tmuxSessionName] || null;
|
|
437
464
|
|
|
438
465
|
for (const task of laneTasks) {
|
|
439
466
|
// Repo filtering at task level
|
|
@@ -486,8 +513,9 @@ function renderLanesTasks(batch, tmuxSessions) {
|
|
|
486
513
|
stepHtml = `<span style="color:var(--text-faint)">${escapeHtml(task.exitReason || "β")}</span>`;
|
|
487
514
|
}
|
|
488
515
|
|
|
489
|
-
// Worker stats from lane state sidecar
|
|
516
|
+
// Worker stats from lane state sidecar + telemetry badges
|
|
490
517
|
let workerHtml = "";
|
|
518
|
+
const telemBadges = task.status !== "pending" ? telemetryBadgesHtml(tel) : "";
|
|
491
519
|
if (ls && ls.workerStatus === "running" && task.status === "running") {
|
|
492
520
|
const elapsed = ls.workerElapsed ? `${Math.round(ls.workerElapsed / 1000)}s` : "";
|
|
493
521
|
const tools = ls.workerToolCount || 0;
|
|
@@ -500,11 +528,22 @@ function renderLanesTasks(batch, tmuxSessions) {
|
|
|
500
528
|
if (ctx) workerHtml += `<span class="worker-stat" title="Context window used">π ${ctx}</span>`;
|
|
501
529
|
if (tokenStr) workerHtml += `<span class="worker-stat" title="Tokens: inputβ outputβ cacheRead(R) cacheWrite(W)">πͺ ${tokenStr}</span>`;
|
|
502
530
|
if (lastTool) workerHtml += `<span class="worker-stat worker-last-tool" title="Last tool call">${escapeHtml(lastTool)}</span>`;
|
|
531
|
+
workerHtml += telemBadges;
|
|
532
|
+
workerHtml += `</div>`;
|
|
533
|
+
} else if (!ls && tel && task.status === "running") {
|
|
534
|
+
// Running task with telemetry but no lane-state yet (early startup)
|
|
535
|
+
const lastTool = tel.lastTool || "";
|
|
536
|
+
workerHtml = `<div class="worker-stats">`;
|
|
537
|
+
if (lastTool) workerHtml += `<span class="worker-stat worker-last-tool" title="Last tool call">${escapeHtml(lastTool)}</span>`;
|
|
538
|
+
workerHtml += telemBadges;
|
|
503
539
|
workerHtml += `</div>`;
|
|
504
540
|
} else if (ls && ls.workerStatus === "done" && task.status !== "pending") {
|
|
505
|
-
workerHtml = `<div class="worker-stats"><span class="worker-stat" style="color:var(--green)">β Worker done</span
|
|
541
|
+
workerHtml = `<div class="worker-stats"><span class="worker-stat" style="color:var(--green)">β Worker done</span>${telemBadges}</div>`;
|
|
506
542
|
} else if (ls && ls.workerStatus === "error" && task.status !== "pending") {
|
|
507
|
-
workerHtml = `<div class="worker-stats"><span class="worker-stat" style="color:var(--red)">β Worker error</span
|
|
543
|
+
workerHtml = `<div class="worker-stats"><span class="worker-stat" style="color:var(--red)">β Worker error</span>${telemBadges}</div>`;
|
|
544
|
+
} else if (telemBadges && task.status !== "pending") {
|
|
545
|
+
// No lane-state but telemetry exists (done/error lane without sidecar)
|
|
546
|
+
workerHtml = `<div class="worker-stats">${telemBadges}</div>`;
|
|
508
547
|
}
|
|
509
548
|
|
|
510
549
|
const isViewingStatus = viewerMode === 'status-md' && viewerTarget === task.taskId;
|
|
@@ -631,6 +631,37 @@ body {
|
|
|
631
631
|
text-overflow: ellipsis;
|
|
632
632
|
}
|
|
633
633
|
|
|
634
|
+
/* βββ Telemetry Badges (retry, compaction) βββββββββββββββββββββββββββββββ */
|
|
635
|
+
|
|
636
|
+
.telem-badge {
|
|
637
|
+
display: inline-flex;
|
|
638
|
+
align-items: center;
|
|
639
|
+
gap: 3px;
|
|
640
|
+
font-family: var(--font-mono);
|
|
641
|
+
font-size: 0.65rem;
|
|
642
|
+
font-weight: 500;
|
|
643
|
+
padding: 1px 7px;
|
|
644
|
+
border-radius: 8px;
|
|
645
|
+
white-space: nowrap;
|
|
646
|
+
margin-left: 4px;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
.telem-retry {
|
|
650
|
+
background: rgba(210,153,34,0.15);
|
|
651
|
+
color: var(--yellow);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
.telem-retry-active {
|
|
655
|
+
background: rgba(210,153,34,0.25);
|
|
656
|
+
color: var(--yellow);
|
|
657
|
+
animation: pulse 1.5s infinite;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
.telem-compaction {
|
|
661
|
+
background: rgba(188,140,255,0.15);
|
|
662
|
+
color: var(--magenta);
|
|
663
|
+
}
|
|
664
|
+
|
|
634
665
|
/* βββ Conversation Viewer ββββββββββββββββββββββββββββββββββββββββββββββββ */
|
|
635
666
|
|
|
636
667
|
.conv-stream {
|
package/dashboard/server.cjs
CHANGED
|
@@ -213,14 +213,337 @@ function loadLaneStates() {
|
|
|
213
213
|
return states;
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
+
// βββ Telemetry JSONL Tailing ββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Module-level tail state for incremental JSONL reading.
|
|
220
|
+
* Persists across poll ticks within this server process.
|
|
221
|
+
* Key: absolute file path β { offset, partial }
|
|
222
|
+
*/
|
|
223
|
+
const telemetryTailStates = new Map();
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Module-level accumulated telemetry per tmux prefix.
|
|
227
|
+
* Persists across poll ticks so incremental tail reads accumulate correctly.
|
|
228
|
+
* Key: tmux prefix β { inputTokens, outputTokens, ... }
|
|
229
|
+
*/
|
|
230
|
+
const telemetryAccumulators = new Map();
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Tracks which files are currently contributing to each prefix.
|
|
234
|
+
* Key: tmux prefix β Set of absolute file paths
|
|
235
|
+
* Used to detect file rotation: when files change, accumulator is reset.
|
|
236
|
+
*/
|
|
237
|
+
const telemetryPrefixFiles = new Map();
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Parse a telemetry JSONL filename to extract lane number and role.
|
|
241
|
+
* Pattern: {opId}-{batchId}-{repoId}[-{taskId}][-lane-{N}]-{role}.jsonl
|
|
242
|
+
* Returns { laneNumber: number|null, role: string } or null if unparseable.
|
|
243
|
+
*/
|
|
244
|
+
function parseTelemetryFilename(filename) {
|
|
245
|
+
// Remove .jsonl extension
|
|
246
|
+
const base = filename.replace(/\.jsonl$/, "");
|
|
247
|
+
// Role is always the last segment
|
|
248
|
+
const lastDash = base.lastIndexOf("-");
|
|
249
|
+
if (lastDash < 0) return null;
|
|
250
|
+
const role = base.slice(lastDash + 1);
|
|
251
|
+
if (role !== "worker" && role !== "reviewer") return null;
|
|
252
|
+
|
|
253
|
+
// Extract lane number from -lane-{N}- pattern
|
|
254
|
+
const laneMatch = base.match(/-lane-(\d+)-/);
|
|
255
|
+
const laneNumber = laneMatch ? parseInt(laneMatch[1], 10) : null;
|
|
256
|
+
|
|
257
|
+
return { laneNumber, role };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Incrementally read new bytes from a JSONL file, parse events, and return them.
|
|
262
|
+
* Handles: file not yet created, empty reads, partial trailing lines, malformed JSON.
|
|
263
|
+
* @param {string} filePath - Absolute path to the JSONL file
|
|
264
|
+
* @returns {object[]} Array of parsed event objects from new data
|
|
265
|
+
*/
|
|
266
|
+
function tailJsonlFile(filePath) {
|
|
267
|
+
// Get or create tail state for this file
|
|
268
|
+
let tailState = telemetryTailStates.get(filePath);
|
|
269
|
+
if (!tailState) {
|
|
270
|
+
tailState = { offset: 0, partial: "" };
|
|
271
|
+
telemetryTailStates.set(filePath, tailState);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Check file size
|
|
275
|
+
let fileSize;
|
|
276
|
+
try {
|
|
277
|
+
fileSize = fs.statSync(filePath).size;
|
|
278
|
+
} catch {
|
|
279
|
+
return []; // File doesn't exist yet
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Handle file truncation/recreation (offset beyond current size)
|
|
283
|
+
if (fileSize < tailState.offset) {
|
|
284
|
+
tailState.offset = 0;
|
|
285
|
+
tailState.partial = "";
|
|
286
|
+
tailState.wasReset = true; // Signal to caller that accumulator should be reset
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (fileSize <= tailState.offset) {
|
|
290
|
+
return []; // No new data
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Read new bytes from offset to end of file
|
|
294
|
+
const bytesToRead = fileSize - tailState.offset;
|
|
295
|
+
const buf = Buffer.alloc(bytesToRead);
|
|
296
|
+
let fd;
|
|
297
|
+
try {
|
|
298
|
+
fd = fs.openSync(filePath, "r");
|
|
299
|
+
} catch {
|
|
300
|
+
return []; // File became inaccessible
|
|
301
|
+
}
|
|
302
|
+
try {
|
|
303
|
+
fs.readSync(fd, buf, 0, bytesToRead, tailState.offset);
|
|
304
|
+
} catch {
|
|
305
|
+
fs.closeSync(fd);
|
|
306
|
+
return []; // Read error β try again next tick
|
|
307
|
+
}
|
|
308
|
+
fs.closeSync(fd);
|
|
309
|
+
tailState.offset = fileSize;
|
|
310
|
+
|
|
311
|
+
// Split into lines, preserving partial trailing line
|
|
312
|
+
const chunk = tailState.partial + buf.toString("utf-8");
|
|
313
|
+
const lines = chunk.split("\n");
|
|
314
|
+
tailState.partial = lines.pop() || "";
|
|
315
|
+
|
|
316
|
+
const events = [];
|
|
317
|
+
for (const line of lines) {
|
|
318
|
+
const trimmed = line.trim();
|
|
319
|
+
if (!trimmed) continue;
|
|
320
|
+
try {
|
|
321
|
+
const event = JSON.parse(trimmed);
|
|
322
|
+
if (event && event.type) events.push(event);
|
|
323
|
+
} catch {
|
|
324
|
+
// Malformed JSON β skip (concurrent write race, truncated line)
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return events;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Load and accumulate telemetry from .pi/telemetry/*.jsonl files.
|
|
332
|
+
* Returns telemetry keyed by tmux session prefix (e.g., "orch-lane-1").
|
|
333
|
+
*
|
|
334
|
+
* Uses batch-state lanes to map lane numbers β tmux prefixes.
|
|
335
|
+
* For standalone /task mode (no lane number in filename), data is keyed as "standalone".
|
|
336
|
+
*
|
|
337
|
+
* @param {object|null} batchState - The batch state from batch-state.json
|
|
338
|
+
* @returns {object} Map of tmuxPrefix β accumulated telemetry
|
|
339
|
+
*/
|
|
340
|
+
function loadTelemetryData(batchState) {
|
|
341
|
+
const telemetryDir = path.join(REPO_ROOT, ".pi", "telemetry");
|
|
342
|
+
const result = {};
|
|
343
|
+
|
|
344
|
+
// Build lane number β tmux prefix mapping from batch state
|
|
345
|
+
const laneToPrefix = {};
|
|
346
|
+
if (batchState && batchState.lanes) {
|
|
347
|
+
for (const lane of batchState.lanes) {
|
|
348
|
+
if (lane.laneNumber != null && lane.tmuxSessionName) {
|
|
349
|
+
laneToPrefix[lane.laneNumber] = lane.tmuxSessionName;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Scan telemetry directory for JSONL files
|
|
355
|
+
let files;
|
|
356
|
+
try {
|
|
357
|
+
files = fs.readdirSync(telemetryDir).filter(f => f.endsWith(".jsonl"));
|
|
358
|
+
} catch {
|
|
359
|
+
// .pi/telemetry/ may not exist (pre-RPC sessions) β degrade gracefully
|
|
360
|
+
return result;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Track which files still exist for tail-state cleanup
|
|
364
|
+
const currentFiles = new Set();
|
|
365
|
+
// Track current fileβprefix mapping to detect file rotation
|
|
366
|
+
const currentPrefixFiles = new Map(); // prefix β Set<filePath>
|
|
367
|
+
|
|
368
|
+
for (const file of files) {
|
|
369
|
+
const filePath = path.join(telemetryDir, file);
|
|
370
|
+
currentFiles.add(filePath);
|
|
371
|
+
|
|
372
|
+
// Parse filename to get lane number and role
|
|
373
|
+
const parsed = parseTelemetryFilename(file);
|
|
374
|
+
if (!parsed) continue;
|
|
375
|
+
|
|
376
|
+
// Determine the key (tmux prefix)
|
|
377
|
+
let prefix;
|
|
378
|
+
if (parsed.laneNumber != null && laneToPrefix[parsed.laneNumber]) {
|
|
379
|
+
prefix = laneToPrefix[parsed.laneNumber];
|
|
380
|
+
} else if (parsed.laneNumber != null) {
|
|
381
|
+
// Lane number found but no batch-state mapping β use heuristic
|
|
382
|
+
prefix = `orch-lane-${parsed.laneNumber}`;
|
|
383
|
+
} else {
|
|
384
|
+
// Standalone /task mode
|
|
385
|
+
prefix = "standalone";
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Track fileβprefix mapping
|
|
389
|
+
if (!currentPrefixFiles.has(prefix)) currentPrefixFiles.set(prefix, new Set());
|
|
390
|
+
currentPrefixFiles.get(prefix).add(filePath);
|
|
391
|
+
|
|
392
|
+
// Check if file set for this prefix has changed (file rotation)
|
|
393
|
+
const prevFiles = telemetryPrefixFiles.get(prefix);
|
|
394
|
+
const isNewFile = !prevFiles || !prevFiles.has(filePath);
|
|
395
|
+
|
|
396
|
+
// Initialize persistent accumulator for this prefix if needed,
|
|
397
|
+
// or reset if files changed (new file appeared for same prefix)
|
|
398
|
+
if (!telemetryAccumulators.has(prefix) || (isNewFile && !telemetryTailStates.has(filePath))) {
|
|
399
|
+
const fresh = {
|
|
400
|
+
inputTokens: 0, outputTokens: 0, cacheReadTokens: 0,
|
|
401
|
+
cacheWriteTokens: 0, cost: 0, toolCalls: 0,
|
|
402
|
+
lastTool: "", retries: 0, retryActive: false,
|
|
403
|
+
lastRetryError: "", compactions: 0, latestTotalTokens: 0,
|
|
404
|
+
};
|
|
405
|
+
telemetryAccumulators.set(prefix, fresh);
|
|
406
|
+
// Also reset tail states for ALL files of this prefix to re-read from beginning
|
|
407
|
+
if (prevFiles) {
|
|
408
|
+
for (const pf of prevFiles) {
|
|
409
|
+
telemetryTailStates.delete(pf);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const acc = telemetryAccumulators.get(prefix);
|
|
415
|
+
result[prefix] = acc; // expose the persistent accumulator in the result
|
|
416
|
+
|
|
417
|
+
// Tail the file for new events
|
|
418
|
+
const events = tailJsonlFile(filePath);
|
|
419
|
+
|
|
420
|
+
// Check if file was truncated β reset accumulator
|
|
421
|
+
const ts = telemetryTailStates.get(filePath);
|
|
422
|
+
if (ts && ts.wasReset) {
|
|
423
|
+
acc.inputTokens = 0; acc.outputTokens = 0; acc.cacheReadTokens = 0;
|
|
424
|
+
acc.cacheWriteTokens = 0; acc.cost = 0; acc.toolCalls = 0;
|
|
425
|
+
acc.lastTool = ""; acc.retries = 0; acc.retryActive = false;
|
|
426
|
+
acc.lastRetryError = ""; acc.compactions = 0; acc.latestTotalTokens = 0;
|
|
427
|
+
ts.wasReset = false;
|
|
428
|
+
}
|
|
429
|
+
for (const event of events) {
|
|
430
|
+
switch (event.type) {
|
|
431
|
+
case "message_end": {
|
|
432
|
+
const usage = event.message?.usage;
|
|
433
|
+
if (usage) {
|
|
434
|
+
acc.inputTokens += usage.input || 0;
|
|
435
|
+
acc.outputTokens += usage.output || 0;
|
|
436
|
+
acc.cacheReadTokens += usage.cacheRead || 0;
|
|
437
|
+
acc.cacheWriteTokens += usage.cacheWrite || 0;
|
|
438
|
+
if (usage.cost) {
|
|
439
|
+
acc.cost += typeof usage.cost === "object"
|
|
440
|
+
? (usage.cost.total || 0)
|
|
441
|
+
: (typeof usage.cost === "number" ? usage.cost : 0);
|
|
442
|
+
}
|
|
443
|
+
const totalTokens = usage.totalTokens
|
|
444
|
+
|| ((usage.input || 0) + (usage.output || 0));
|
|
445
|
+
if (totalTokens > acc.latestTotalTokens) {
|
|
446
|
+
acc.latestTotalTokens = totalTokens;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
case "tool_execution_start": {
|
|
452
|
+
acc.toolCalls++;
|
|
453
|
+
const toolDesc = event.toolName || "unknown";
|
|
454
|
+
let argPreview = "";
|
|
455
|
+
if (event.args) {
|
|
456
|
+
if (typeof event.args === "string") {
|
|
457
|
+
argPreview = event.args.slice(0, 80);
|
|
458
|
+
} else if (typeof event.args === "object") {
|
|
459
|
+
const firstVal = Object.values(event.args)[0];
|
|
460
|
+
if (typeof firstVal === "string") {
|
|
461
|
+
argPreview = firstVal.slice(0, 80);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
acc.lastTool = argPreview ? `${toolDesc} ${argPreview}` : toolDesc;
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
468
|
+
case "auto_retry_start": {
|
|
469
|
+
acc.retries++;
|
|
470
|
+
acc.retryActive = true;
|
|
471
|
+
acc.lastRetryError = event.errorMessage || event.error || "unknown";
|
|
472
|
+
break;
|
|
473
|
+
}
|
|
474
|
+
case "auto_retry_end": {
|
|
475
|
+
acc.retryActive = false;
|
|
476
|
+
break;
|
|
477
|
+
}
|
|
478
|
+
case "auto_compaction_start": {
|
|
479
|
+
acc.compactions++;
|
|
480
|
+
break;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Clean up tail states for files that no longer exist
|
|
487
|
+
for (const [filePath] of telemetryTailStates) {
|
|
488
|
+
if (filePath.startsWith(telemetryDir) && !currentFiles.has(filePath)) {
|
|
489
|
+
telemetryTailStates.delete(filePath);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Update prefixβfiles tracking for next call
|
|
494
|
+
// Clean up accumulators and tracking for prefixes that have no remaining files
|
|
495
|
+
const activePrefixes = new Set(Object.keys(result));
|
|
496
|
+
for (const [prefix] of telemetryAccumulators) {
|
|
497
|
+
if (!activePrefixes.has(prefix)) {
|
|
498
|
+
telemetryAccumulators.delete(prefix);
|
|
499
|
+
telemetryPrefixFiles.delete(prefix);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
// Store current file mappings for next call's rotation detection
|
|
503
|
+
for (const [prefix, fileSet] of currentPrefixFiles) {
|
|
504
|
+
telemetryPrefixFiles.set(prefix, fileSet);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
return result;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Compute batch total cost from lane states (primary) and telemetry (supplementary).
|
|
512
|
+
* Lane states are authoritative β telemetry provides additional data only for lanes
|
|
513
|
+
* that have no lane-state entry (e.g., very early in session startup).
|
|
514
|
+
*/
|
|
515
|
+
function computeBatchTotalCost(laneStates, telemetry) {
|
|
516
|
+
let totalCost = 0;
|
|
517
|
+
const coveredPrefixes = new Set();
|
|
518
|
+
|
|
519
|
+
// Primary: sum cost from lane states
|
|
520
|
+
for (const [prefix, ls] of Object.entries(laneStates)) {
|
|
521
|
+
if (ls.workerCostUsd) {
|
|
522
|
+
totalCost += ls.workerCostUsd;
|
|
523
|
+
coveredPrefixes.add(prefix);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Supplementary: add cost from telemetry for uncovered lanes only
|
|
528
|
+
for (const [prefix, tel] of Object.entries(telemetry)) {
|
|
529
|
+
if (!coveredPrefixes.has(prefix) && tel.cost > 0) {
|
|
530
|
+
totalCost += tel.cost;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return totalCost;
|
|
535
|
+
}
|
|
536
|
+
|
|
216
537
|
/** Build full dashboard state object for the frontend. */
|
|
217
538
|
function buildDashboardState() {
|
|
218
539
|
const state = loadBatchState();
|
|
219
540
|
const tmuxSessions = getTmuxSessions();
|
|
220
541
|
const laneStates = loadLaneStates();
|
|
542
|
+
const telemetry = loadTelemetryData(state);
|
|
543
|
+
const batchTotalCost = computeBatchTotalCost(laneStates, telemetry);
|
|
221
544
|
|
|
222
545
|
if (!state) {
|
|
223
|
-
return { batch: null, tmuxSessions, laneStates: {}, timestamp: Date.now() };
|
|
546
|
+
return { batch: null, tmuxSessions, laneStates: {}, telemetry: {}, batchTotalCost: 0, timestamp: Date.now() };
|
|
224
547
|
}
|
|
225
548
|
|
|
226
549
|
const tasks = (state.tasks || []).map((task) => {
|
|
@@ -237,6 +560,8 @@ function buildDashboardState() {
|
|
|
237
560
|
|
|
238
561
|
return {
|
|
239
562
|
laneStates,
|
|
563
|
+
telemetry,
|
|
564
|
+
batchTotalCost,
|
|
240
565
|
batch: {
|
|
241
566
|
batchId: state.batchId,
|
|
242
567
|
phase: state.phase,
|