taskplane 0.24.1 → 0.24.3
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 +9 -4
- package/dashboard/server.cjs +38 -20
- package/extensions/taskplane/agent-bridge-extension.ts +151 -13
- package/extensions/taskplane/config-schema.ts +1 -1
- package/extensions/taskplane/execution.ts +1 -1
- package/extensions/taskplane/lane-runner.ts +138 -56
- package/package.json +1 -1
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,
|
|
@@ -610,7 +610,7 @@ export const DEFAULT_ORCHESTRATOR_SECTION: OrchestratorSection = {
|
|
|
610
610
|
onTaskFailure: "skip-dependents",
|
|
611
611
|
onMergeFailure: "pause",
|
|
612
612
|
stallTimeout: 30,
|
|
613
|
-
maxWorkerMinutes:
|
|
613
|
+
maxWorkerMinutes: 120,
|
|
614
614
|
abortGracePeriod: 60,
|
|
615
615
|
},
|
|
616
616
|
monitoring: {
|
|
@@ -2214,7 +2214,7 @@ export async function executeLaneV2(
|
|
|
2214
2214
|
projectName: config.project?.name || "project",
|
|
2215
2215
|
maxIterations: 20,
|
|
2216
2216
|
noProgressLimit: 3,
|
|
2217
|
-
maxWorkerMinutes: config.failure?.maxWorkerMinutes ||
|
|
2217
|
+
maxWorkerMinutes: config.failure?.maxWorkerMinutes || 120,
|
|
2218
2218
|
warnPercent: 85,
|
|
2219
2219
|
killPercent: 95,
|
|
2220
2220
|
onSupervisorAlert,
|
|
@@ -283,27 +283,43 @@ 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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
289
|
+
try {
|
|
290
|
+
// Context pressure check
|
|
291
|
+
if (telemetry.contextUsage) {
|
|
292
|
+
const pct = telemetry.contextUsage.percent;
|
|
293
|
+
if (pct >= config.warnPercent) {
|
|
294
|
+
const msg = `Wrap up (context ${Math.round(pct)}%)`;
|
|
295
|
+
if (!existsSync(wrapUpFile)) writeFileSync(wrapUpFile, msg);
|
|
296
|
+
}
|
|
297
|
+
if (pct >= config.killPercent) {
|
|
298
|
+
workerKillReason = "context";
|
|
299
|
+
spawned.kill();
|
|
300
|
+
}
|
|
298
301
|
}
|
|
299
|
-
}
|
|
300
302
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
303
|
+
iterationTelemetry = telemetry;
|
|
304
|
+
lastTelemetry = telemetry;
|
|
305
|
+
// Emit lane snapshot
|
|
306
|
+
emitSnapshot(config, taskId, "running", telemetry, statusPath);
|
|
307
|
+
} catch { /* non-fatal: telemetry callback must never crash the engine */ }
|
|
304
308
|
});
|
|
305
309
|
|
|
306
|
-
|
|
310
|
+
// Reviewer telemetry is written by the worker bridge during review_step.
|
|
311
|
+
// Poll snapshot refresh independently from worker message_end cadence so
|
|
312
|
+
// the dashboard sees reviewer activity while tool calls are in-flight.
|
|
313
|
+
const reviewerRefresh = setInterval(() => {
|
|
314
|
+
try { emitSnapshot(config, taskId, "running", iterationTelemetry, statusPath); } catch { /* non-fatal */ }
|
|
315
|
+
}, 1000);
|
|
316
|
+
|
|
317
|
+
let workerResult: AgentHostResult;
|
|
318
|
+
try {
|
|
319
|
+
workerResult = await spawned.promise;
|
|
320
|
+
} finally {
|
|
321
|
+
clearInterval(reviewerRefresh);
|
|
322
|
+
}
|
|
307
323
|
|
|
308
324
|
// TP-115: Update lastTelemetry with definitive final values from AgentHostResult
|
|
309
325
|
lastTelemetry = workerResult;
|
|
@@ -538,6 +554,65 @@ function makeResult(
|
|
|
538
554
|
return result;
|
|
539
555
|
}
|
|
540
556
|
|
|
557
|
+
/** Max age for reviewer state file before it's considered stale (2 minutes). */
|
|
558
|
+
const REVIEWER_STATE_STALE_MS = 120_000;
|
|
559
|
+
|
|
560
|
+
export function readReviewerTelemetrySnapshot(
|
|
561
|
+
config: LaneRunnerConfig,
|
|
562
|
+
statusPath: string,
|
|
563
|
+
): (RuntimeAgentTelemetrySnapshot & { reviewType?: string; reviewStep?: number }) | null {
|
|
564
|
+
const reviewerPath = join(dirname(statusPath), ".reviewer-state.json");
|
|
565
|
+
if (!existsSync(reviewerPath)) return null;
|
|
566
|
+
|
|
567
|
+
try {
|
|
568
|
+
const raw = readFileSync(reviewerPath, "utf-8");
|
|
569
|
+
const parsed = JSON.parse(raw) as Partial<{
|
|
570
|
+
status: string;
|
|
571
|
+
elapsedMs: number;
|
|
572
|
+
toolCalls: number;
|
|
573
|
+
contextPct: number;
|
|
574
|
+
costUsd: number;
|
|
575
|
+
lastTool: string;
|
|
576
|
+
inputTokens: number;
|
|
577
|
+
outputTokens: number;
|
|
578
|
+
cacheReadTokens: number;
|
|
579
|
+
cacheWriteTokens: number;
|
|
580
|
+
updatedAt: number;
|
|
581
|
+
reviewType: string;
|
|
582
|
+
reviewStep: number;
|
|
583
|
+
}>;
|
|
584
|
+
|
|
585
|
+
if (parsed.status !== "running") return null;
|
|
586
|
+
|
|
587
|
+
// Stale guard: if updatedAt is present and older than threshold, ignore
|
|
588
|
+
if (parsed.updatedAt && (Date.now() - parsed.updatedAt) > REVIEWER_STATE_STALE_MS) return null;
|
|
589
|
+
|
|
590
|
+
return {
|
|
591
|
+
agentId: buildRuntimeAgentId(config.agentIdPrefix, config.laneNumber, "reviewer"),
|
|
592
|
+
status: "running",
|
|
593
|
+
elapsedMs: Number.isFinite(parsed.elapsedMs) ? Number(parsed.elapsedMs) : 0,
|
|
594
|
+
toolCalls: Number.isFinite(parsed.toolCalls) ? Number(parsed.toolCalls) : 0,
|
|
595
|
+
contextPct: Number.isFinite(parsed.contextPct) ? Number(parsed.contextPct) : 0,
|
|
596
|
+
costUsd: Number.isFinite(parsed.costUsd) ? Number(parsed.costUsd) : 0,
|
|
597
|
+
lastTool: typeof parsed.lastTool === "string" ? parsed.lastTool : "",
|
|
598
|
+
inputTokens: Number.isFinite(parsed.inputTokens) ? Number(parsed.inputTokens) : 0,
|
|
599
|
+
outputTokens: Number.isFinite(parsed.outputTokens) ? Number(parsed.outputTokens) : 0,
|
|
600
|
+
cacheReadTokens: Number.isFinite(parsed.cacheReadTokens) ? Number(parsed.cacheReadTokens) : 0,
|
|
601
|
+
cacheWriteTokens: Number.isFinite(parsed.cacheWriteTokens) ? Number(parsed.cacheWriteTokens) : 0,
|
|
602
|
+
reviewType: typeof parsed.reviewType === "string" ? parsed.reviewType : undefined,
|
|
603
|
+
reviewStep: Number.isFinite(parsed.reviewStep) ? Number(parsed.reviewStep) : undefined,
|
|
604
|
+
};
|
|
605
|
+
} catch {
|
|
606
|
+
return null;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Emit a lane snapshot to disk. NON-THROWING by contract — all errors are
|
|
612
|
+
* caught and logged. This function is called from setInterval callbacks
|
|
613
|
+
* and onTelemetry callbacks where an unhandled throw would trigger
|
|
614
|
+
* uncaughtException and crash the engine-worker process.
|
|
615
|
+
*/
|
|
541
616
|
function emitSnapshot(
|
|
542
617
|
config: LaneRunnerConfig,
|
|
543
618
|
taskId: string,
|
|
@@ -545,49 +620,56 @@ function emitSnapshot(
|
|
|
545
620
|
telemetry: Partial<AgentHostResult>,
|
|
546
621
|
statusPath: string,
|
|
547
622
|
): void {
|
|
548
|
-
// Parse progress from STATUS.md
|
|
549
|
-
let progress: RuntimeTaskProgress | null = null;
|
|
550
623
|
try {
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
624
|
+
// Parse progress from STATUS.md
|
|
625
|
+
let progress: RuntimeTaskProgress | null = null;
|
|
626
|
+
try {
|
|
627
|
+
const content = readFileSync(statusPath, "utf-8");
|
|
628
|
+
const parsed = parseStatusMd(content);
|
|
629
|
+
const currentStepMatch = content.match(/\*\*Current Step:\*\*\s*(.+)/);
|
|
630
|
+
const checked = parsed.steps.reduce((sum, s) => sum + s.totalChecked, 0);
|
|
631
|
+
const total = parsed.steps.reduce((sum, s) => sum + s.totalItems, 0);
|
|
632
|
+
progress = {
|
|
633
|
+
currentStep: currentStepMatch?.[1]?.trim() || "Unknown",
|
|
634
|
+
checked,
|
|
635
|
+
total,
|
|
636
|
+
iteration: parsed.iteration,
|
|
637
|
+
reviews: parsed.reviewCounter,
|
|
638
|
+
};
|
|
639
|
+
} catch { /* best effort */ }
|
|
640
|
+
|
|
641
|
+
const reviewerSnapshot = readReviewerTelemetrySnapshot(config, statusPath);
|
|
642
|
+
|
|
643
|
+
const snapshot: RuntimeLaneSnapshot = {
|
|
644
|
+
batchId: config.batchId,
|
|
645
|
+
laneNumber: config.laneNumber,
|
|
646
|
+
laneId: `lane-${config.laneNumber}`,
|
|
647
|
+
repoId: config.repoId,
|
|
648
|
+
taskId,
|
|
649
|
+
segmentId: null,
|
|
650
|
+
status,
|
|
651
|
+
worker: {
|
|
652
|
+
agentId: buildRuntimeAgentId(config.agentIdPrefix, config.laneNumber, "worker"),
|
|
653
|
+
status: mapLaneSnapshotStatusToWorkerStatus(status),
|
|
654
|
+
elapsedMs: telemetry.durationMs ?? 0,
|
|
655
|
+
toolCalls: telemetry.toolCalls ?? 0,
|
|
656
|
+
contextPct: telemetry.contextUsage?.percent ?? 0,
|
|
657
|
+
costUsd: telemetry.costUsd ?? 0,
|
|
658
|
+
lastTool: telemetry.lastTool ?? "",
|
|
659
|
+
inputTokens: telemetry.inputTokens ?? 0,
|
|
660
|
+
outputTokens: telemetry.outputTokens ?? 0,
|
|
661
|
+
cacheReadTokens: telemetry.cacheReadTokens ?? 0,
|
|
662
|
+
cacheWriteTokens: telemetry.cacheWriteTokens ?? 0,
|
|
663
|
+
},
|
|
664
|
+
reviewer: reviewerSnapshot,
|
|
665
|
+
progress,
|
|
666
|
+
updatedAt: Date.now(),
|
|
562
667
|
};
|
|
563
|
-
} catch { /* best effort */ }
|
|
564
|
-
|
|
565
|
-
const snapshot: RuntimeLaneSnapshot = {
|
|
566
|
-
batchId: config.batchId,
|
|
567
|
-
laneNumber: config.laneNumber,
|
|
568
|
-
laneId: `lane-${config.laneNumber}`,
|
|
569
|
-
repoId: config.repoId,
|
|
570
|
-
taskId,
|
|
571
|
-
segmentId: null,
|
|
572
|
-
status,
|
|
573
|
-
worker: {
|
|
574
|
-
agentId: buildRuntimeAgentId(config.agentIdPrefix, config.laneNumber, "worker"),
|
|
575
|
-
status: mapLaneSnapshotStatusToWorkerStatus(status),
|
|
576
|
-
elapsedMs: telemetry.durationMs ?? 0,
|
|
577
|
-
toolCalls: telemetry.toolCalls ?? 0,
|
|
578
|
-
contextPct: telemetry.contextUsage?.percent ?? 0,
|
|
579
|
-
costUsd: telemetry.costUsd ?? 0,
|
|
580
|
-
lastTool: telemetry.lastTool ?? "",
|
|
581
|
-
inputTokens: telemetry.inputTokens ?? 0,
|
|
582
|
-
outputTokens: telemetry.outputTokens ?? 0,
|
|
583
|
-
cacheReadTokens: telemetry.cacheReadTokens ?? 0,
|
|
584
|
-
cacheWriteTokens: telemetry.cacheWriteTokens ?? 0,
|
|
585
|
-
},
|
|
586
|
-
reviewer: null,
|
|
587
|
-
progress,
|
|
588
|
-
updatedAt: Date.now(),
|
|
589
|
-
};
|
|
590
668
|
|
|
591
|
-
|
|
669
|
+
writeLaneSnapshot(config.stateRoot, config.batchId, config.laneNumber, snapshot as any);
|
|
670
|
+
} catch {
|
|
671
|
+
// Non-fatal: snapshot is telemetry, not execution-critical.
|
|
672
|
+
// Swallow to prevent uncaughtException crash in setInterval/callback contexts.
|
|
673
|
+
}
|
|
592
674
|
}
|
|
593
675
|
|