taskplane 0.23.4 → 0.23.6

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) => {
@@ -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 ?? {},
@@ -1680,9 +1706,11 @@ export function presentBatchSummary(
1680
1706
  const duration = batchState.endedAt && batchState.startedAt
1681
1707
  ? formatDurationMs(batchState.endedAt - batchState.startedAt)
1682
1708
  : "in progress";
1683
- const cost = (diagnostics?.batchCost ?? 0) > 0
1684
- ? `$${(diagnostics?.batchCost ?? 0).toFixed(2)}`
1685
- : "not tracked";
1709
+ // TP-115: Use V2 lane snapshot cost when diagnostics.batchCost is zero
1710
+ const rawCost = (diagnostics?.batchCost ?? 0) > 0
1711
+ ? diagnostics!.batchCost
1712
+ : computeV2BatchCost(stateRoot, batchState.batchId);
1713
+ const cost = rawCost > 0 ? `$${rawCost.toFixed(2)}` : "not tracked";
1686
1714
  const filename = `${opId}-${batchState.batchId}-summary.md`;
1687
1715
 
1688
1716
  const conciseText =
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskplane",
3
- "version": "0.23.4",
3
+ "version": "0.23.6",
4
4
  "description": "AI agent orchestration for pi — parallel task execution with checkpoint discipline",
5
5
  "keywords": [
6
6
  "pi-package",