taskplane 0.23.3 → 0.23.5

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.
@@ -2253,28 +2253,57 @@ export async function executeOrchBatch(
2253
2253
 
2254
2254
  // ── Save batch history (before cleanup deletes sidecar files) ────
2255
2255
  try {
2256
- // Read token data from sidecar files while they still exist
2256
+ // Read token data from sidecar files or V2 lane snapshots
2257
2257
  const piDir = join(stateRoot, ".pi");
2258
2258
  const laneTokens = new Map<string, TokenCounts>();
2259
+
2260
+ // TP-115: Try V2 lane snapshots first (authoritative for Runtime V2)
2259
2261
  try {
2260
- const files = readdirSync(piDir).filter(f => f.startsWith("lane-state-") && f.endsWith(".json"));
2261
- for (const f of files) {
2262
- try {
2263
- const raw = readFileSync(join(piDir, f), "utf-8").trim();
2264
- if (!raw) continue;
2265
- const data = JSON.parse(raw);
2266
- if (data.prefix) {
2267
- laneTokens.set(data.prefix, {
2268
- input: data.workerInputTokens || 0,
2269
- output: data.workerOutputTokens || 0,
2270
- cacheRead: data.workerCacheReadTokens || 0,
2271
- cacheWrite: data.workerCacheWriteTokens || 0,
2272
- costUsd: data.workerCostUsd || 0,
2262
+ const lanesDir = join(piDir, "runtime", batchState.batchId, "lanes");
2263
+ if (existsSync(lanesDir)) {
2264
+ const files = readdirSync(lanesDir).filter(f => f.startsWith("lane-") && f.endsWith(".json"));
2265
+ for (const f of files) {
2266
+ try {
2267
+ const snap = JSON.parse(readFileSync(join(lanesDir, f), "utf-8"));
2268
+ const w = snap.worker || {};
2269
+ const r = snap.reviewer || {};
2270
+ // Key by session name (match lane record) for per-task lookup
2271
+ const laneRec = batchState.lanes.find((l: { laneNumber: number }) => l.laneNumber === snap.laneNumber);
2272
+ const key = laneRec?.tmuxSessionName || `lane-${snap.laneNumber}`;
2273
+ laneTokens.set(key, {
2274
+ input: (w.inputTokens || 0) + (r.inputTokens || 0),
2275
+ output: (w.outputTokens || 0) + (r.outputTokens || 0),
2276
+ cacheRead: (w.cacheReadTokens || 0) + (r.cacheReadTokens || 0),
2277
+ cacheWrite: (w.cacheWriteTokens || 0) + (r.cacheWriteTokens || 0),
2278
+ costUsd: (w.costUsd || 0) + (r.costUsd || 0),
2273
2279
  });
2274
- }
2275
- } catch { /* skip invalid files */ }
2280
+ } catch { /* skip invalid files */ }
2281
+ }
2276
2282
  }
2277
- } catch { /* .pi dir may not exist */ }
2283
+ } catch { /* runtime dir may not exist */ }
2284
+
2285
+ // Legacy fallback: lane-state-*.json sidecars (only if V2 found nothing)
2286
+ if (laneTokens.size === 0) {
2287
+ try {
2288
+ const files = readdirSync(piDir).filter(f => f.startsWith("lane-state-") && f.endsWith(".json"));
2289
+ for (const f of files) {
2290
+ try {
2291
+ const raw = readFileSync(join(piDir, f), "utf-8").trim();
2292
+ if (!raw) continue;
2293
+ const data = JSON.parse(raw);
2294
+ if (data.prefix) {
2295
+ laneTokens.set(data.prefix, {
2296
+ input: data.workerInputTokens || 0,
2297
+ output: data.workerOutputTokens || 0,
2298
+ cacheRead: data.workerCacheReadTokens || 0,
2299
+ cacheWrite: data.workerCacheWriteTokens || 0,
2300
+ costUsd: data.workerCostUsd || 0,
2301
+ });
2302
+ }
2303
+ } catch { /* skip invalid files */ }
2304
+ }
2305
+ } catch { /* .pi dir may not exist */ }
2306
+ }
2278
2307
 
2279
2308
  // Build per-task summaries from allTaskOutcomes + wave plan
2280
2309
  const taskSummaries: BatchTaskSummary[] = allTaskOutcomes.map((to) => {
@@ -1758,13 +1758,19 @@ export async function resolveTaskMonitorState(
1758
1758
  // TP-115: Backend-aware liveness check.
1759
1759
  // V2: read the lane snapshot file written by lane-runner every second.
1760
1760
  // Snapshot status is authoritative — no PID probing needed.
1761
+ // If snapshot doesn't exist yet, assume alive (lane-runner startup race).
1761
1762
  // Legacy: check TMUX session.
1762
1763
  let sessionAlive: boolean;
1763
1764
  if (runtimeBackend === "v2" && v2Context) {
1764
1765
  const snap = readLaneSnapshot(v2Context.stateRoot, v2Context.batchId, v2Context.laneNumber);
1765
- sessionAlive = snap != null && snap.status === "running";
1766
+ if (snap == null) {
1767
+ // Snapshot not written yet — lane-runner is still starting up.
1768
+ // Assume alive to avoid false "failed" from monitor racing lane startup.
1769
+ sessionAlive = true;
1770
+ } else {
1771
+ sessionAlive = snap.status === "running";
1772
+ }
1766
1773
  } else if (runtimeBackend === "v2") {
1767
- // Fallback to registry-based check if no v2Context
1768
1774
  sessionAlive = isV2AgentAlive(sessionName, runtimeBackend);
1769
1775
  } else {
1770
1776
  sessionAlive = await tmuxHasSessionAsync(sessionName);
@@ -28,7 +28,7 @@
28
28
 
29
29
  import { join, dirname } from "path";
30
30
  import { fileURLToPath } from "url";
31
- import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync, renameSync, statSync, openSync, readSync, closeSync, appendFileSync } from "fs";
31
+ import { existsSync, readFileSync, readdirSync, writeFileSync, unlinkSync, mkdirSync, renameSync, statSync, openSync, readSync, closeSync, appendFileSync } from "fs";
32
32
  import { stat as fsStat, open as fsOpen, readFile as fsReadFile, writeFile as fsWriteFile, rename as fsRename } from "fs/promises";
33
33
  import { execFileSync } from "child_process";
34
34
  import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
@@ -1264,6 +1264,30 @@ function formatDurationMs(ms: number): string {
1264
1264
  *
1265
1265
  * @since TP-043
1266
1266
  */
1267
+
1268
+ /**
1269
+ * TP-115: Compute batch cost from V2 lane snapshots.
1270
+ * Reads .pi/runtime/{batchId}/lanes/*.json and sums worker + reviewer costUsd.
1271
+ * Returns 0 if no V2 data exists.
1272
+ * @since TP-115
1273
+ */
1274
+ function computeV2BatchCost(stateRoot: string, batchId: string): number {
1275
+ try {
1276
+ const lanesDir = join(stateRoot, ".pi", "runtime", batchId, "lanes");
1277
+ if (!existsSync(lanesDir)) return 0;
1278
+ const files = readdirSync(lanesDir).filter(f => f.startsWith("lane-") && f.endsWith(".json"));
1279
+ let total = 0;
1280
+ for (const f of files) {
1281
+ try {
1282
+ const snap = JSON.parse(readFileSync(join(lanesDir, f), "utf-8"));
1283
+ total += snap.worker?.costUsd || 0;
1284
+ total += snap.reviewer?.costUsd || 0;
1285
+ } catch { /* skip */ }
1286
+ }
1287
+ return total;
1288
+ } catch { return 0; }
1289
+ }
1290
+
1267
1291
  export function collectBatchSummaryData(
1268
1292
  batchState: OrchBatchRuntimeState,
1269
1293
  stateRoot: string,
@@ -1297,7 +1321,9 @@ export function collectBatchSummaryData(
1297
1321
  failedTasks: batchState.failedTasks,
1298
1322
  skippedTasks: batchState.skippedTasks,
1299
1323
  blockedTasks: batchState.blockedTasks,
1300
- batchCost: diagnostics?.batchCost ?? 0,
1324
+ batchCost: (diagnostics?.batchCost ?? 0) > 0
1325
+ ? diagnostics!.batchCost
1326
+ : computeV2BatchCost(stateRoot, batchState.batchId),
1301
1327
  wavePlan: [], // Not directly available on runtime state — use waveResults
1302
1328
  waveResults,
1303
1329
  taskExits: diagnostics?.taskExits ?? {},
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskplane",
3
- "version": "0.23.3",
3
+ "version": "0.23.5",
4
4
  "description": "AI agent orchestration for pi — parallel task execution with checkpoint discipline",
5
5
  "keywords": [
6
6
  "pi-package",