taskplane 0.24.1 → 0.24.2
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/dashboard/public/app.js
CHANGED
|
@@ -101,6 +101,11 @@ function mergeV2LaneSnapshot(legacyLs, v2snap) {
|
|
|
101
101
|
return base;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
function isReviewerActiveForTask(ls, task) {
|
|
105
|
+
if (!ls || !task) return false;
|
|
106
|
+
return !!(ls.reviewerStatus === "running" && task.status === "running" && (!ls.taskId || ls.taskId === task.taskId));
|
|
107
|
+
}
|
|
108
|
+
|
|
104
109
|
/** Build a compact token summary string from lane state sidecar data.
|
|
105
110
|
* Display: ↑total_input ↓output (cost)
|
|
106
111
|
* Anthropic splits input into: uncached `input` + `cacheRead`.
|
|
@@ -612,10 +617,10 @@ function renderLanesTasks(batch, tmuxSessions) {
|
|
|
612
617
|
|
|
613
618
|
// Worker stats from lane state sidecar + telemetry badges
|
|
614
619
|
let workerHtml = "";
|
|
615
|
-
// Reviewer sub-row should only appear under the task
|
|
616
|
-
//
|
|
617
|
-
//
|
|
618
|
-
const reviewerActive = ls
|
|
620
|
+
// Reviewer sub-row should only appear under the active running task in this lane.
|
|
621
|
+
// Runtime V2 snapshots provide taskId; during early startup it can be briefly unset,
|
|
622
|
+
// so allow a task-status fallback while still avoiding duplicate rows.
|
|
623
|
+
const reviewerActive = isReviewerActiveForTask(ls, task);
|
|
619
624
|
const telemBadges = task.status !== "pending" ? telemetryBadgesHtml(tel, reviewerActive) : "";
|
|
620
625
|
if (ls && ls.workerStatus === "running" && task.status === "running") {
|
|
621
626
|
const elapsed = ls.workerElapsed ? `${Math.round(ls.workerElapsed / 1000)}s` : "";
|
package/dashboard/server.cjs
CHANGED
|
@@ -997,6 +997,43 @@ function computeBatchTotalCost(laneStates, telemetry) {
|
|
|
997
997
|
return totalCost;
|
|
998
998
|
}
|
|
999
999
|
|
|
1000
|
+
function synthesizeLaneStateFromSnapshot(key, snap, fallbackBatchId) {
|
|
1001
|
+
const w = snap.worker || {};
|
|
1002
|
+
const r = snap.reviewer || null;
|
|
1003
|
+
const statusMap = { running: "running", spawning: "running", exited: "done", crashed: "error", killed: "error", timed_out: "error", wrapping_up: "running" };
|
|
1004
|
+
const reviewerStatusMap = { running: "running", spawning: "running", wrapping_up: "running", exited: "done", crashed: "done", killed: "done", timed_out: "done" };
|
|
1005
|
+
|
|
1006
|
+
return {
|
|
1007
|
+
prefix: key,
|
|
1008
|
+
taskId: snap.taskId || null,
|
|
1009
|
+
phase: snap.status === "running" ? "worker-active" : snap.status === "complete" ? "complete" : "idle",
|
|
1010
|
+
workerStatus: statusMap[w.status] || w.status || "idle",
|
|
1011
|
+
workerElapsed: w.elapsedMs || 0,
|
|
1012
|
+
workerContextPct: w.contextPct || 0,
|
|
1013
|
+
workerLastTool: w.lastTool || "",
|
|
1014
|
+
workerToolCount: w.toolCalls || 0,
|
|
1015
|
+
workerInputTokens: w.inputTokens || 0,
|
|
1016
|
+
workerOutputTokens: w.outputTokens || 0,
|
|
1017
|
+
workerCacheReadTokens: w.cacheReadTokens || 0,
|
|
1018
|
+
workerCacheWriteTokens: w.cacheWriteTokens || 0,
|
|
1019
|
+
workerCostUsd: w.costUsd || 0,
|
|
1020
|
+
reviewerStatus: r ? (reviewerStatusMap[r.status] || r.status || "running") : "idle",
|
|
1021
|
+
reviewerElapsed: r?.elapsedMs || 0,
|
|
1022
|
+
reviewerContextPct: r?.contextPct || 0,
|
|
1023
|
+
reviewerLastTool: r?.lastTool || "",
|
|
1024
|
+
reviewerToolCount: r?.toolCalls || 0,
|
|
1025
|
+
reviewerCostUsd: r?.costUsd || 0,
|
|
1026
|
+
reviewerInputTokens: r?.inputTokens || 0,
|
|
1027
|
+
reviewerOutputTokens: r?.outputTokens || 0,
|
|
1028
|
+
reviewerCacheReadTokens: r?.cacheReadTokens || 0,
|
|
1029
|
+
reviewerCacheWriteTokens: r?.cacheWriteTokens || 0,
|
|
1030
|
+
reviewerType: r?.reviewType || "",
|
|
1031
|
+
reviewerStep: r?.reviewStep || 0,
|
|
1032
|
+
batchId: snap.batchId || fallbackBatchId,
|
|
1033
|
+
timestamp: snap.updatedAt || Date.now(),
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1000
1037
|
/** Build full dashboard state object for the frontend. */
|
|
1001
1038
|
function buildDashboardState() {
|
|
1002
1039
|
const state = loadBatchState();
|
|
@@ -1046,26 +1083,7 @@ function buildDashboardState() {
|
|
|
1046
1083
|
const laneRec = (state.lanes || []).find(l => l.laneNumber === Number(laneNum));
|
|
1047
1084
|
const key = laneRec ? (laneRec.laneSessionId) : `lane-${laneNum}`;
|
|
1048
1085
|
if (!laneStates[key] || (snap.updatedAt && snap.updatedAt > (laneStates[key].timestamp || 0))) {
|
|
1049
|
-
|
|
1050
|
-
const statusMap = { running: "running", spawning: "running", exited: "done", crashed: "error", killed: "error", timed_out: "error", wrapping_up: "running" };
|
|
1051
|
-
laneStates[key] = {
|
|
1052
|
-
prefix: key,
|
|
1053
|
-
taskId: snap.taskId || null,
|
|
1054
|
-
phase: snap.status === "running" ? "worker-active" : snap.status === "complete" ? "complete" : "idle",
|
|
1055
|
-
workerStatus: statusMap[w.status] || w.status || "idle",
|
|
1056
|
-
workerElapsed: w.elapsedMs || 0,
|
|
1057
|
-
workerContextPct: w.contextPct || 0,
|
|
1058
|
-
workerLastTool: w.lastTool || "",
|
|
1059
|
-
workerToolCount: w.toolCalls || 0,
|
|
1060
|
-
workerInputTokens: w.inputTokens || 0,
|
|
1061
|
-
workerOutputTokens: w.outputTokens || 0,
|
|
1062
|
-
workerCacheReadTokens: w.cacheReadTokens || 0,
|
|
1063
|
-
workerCacheWriteTokens: w.cacheWriteTokens || 0,
|
|
1064
|
-
workerCostUsd: w.costUsd || 0,
|
|
1065
|
-
reviewerStatus: "idle",
|
|
1066
|
-
batchId: snap.batchId || state.batchId,
|
|
1067
|
-
timestamp: snap.updatedAt || Date.now(),
|
|
1068
|
-
};
|
|
1086
|
+
laneStates[key] = synthesizeLaneStateFromSnapshot(key, snap, state.batchId);
|
|
1069
1087
|
}
|
|
1070
1088
|
}
|
|
1071
1089
|
}
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
26
26
|
import { Type } from "@mariozechner/pi-ai";
|
|
27
|
-
import { writeFileSync, readFileSync, existsSync, mkdirSync, renameSync } from "fs";
|
|
27
|
+
import { writeFileSync, readFileSync, existsSync, mkdirSync, renameSync, unlinkSync } from "fs";
|
|
28
28
|
import { join, dirname } from "path";
|
|
29
29
|
import { spawn as nodeSpawn } from "child_process";
|
|
30
30
|
import { randomBytes } from "crypto";
|
|
@@ -216,11 +216,44 @@ export default function (pi: ExtensionAPI) {
|
|
|
216
216
|
return basePrompt;
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
+
function reviewerStatePath(taskFolder: string): string {
|
|
220
|
+
return join(taskFolder, ".reviewer-state.json");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function writeReviewerState(taskFolder: string, state: {
|
|
224
|
+
status: "running" | "done" | "error";
|
|
225
|
+
elapsedMs: number;
|
|
226
|
+
toolCalls: number;
|
|
227
|
+
contextPct: number;
|
|
228
|
+
costUsd: number;
|
|
229
|
+
lastTool: string;
|
|
230
|
+
inputTokens: number;
|
|
231
|
+
outputTokens: number;
|
|
232
|
+
cacheReadTokens: number;
|
|
233
|
+
cacheWriteTokens: number;
|
|
234
|
+
updatedAt: number;
|
|
235
|
+
reviewType?: string;
|
|
236
|
+
reviewStep?: number;
|
|
237
|
+
}): void {
|
|
238
|
+
const filePath = reviewerStatePath(taskFolder);
|
|
239
|
+
const tmpPath = filePath + ".tmp";
|
|
240
|
+
writeFileSync(tmpPath, JSON.stringify(state, null, 2) + "\n", "utf-8");
|
|
241
|
+
renameSync(tmpPath, filePath);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function removeReviewerState(taskFolder: string): void {
|
|
245
|
+
const filePath = reviewerStatePath(taskFolder);
|
|
246
|
+
if (!existsSync(filePath)) return;
|
|
247
|
+
try { unlinkSync(filePath); } catch { /* best effort */ }
|
|
248
|
+
}
|
|
249
|
+
|
|
219
250
|
/**
|
|
220
251
|
* Spawn a reviewer Pi subprocess and wait for it to complete.
|
|
221
252
|
* Returns the process exit code.
|
|
222
253
|
*/
|
|
223
|
-
function spawnReviewer(prompt: string, systemPrompt: string, cwd: string): Promise<number> {
|
|
254
|
+
function spawnReviewer(prompt: string, systemPrompt: string, cwd: string, taskFolder: string, reviewType?: string, reviewStep?: number): Promise<number> {
|
|
255
|
+
// Pre-clean stale reviewer state from prior interrupted review
|
|
256
|
+
removeReviewerState(taskFolder);
|
|
224
257
|
return new Promise((resolve) => {
|
|
225
258
|
const cliPath = resolvePiCli();
|
|
226
259
|
const args = [
|
|
@@ -235,23 +268,124 @@ export default function (pi: ExtensionAPI) {
|
|
|
235
268
|
env: { ...process.env },
|
|
236
269
|
});
|
|
237
270
|
|
|
238
|
-
|
|
271
|
+
const startedAt = Date.now();
|
|
272
|
+
let inputTokens = 0;
|
|
273
|
+
let outputTokens = 0;
|
|
274
|
+
let cacheReadTokens = 0;
|
|
275
|
+
let cacheWriteTokens = 0;
|
|
276
|
+
let costUsd = 0;
|
|
277
|
+
let toolCalls = 0;
|
|
278
|
+
let lastTool = "";
|
|
279
|
+
let contextPct = 0;
|
|
280
|
+
let stdoutBuf = "";
|
|
281
|
+
let finalized = false;
|
|
282
|
+
|
|
283
|
+
const emitState = (status: "running" | "done" | "error") => {
|
|
284
|
+
try {
|
|
285
|
+
writeReviewerState(taskFolder, {
|
|
286
|
+
status,
|
|
287
|
+
elapsedMs: Date.now() - startedAt,
|
|
288
|
+
toolCalls,
|
|
289
|
+
contextPct,
|
|
290
|
+
costUsd,
|
|
291
|
+
lastTool,
|
|
292
|
+
inputTokens,
|
|
293
|
+
outputTokens,
|
|
294
|
+
cacheReadTokens,
|
|
295
|
+
cacheWriteTokens,
|
|
296
|
+
updatedAt: Date.now(),
|
|
297
|
+
reviewType,
|
|
298
|
+
reviewStep,
|
|
299
|
+
});
|
|
300
|
+
} catch { /* best effort */ }
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
// Write initial "running" state immediately so dashboard shows
|
|
304
|
+
// the reviewer sub-row before the first message_end arrives.
|
|
305
|
+
emitState("running");
|
|
306
|
+
|
|
307
|
+
const closeStdin = () => {
|
|
308
|
+
setTimeout(() => {
|
|
309
|
+
try { proc.stdin?.end(); } catch { /* ignore */ }
|
|
310
|
+
}, 100);
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const finalize = (code: number) => {
|
|
314
|
+
if (finalized) return;
|
|
315
|
+
finalized = true;
|
|
316
|
+
emitState(code === 0 ? "done" : "error");
|
|
317
|
+
resolve(code);
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
const handleEvent = (event: any) => {
|
|
321
|
+
if (!event || typeof event.type !== "string") return;
|
|
322
|
+
switch (event.type) {
|
|
323
|
+
case "message_end": {
|
|
324
|
+
const usage = event.message?.usage;
|
|
325
|
+
if (usage) {
|
|
326
|
+
inputTokens += usage.input || 0;
|
|
327
|
+
outputTokens += usage.output || 0;
|
|
328
|
+
cacheReadTokens += usage.cacheRead || 0;
|
|
329
|
+
cacheWriteTokens += usage.cacheWrite || 0;
|
|
330
|
+
if (usage.cost) {
|
|
331
|
+
costUsd += typeof usage.cost === "object"
|
|
332
|
+
? (usage.cost.total || 0)
|
|
333
|
+
: (typeof usage.cost === "number" ? usage.cost : 0);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
emitState("running");
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
case "tool_execution_start": {
|
|
340
|
+
toolCalls++;
|
|
341
|
+
const toolName = event.toolName || "tool";
|
|
342
|
+
const argPreview = typeof event.args === "string"
|
|
343
|
+
? event.args.slice(0, 80)
|
|
344
|
+
: (event.args && typeof Object.values(event.args)[0] === "string"
|
|
345
|
+
? String(Object.values(event.args)[0]).slice(0, 80)
|
|
346
|
+
: "");
|
|
347
|
+
lastTool = argPreview ? `${toolName}: ${argPreview}` : toolName;
|
|
348
|
+
emitState("running");
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
case "response": {
|
|
352
|
+
const pct = event.success === true ? event.data?.contextUsage?.percent : undefined;
|
|
353
|
+
if (typeof pct === "number" && Number.isFinite(pct)) {
|
|
354
|
+
contextPct = pct;
|
|
355
|
+
}
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
case "agent_end": {
|
|
359
|
+
closeStdin();
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
// Send prompt immediately
|
|
239
366
|
proc.stdin?.write(JSON.stringify({ type: "prompt", message: prompt }) + "\n");
|
|
240
367
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
368
|
+
proc.stdout?.on("data", (chunk: Buffer | string) => {
|
|
369
|
+
stdoutBuf += typeof chunk === "string" ? chunk : chunk.toString("utf-8");
|
|
370
|
+
let idx = -1;
|
|
371
|
+
while ((idx = stdoutBuf.indexOf("\n")) >= 0) {
|
|
372
|
+
let line = stdoutBuf.slice(0, idx);
|
|
373
|
+
stdoutBuf = stdoutBuf.slice(idx + 1);
|
|
374
|
+
if (line.endsWith("\r")) line = line.slice(0, -1);
|
|
375
|
+
if (!line.trim()) continue;
|
|
376
|
+
let event: any;
|
|
377
|
+
try { event = JSON.parse(line); } catch { continue; }
|
|
378
|
+
handleEvent(event);
|
|
247
379
|
}
|
|
248
380
|
});
|
|
249
381
|
|
|
250
|
-
proc.on("close", (code) =>
|
|
251
|
-
proc.on("error", () =>
|
|
382
|
+
proc.on("close", (code) => finalize(code ?? 1));
|
|
383
|
+
proc.on("error", () => finalize(1));
|
|
252
384
|
|
|
253
385
|
// Timeout: 10 minutes
|
|
254
|
-
setTimeout(() => {
|
|
386
|
+
setTimeout(() => {
|
|
387
|
+
try { proc.kill("SIGTERM"); } catch { /* ignore */ }
|
|
388
|
+
}, 10 * 60 * 1000);
|
|
255
389
|
});
|
|
256
390
|
}
|
|
257
391
|
|
|
@@ -367,7 +501,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
367
501
|
|
|
368
502
|
try {
|
|
369
503
|
const systemPrompt = loadReviewerPrompt();
|
|
370
|
-
const exitCode = await spawnReviewer(reviewPrompt, systemPrompt, cwd);
|
|
504
|
+
const exitCode = await spawnReviewer(reviewPrompt, systemPrompt, cwd, taskFolder, reviewType, stepNum);
|
|
371
505
|
|
|
372
506
|
// Update review counter in STATUS.md
|
|
373
507
|
try {
|
|
@@ -395,6 +529,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
395
529
|
writeFileSync(statusPath, status.trimEnd() + "\n" + logEntry);
|
|
396
530
|
} catch { /* best effort */ }
|
|
397
531
|
|
|
532
|
+
removeReviewerState(taskFolder);
|
|
533
|
+
|
|
398
534
|
const reviewFile = `.reviews/R${num}-${reviewType}-step${stepNum}.md`;
|
|
399
535
|
if (verdict === "APPROVE") {
|
|
400
536
|
return { content: [{ type: "text" as const, text: `APPROVE` }], details: undefined };
|
|
@@ -408,9 +544,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
408
544
|
return { content: [{ type: "text" as const, text: `Review complete (verdict unclear). See ${reviewFile}` }], details: undefined };
|
|
409
545
|
}
|
|
410
546
|
} else {
|
|
547
|
+
removeReviewerState(taskFolder);
|
|
411
548
|
return { content: [{ type: "text" as const, text: `UNAVAILABLE — reviewer exited (code ${exitCode}) but produced no output.` }], details: undefined };
|
|
412
549
|
}
|
|
413
550
|
} catch (err) {
|
|
551
|
+
removeReviewerState(taskFolder);
|
|
414
552
|
return {
|
|
415
553
|
content: [{ type: "text" as const, text: `UNAVAILABLE — reviewer failed: ${err instanceof Error ? err.message : String(err)}` }],
|
|
416
554
|
details: undefined,
|
|
@@ -283,6 +283,7 @@ export async function executeTaskV2(
|
|
|
283
283
|
|
|
284
284
|
// Context pressure: write wrap-up signal before kill
|
|
285
285
|
let workerKillReason: "context" | "timer" | null = null;
|
|
286
|
+
let iterationTelemetry: Partial<AgentHostResult> = {};
|
|
286
287
|
|
|
287
288
|
const spawned = spawnAgent(hostOpts, undefined, (telemetry) => {
|
|
288
289
|
// Context pressure check
|
|
@@ -298,12 +299,25 @@ export async function executeTaskV2(
|
|
|
298
299
|
}
|
|
299
300
|
}
|
|
300
301
|
|
|
302
|
+
iterationTelemetry = telemetry;
|
|
301
303
|
lastTelemetry = telemetry;
|
|
302
304
|
// Emit lane snapshot
|
|
303
305
|
emitSnapshot(config, taskId, "running", telemetry, statusPath);
|
|
304
306
|
});
|
|
305
307
|
|
|
306
|
-
|
|
308
|
+
// Reviewer telemetry is written by the worker bridge during review_step.
|
|
309
|
+
// Poll snapshot refresh independently from worker message_end cadence so
|
|
310
|
+
// the dashboard sees reviewer activity while tool calls are in-flight.
|
|
311
|
+
const reviewerRefresh = setInterval(() => {
|
|
312
|
+
emitSnapshot(config, taskId, "running", iterationTelemetry, statusPath);
|
|
313
|
+
}, 1000);
|
|
314
|
+
|
|
315
|
+
let workerResult: AgentHostResult;
|
|
316
|
+
try {
|
|
317
|
+
workerResult = await spawned.promise;
|
|
318
|
+
} finally {
|
|
319
|
+
clearInterval(reviewerRefresh);
|
|
320
|
+
}
|
|
307
321
|
|
|
308
322
|
// TP-115: Update lastTelemetry with definitive final values from AgentHostResult
|
|
309
323
|
lastTelemetry = workerResult;
|
|
@@ -538,6 +552,59 @@ function makeResult(
|
|
|
538
552
|
return result;
|
|
539
553
|
}
|
|
540
554
|
|
|
555
|
+
/** Max age for reviewer state file before it's considered stale (2 minutes). */
|
|
556
|
+
const REVIEWER_STATE_STALE_MS = 120_000;
|
|
557
|
+
|
|
558
|
+
export function readReviewerTelemetrySnapshot(
|
|
559
|
+
config: LaneRunnerConfig,
|
|
560
|
+
statusPath: string,
|
|
561
|
+
): (RuntimeAgentTelemetrySnapshot & { reviewType?: string; reviewStep?: number }) | null {
|
|
562
|
+
const reviewerPath = join(dirname(statusPath), ".reviewer-state.json");
|
|
563
|
+
if (!existsSync(reviewerPath)) return null;
|
|
564
|
+
|
|
565
|
+
try {
|
|
566
|
+
const raw = readFileSync(reviewerPath, "utf-8");
|
|
567
|
+
const parsed = JSON.parse(raw) as Partial<{
|
|
568
|
+
status: string;
|
|
569
|
+
elapsedMs: number;
|
|
570
|
+
toolCalls: number;
|
|
571
|
+
contextPct: number;
|
|
572
|
+
costUsd: number;
|
|
573
|
+
lastTool: string;
|
|
574
|
+
inputTokens: number;
|
|
575
|
+
outputTokens: number;
|
|
576
|
+
cacheReadTokens: number;
|
|
577
|
+
cacheWriteTokens: number;
|
|
578
|
+
updatedAt: number;
|
|
579
|
+
reviewType: string;
|
|
580
|
+
reviewStep: number;
|
|
581
|
+
}>;
|
|
582
|
+
|
|
583
|
+
if (parsed.status !== "running") return null;
|
|
584
|
+
|
|
585
|
+
// Stale guard: if updatedAt is present and older than threshold, ignore
|
|
586
|
+
if (parsed.updatedAt && (Date.now() - parsed.updatedAt) > REVIEWER_STATE_STALE_MS) return null;
|
|
587
|
+
|
|
588
|
+
return {
|
|
589
|
+
agentId: buildRuntimeAgentId(config.agentIdPrefix, config.laneNumber, "reviewer"),
|
|
590
|
+
status: "running",
|
|
591
|
+
elapsedMs: Number.isFinite(parsed.elapsedMs) ? Number(parsed.elapsedMs) : 0,
|
|
592
|
+
toolCalls: Number.isFinite(parsed.toolCalls) ? Number(parsed.toolCalls) : 0,
|
|
593
|
+
contextPct: Number.isFinite(parsed.contextPct) ? Number(parsed.contextPct) : 0,
|
|
594
|
+
costUsd: Number.isFinite(parsed.costUsd) ? Number(parsed.costUsd) : 0,
|
|
595
|
+
lastTool: typeof parsed.lastTool === "string" ? parsed.lastTool : "",
|
|
596
|
+
inputTokens: Number.isFinite(parsed.inputTokens) ? Number(parsed.inputTokens) : 0,
|
|
597
|
+
outputTokens: Number.isFinite(parsed.outputTokens) ? Number(parsed.outputTokens) : 0,
|
|
598
|
+
cacheReadTokens: Number.isFinite(parsed.cacheReadTokens) ? Number(parsed.cacheReadTokens) : 0,
|
|
599
|
+
cacheWriteTokens: Number.isFinite(parsed.cacheWriteTokens) ? Number(parsed.cacheWriteTokens) : 0,
|
|
600
|
+
reviewType: typeof parsed.reviewType === "string" ? parsed.reviewType : undefined,
|
|
601
|
+
reviewStep: Number.isFinite(parsed.reviewStep) ? Number(parsed.reviewStep) : undefined,
|
|
602
|
+
};
|
|
603
|
+
} catch {
|
|
604
|
+
return null;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
541
608
|
function emitSnapshot(
|
|
542
609
|
config: LaneRunnerConfig,
|
|
543
610
|
taskId: string,
|
|
@@ -562,6 +629,8 @@ function emitSnapshot(
|
|
|
562
629
|
};
|
|
563
630
|
} catch { /* best effort */ }
|
|
564
631
|
|
|
632
|
+
const reviewerSnapshot = readReviewerTelemetrySnapshot(config, statusPath);
|
|
633
|
+
|
|
565
634
|
const snapshot: RuntimeLaneSnapshot = {
|
|
566
635
|
batchId: config.batchId,
|
|
567
636
|
laneNumber: config.laneNumber,
|
|
@@ -583,7 +652,7 @@ function emitSnapshot(
|
|
|
583
652
|
cacheReadTokens: telemetry.cacheReadTokens ?? 0,
|
|
584
653
|
cacheWriteTokens: telemetry.cacheWriteTokens ?? 0,
|
|
585
654
|
},
|
|
586
|
-
reviewer:
|
|
655
|
+
reviewer: reviewerSnapshot,
|
|
587
656
|
progress,
|
|
588
657
|
updatedAt: Date.now(),
|
|
589
658
|
};
|