taskplane 0.23.12 → 0.23.14
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/extensions/taskplane/engine.ts +96 -39
- package/extensions/taskplane/execution.ts +100 -9
- package/extensions/taskplane/lane-runner.ts +14 -0
- package/extensions/taskplane/persistence.ts +43 -11
- package/extensions/taskplane/resume.ts +3 -0
- package/extensions/taskplane/types.ts +37 -0
- package/package.json +1 -1
|
@@ -64,6 +64,61 @@ function emitTier0Escalation(
|
|
|
64
64
|
});
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
/** Zero-token sentinel used for task/wave/batch aggregation. */
|
|
68
|
+
const ZERO_TOKENS: TokenCounts = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, costUsd: 0 };
|
|
69
|
+
|
|
70
|
+
/** Map embedded outcome telemetry to the batch-history TokenCounts shape. */
|
|
71
|
+
export function taskTokensFromOutcomeTelemetry(outcome: LaneTaskOutcome): TokenCounts {
|
|
72
|
+
const telemetry = outcome.telemetry;
|
|
73
|
+
if (!telemetry) return { ...ZERO_TOKENS };
|
|
74
|
+
return {
|
|
75
|
+
input: telemetry.inputTokens,
|
|
76
|
+
output: telemetry.outputTokens,
|
|
77
|
+
cacheRead: telemetry.cacheReadTokens,
|
|
78
|
+
cacheWrite: telemetry.cacheWriteTokens,
|
|
79
|
+
costUsd: telemetry.costUsd,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Resolve per-task token counts for batch history.
|
|
85
|
+
*
|
|
86
|
+
* Priority:
|
|
87
|
+
* 1) Embedded `LaneTaskOutcome.telemetry` (authoritative Runtime V2 path)
|
|
88
|
+
* 2) V2 lane snapshot fallback by numeric laneNumber (legacy outcomes)
|
|
89
|
+
* 3) Legacy lane-state sidecar keys by sessionName prefix
|
|
90
|
+
* 4) Zero tokens
|
|
91
|
+
*/
|
|
92
|
+
export function resolveBatchHistoryTaskTokens(
|
|
93
|
+
outcome: LaneTaskOutcome,
|
|
94
|
+
laneNumber: number,
|
|
95
|
+
v2LaneTokensByNumber: Map<number, TokenCounts>,
|
|
96
|
+
legacyLaneTokensByKey: Map<string, TokenCounts>,
|
|
97
|
+
): TokenCounts {
|
|
98
|
+
// Skipped tasks did not run an agent process.
|
|
99
|
+
if (outcome.status === "skipped") return { ...ZERO_TOKENS };
|
|
100
|
+
|
|
101
|
+
if (outcome.telemetry) {
|
|
102
|
+
return taskTokensFromOutcomeTelemetry(outcome);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (laneNumber > 0) {
|
|
106
|
+
const v2 = v2LaneTokensByNumber.get(laneNumber);
|
|
107
|
+
if (v2) return v2;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const bySession = legacyLaneTokensByKey.get(outcome.sessionName)
|
|
111
|
+
|| legacyLaneTokensByKey.get(outcome.sessionName?.replace(/-(?:worker|reviewer)$/, ""));
|
|
112
|
+
if (bySession) return bySession;
|
|
113
|
+
|
|
114
|
+
if (laneNumber > 0) {
|
|
115
|
+
const byLaneKey = legacyLaneTokensByKey.get(`lane-${laneNumber}`);
|
|
116
|
+
if (byLaneKey) return byLaneKey;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return { ...ZERO_TOKENS };
|
|
120
|
+
}
|
|
121
|
+
|
|
67
122
|
/**
|
|
68
123
|
* Attempt automatic retry for failed tasks with retryable exit classifications.
|
|
69
124
|
*
|
|
@@ -2253,11 +2308,13 @@ export async function executeOrchBatch(
|
|
|
2253
2308
|
|
|
2254
2309
|
// ── Save batch history (before cleanup deletes sidecar files) ────
|
|
2255
2310
|
try {
|
|
2256
|
-
// Read token data from
|
|
2311
|
+
// Read fallback token data from V2 lane snapshots and legacy sidecars.
|
|
2312
|
+
// Primary source for Runtime V2 is now `LaneTaskOutcome.telemetry`.
|
|
2257
2313
|
const piDir = join(stateRoot, ".pi");
|
|
2258
|
-
const
|
|
2314
|
+
const v2LaneTokensByNumber = new Map<number, TokenCounts>();
|
|
2315
|
+
const legacyLaneTokensByKey = new Map<string, TokenCounts>();
|
|
2259
2316
|
|
|
2260
|
-
//
|
|
2317
|
+
// V2 snapshot fallback (used only when outcome.telemetry is absent).
|
|
2261
2318
|
try {
|
|
2262
2319
|
const lanesDir = join(piDir, "runtime", batchState.batchId, "lanes");
|
|
2263
2320
|
if (existsSync(lanesDir)) {
|
|
@@ -2265,11 +2322,11 @@ export async function executeOrchBatch(
|
|
|
2265
2322
|
for (const f of files) {
|
|
2266
2323
|
try {
|
|
2267
2324
|
const snap = JSON.parse(readFileSync(join(lanesDir, f), "utf-8"));
|
|
2325
|
+
const laneNumber = typeof snap.laneNumber === "number" ? snap.laneNumber : 0;
|
|
2326
|
+
if (laneNumber <= 0) continue;
|
|
2268
2327
|
const w = snap.worker || {};
|
|
2269
2328
|
const r = snap.reviewer || {};
|
|
2270
|
-
|
|
2271
|
-
const key = `lane-${snap.laneNumber}`;
|
|
2272
|
-
laneTokens.set(key, {
|
|
2329
|
+
v2LaneTokensByNumber.set(laneNumber, {
|
|
2273
2330
|
input: (w.inputTokens || 0) + (r.inputTokens || 0),
|
|
2274
2331
|
output: (w.outputTokens || 0) + (r.outputTokens || 0),
|
|
2275
2332
|
cacheRead: (w.cacheReadTokens || 0) + (r.cacheReadTokens || 0),
|
|
@@ -2281,50 +2338,50 @@ export async function executeOrchBatch(
|
|
|
2281
2338
|
}
|
|
2282
2339
|
} catch { /* runtime dir may not exist */ }
|
|
2283
2340
|
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
}
|
|
2305
|
-
} catch { /* .pi dir may not exist */ }
|
|
2306
|
-
}
|
|
2341
|
+
// Legacy fallback: lane-state-*.json sidecars (pre-V2).
|
|
2342
|
+
try {
|
|
2343
|
+
const files = readdirSync(piDir).filter(f => f.startsWith("lane-state-") && f.endsWith(".json"));
|
|
2344
|
+
for (const f of files) {
|
|
2345
|
+
try {
|
|
2346
|
+
const raw = readFileSync(join(piDir, f), "utf-8").trim();
|
|
2347
|
+
if (!raw) continue;
|
|
2348
|
+
const data = JSON.parse(raw);
|
|
2349
|
+
if (data.prefix) {
|
|
2350
|
+
legacyLaneTokensByKey.set(data.prefix, {
|
|
2351
|
+
input: data.workerInputTokens || 0,
|
|
2352
|
+
output: data.workerOutputTokens || 0,
|
|
2353
|
+
cacheRead: data.workerCacheReadTokens || 0,
|
|
2354
|
+
cacheWrite: data.workerCacheWriteTokens || 0,
|
|
2355
|
+
costUsd: data.workerCostUsd || 0,
|
|
2356
|
+
});
|
|
2357
|
+
}
|
|
2358
|
+
} catch { /* skip invalid files */ }
|
|
2359
|
+
}
|
|
2360
|
+
} catch { /* .pi dir may not exist */ }
|
|
2307
2361
|
|
|
2308
2362
|
// Build per-task summaries from allTaskOutcomes + wave plan
|
|
2309
2363
|
const taskSummaries: BatchTaskSummary[] = allTaskOutcomes.map((to) => {
|
|
2310
2364
|
// Find which wave and lane this task ran in
|
|
2311
|
-
let wave = 0
|
|
2365
|
+
let wave = 0;
|
|
2312
2366
|
for (let wi = 0; wi < wavePlan.length; wi++) {
|
|
2313
2367
|
if (wavePlan[wi].includes(to.taskId)) { wave = wi + 1; break; }
|
|
2314
2368
|
}
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2369
|
+
const lane = to.laneNumber
|
|
2370
|
+
?? (() => {
|
|
2371
|
+
const laneMatch = to.sessionName?.match(/lane-(\d+)/);
|
|
2372
|
+
return laneMatch ? parseInt(laneMatch[1], 10) : 0;
|
|
2373
|
+
})();
|
|
2318
2374
|
|
|
2319
2375
|
// Compute duration from start/end times
|
|
2320
2376
|
const durationMs = (to.startTime && to.endTime) ? (to.endTime - to.startTime) : 0;
|
|
2321
2377
|
|
|
2322
|
-
//
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2378
|
+
// TP-116: Resolve tokens from outcome telemetry first; only fallback for legacy outcomes.
|
|
2379
|
+
const tokens = resolveBatchHistoryTaskTokens(
|
|
2380
|
+
to,
|
|
2381
|
+
lane,
|
|
2382
|
+
v2LaneTokensByNumber,
|
|
2383
|
+
legacyLaneTokensByKey,
|
|
2384
|
+
);
|
|
2328
2385
|
|
|
2329
2386
|
return {
|
|
2330
2387
|
taskId: to.taskId,
|
|
@@ -1369,6 +1369,7 @@ export async function executeLane(
|
|
|
1369
1369
|
exitReason: reason,
|
|
1370
1370
|
sessionName: lane.tmuxSessionName,
|
|
1371
1371
|
doneFileFound: false,
|
|
1372
|
+
laneNumber: lane.laneNumber,
|
|
1372
1373
|
});
|
|
1373
1374
|
continue;
|
|
1374
1375
|
}
|
|
@@ -1399,6 +1400,7 @@ export async function executeLane(
|
|
|
1399
1400
|
exitReason: pollResult.exitReason,
|
|
1400
1401
|
sessionName: lane.tmuxSessionName,
|
|
1401
1402
|
doneFileFound: pollResult.doneFileFound,
|
|
1403
|
+
laneNumber: lane.laneNumber,
|
|
1402
1404
|
};
|
|
1403
1405
|
|
|
1404
1406
|
// After task succeeds, commit any uncommitted artifacts (.DONE, final
|
|
@@ -1446,6 +1448,7 @@ export async function executeLane(
|
|
|
1446
1448
|
exitReason: errMsg,
|
|
1447
1449
|
sessionName: lane.tmuxSessionName,
|
|
1448
1450
|
doneFileFound: false,
|
|
1451
|
+
laneNumber: lane.laneNumber,
|
|
1449
1452
|
};
|
|
1450
1453
|
|
|
1451
1454
|
shouldSkipRemaining = true;
|
|
@@ -2577,6 +2580,7 @@ export async function executeWave(
|
|
|
2577
2580
|
exitReason: `Lane promise rejected: ${errMsg}`,
|
|
2578
2581
|
sessionName: lanes[idx].tmuxSessionName,
|
|
2579
2582
|
doneFileFound: false,
|
|
2583
|
+
laneNumber: lanes[idx].laneNumber,
|
|
2580
2584
|
})),
|
|
2581
2585
|
overallStatus: "failed" as const,
|
|
2582
2586
|
startTime: startedAt,
|
|
@@ -2779,6 +2783,7 @@ export async function executeWithStopAll(
|
|
|
2779
2783
|
exitReason: `Lane aborted: ${errMsg}`,
|
|
2780
2784
|
sessionName: lanes[idx].tmuxSessionName,
|
|
2781
2785
|
doneFileFound: false,
|
|
2786
|
+
laneNumber: lanes[idx].laneNumber,
|
|
2782
2787
|
})),
|
|
2783
2788
|
overallStatus: "failed",
|
|
2784
2789
|
startTime: Date.now(),
|
|
@@ -2933,6 +2938,83 @@ export function buildAgentIdFromLane(
|
|
|
2933
2938
|
*
|
|
2934
2939
|
* @since TP-102
|
|
2935
2940
|
*/
|
|
2941
|
+
/**
|
|
2942
|
+
* Parse an agent .md file: extract frontmatter and body.
|
|
2943
|
+
* Returns null if file doesn't exist or is malformed.
|
|
2944
|
+
* @since TP-117
|
|
2945
|
+
*/
|
|
2946
|
+
function parseAgentFile(filePath: string): { fm: Record<string, string>; body: string } | null {
|
|
2947
|
+
try {
|
|
2948
|
+
if (!existsSync(filePath)) return null;
|
|
2949
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
2950
|
+
const fmEnd = raw.indexOf("---", 4);
|
|
2951
|
+
if (fmEnd < 0) return { fm: {}, body: raw.trim() };
|
|
2952
|
+
const fmBlock = raw.slice(4, fmEnd).trim();
|
|
2953
|
+
const fm: Record<string, string> = {};
|
|
2954
|
+
for (const line of fmBlock.split("\n")) {
|
|
2955
|
+
const m = line.match(/^([\w-]+)\s*:\s*(.+)/);
|
|
2956
|
+
if (m) fm[m[1]] = m[2].trim();
|
|
2957
|
+
}
|
|
2958
|
+
return { fm, body: raw.slice(fmEnd + 3).trim() };
|
|
2959
|
+
} catch { return null; }
|
|
2960
|
+
}
|
|
2961
|
+
|
|
2962
|
+
/**
|
|
2963
|
+
* Load the base agent prompt from the taskplane package's templates/ directory.
|
|
2964
|
+
* Resolves the package root via well-known npm global paths.
|
|
2965
|
+
* @since TP-117
|
|
2966
|
+
*/
|
|
2967
|
+
function loadBaseAgentPrompt(agentName: string): string {
|
|
2968
|
+
const relPath = join("node_modules", "taskplane", "templates", "agents", `${agentName}.md`);
|
|
2969
|
+
const candidates: string[] = [];
|
|
2970
|
+
|
|
2971
|
+
// Global npm paths
|
|
2972
|
+
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
2973
|
+
if (process.env.APPDATA) candidates.push(join(process.env.APPDATA, "npm", relPath));
|
|
2974
|
+
if (home) {
|
|
2975
|
+
candidates.push(join(home, "AppData", "Roaming", "npm", relPath));
|
|
2976
|
+
candidates.push(join(home, ".npm-global", "lib", relPath));
|
|
2977
|
+
}
|
|
2978
|
+
candidates.push(join("/usr", "local", "lib", relPath));
|
|
2979
|
+
candidates.push(join("/opt", "homebrew", "lib", relPath));
|
|
2980
|
+
|
|
2981
|
+
// Dynamic: npm root -g
|
|
2982
|
+
try {
|
|
2983
|
+
const result = spawnSync("npm", ["root", "-g"], { encoding: "utf-8", timeout: 5000, shell: true });
|
|
2984
|
+
if (result.stdout?.trim()) {
|
|
2985
|
+
candidates.push(join(result.stdout.trim(), "taskplane", "templates", "agents", `${agentName}.md`));
|
|
2986
|
+
}
|
|
2987
|
+
} catch { /* ignore */ }
|
|
2988
|
+
|
|
2989
|
+
for (const p of candidates) {
|
|
2990
|
+
const def = parseAgentFile(p);
|
|
2991
|
+
if (def?.body) return def.body;
|
|
2992
|
+
}
|
|
2993
|
+
return "";
|
|
2994
|
+
}
|
|
2995
|
+
|
|
2996
|
+
/**
|
|
2997
|
+
* Load local project agent prompt from .pi/agents/ or agents/ directory.
|
|
2998
|
+
* Supports standalone mode (local replaces base entirely).
|
|
2999
|
+
* @since TP-117
|
|
3000
|
+
*/
|
|
3001
|
+
function loadLocalAgentPrompt(stateRoot: string, agentName: string): string {
|
|
3002
|
+
const paths = [
|
|
3003
|
+
join(stateRoot, ".pi", "agents", `${agentName}.md`),
|
|
3004
|
+
join(stateRoot, "agents", `${agentName}.md`),
|
|
3005
|
+
];
|
|
3006
|
+
for (const p of paths) {
|
|
3007
|
+
const def = parseAgentFile(p);
|
|
3008
|
+
if (def) {
|
|
3009
|
+
// standalone: true → use local as-is (body only, replaces base)
|
|
3010
|
+
if (def.fm.standalone === "true") return def.body;
|
|
3011
|
+
// Otherwise return body as project-specific guidance to append
|
|
3012
|
+
if (def.body) return def.body;
|
|
3013
|
+
}
|
|
3014
|
+
}
|
|
3015
|
+
return "";
|
|
3016
|
+
}
|
|
3017
|
+
|
|
2936
3018
|
export function resolveRuntimeStateRoot(
|
|
2937
3019
|
repoRoot: string,
|
|
2938
3020
|
workspaceRoot?: string,
|
|
@@ -2982,16 +3064,20 @@ export async function executeLaneV2(
|
|
|
2982
3064
|
const opId = resolveOperatorId(config);
|
|
2983
3065
|
const agentIdPrefix = `${tmuxPrefix}-${opId}`;
|
|
2984
3066
|
|
|
2985
|
-
// Load worker agent definition
|
|
3067
|
+
// Load worker agent definition: compose base template + local project guidance.
|
|
3068
|
+
// The base template (templates/agents/task-worker.md) contains critical behavioral
|
|
3069
|
+
// rules: checkpoint discipline, STATUS.md resume algorithm, review_step instructions.
|
|
3070
|
+
// The local file (.pi/agents/task-worker.md) adds project-specific guidance.
|
|
2986
3071
|
let workerSystemPrompt = "You are a task execution agent. Read STATUS.md first, find unchecked items, work on them, checkpoint after each.";
|
|
2987
3072
|
try {
|
|
2988
|
-
const
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
3073
|
+
const basePrompt = loadBaseAgentPrompt("task-worker");
|
|
3074
|
+
const localPrompt = loadLocalAgentPrompt(stateRoot, "task-worker");
|
|
3075
|
+
if (basePrompt && localPrompt) {
|
|
3076
|
+
workerSystemPrompt = basePrompt + "\n\n---\n\n## Project-Specific Guidance\n\n" + localPrompt;
|
|
3077
|
+
} else if (basePrompt) {
|
|
3078
|
+
workerSystemPrompt = basePrompt;
|
|
3079
|
+
} else if (localPrompt) {
|
|
3080
|
+
workerSystemPrompt = localPrompt;
|
|
2995
3081
|
}
|
|
2996
3082
|
} catch { /* use default */ }
|
|
2997
3083
|
|
|
@@ -3011,6 +3097,7 @@ export async function executeLaneV2(
|
|
|
3011
3097
|
exitReason: reason,
|
|
3012
3098
|
sessionName: buildRuntimeAgentId(agentIdPrefix, lane.laneNumber, "worker"),
|
|
3013
3099
|
doneFileFound: false,
|
|
3100
|
+
laneNumber: lane.laneNumber,
|
|
3014
3101
|
});
|
|
3015
3102
|
continue;
|
|
3016
3103
|
}
|
|
@@ -3040,7 +3127,10 @@ export async function executeLaneV2(
|
|
|
3040
3127
|
|
|
3041
3128
|
try {
|
|
3042
3129
|
const result = await executeTaskV2(unit, laneRunnerConfig, pauseSignal);
|
|
3043
|
-
outcomes.push(
|
|
3130
|
+
outcomes.push({
|
|
3131
|
+
...result.outcome,
|
|
3132
|
+
laneNumber: result.outcome.laneNumber ?? lane.laneNumber,
|
|
3133
|
+
});
|
|
3044
3134
|
|
|
3045
3135
|
// Commit artifacts after success (same as legacy path)
|
|
3046
3136
|
if (result.outcome.status === "succeeded") {
|
|
@@ -3066,6 +3156,7 @@ export async function executeLaneV2(
|
|
|
3066
3156
|
exitReason: `Runtime V2 execution error: ${errMsg}`,
|
|
3067
3157
|
sessionName: buildRuntimeAgentId(agentIdPrefix, lane.laneNumber, "worker"),
|
|
3068
3158
|
doneFileFound: false,
|
|
3159
|
+
laneNumber: lane.laneNumber,
|
|
3069
3160
|
});
|
|
3070
3161
|
shouldSkipRemaining = true;
|
|
3071
3162
|
}
|
|
@@ -496,6 +496,18 @@ function makeResult(
|
|
|
496
496
|
statusPath?: string,
|
|
497
497
|
finalTelemetry?: Partial<AgentHostResult>,
|
|
498
498
|
): LaneRunnerTaskResult {
|
|
499
|
+
const telemetry = status === "skipped"
|
|
500
|
+
? undefined
|
|
501
|
+
: {
|
|
502
|
+
inputTokens: finalTelemetry?.inputTokens ?? 0,
|
|
503
|
+
outputTokens: finalTelemetry?.outputTokens ?? 0,
|
|
504
|
+
cacheReadTokens: finalTelemetry?.cacheReadTokens ?? 0,
|
|
505
|
+
cacheWriteTokens: finalTelemetry?.cacheWriteTokens ?? 0,
|
|
506
|
+
costUsd: finalTelemetry?.costUsd ?? 0,
|
|
507
|
+
toolCalls: finalTelemetry?.toolCalls ?? 0,
|
|
508
|
+
durationMs: finalTelemetry?.durationMs ?? 0,
|
|
509
|
+
};
|
|
510
|
+
|
|
499
511
|
const result: LaneRunnerTaskResult = {
|
|
500
512
|
outcome: {
|
|
501
513
|
taskId,
|
|
@@ -505,6 +517,8 @@ function makeResult(
|
|
|
505
517
|
exitReason,
|
|
506
518
|
sessionName,
|
|
507
519
|
doneFileFound,
|
|
520
|
+
laneNumber: config?.laneNumber,
|
|
521
|
+
telemetry,
|
|
508
522
|
},
|
|
509
523
|
iterations,
|
|
510
524
|
costUsd,
|
|
@@ -50,6 +50,21 @@ export function hasTaskDoneMarker(taskFolder: string): boolean {
|
|
|
50
50
|
return false;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Compare optional embedded outcome telemetry.
|
|
55
|
+
*/
|
|
56
|
+
function sameOutcomeTelemetry(a: LaneTaskOutcome["telemetry"], b: LaneTaskOutcome["telemetry"]): boolean {
|
|
57
|
+
if (!a && !b) return true;
|
|
58
|
+
if (!a || !b) return false;
|
|
59
|
+
return a.inputTokens === b.inputTokens
|
|
60
|
+
&& a.outputTokens === b.outputTokens
|
|
61
|
+
&& a.cacheReadTokens === b.cacheReadTokens
|
|
62
|
+
&& a.cacheWriteTokens === b.cacheWriteTokens
|
|
63
|
+
&& a.costUsd === b.costUsd
|
|
64
|
+
&& a.toolCalls === b.toolCalls
|
|
65
|
+
&& a.durationMs === b.durationMs;
|
|
66
|
+
}
|
|
67
|
+
|
|
53
68
|
/**
|
|
54
69
|
* Upsert a task outcome in-place. Returns true if changed.
|
|
55
70
|
*/
|
|
@@ -61,19 +76,27 @@ export function upsertTaskOutcome(outcomes: LaneTaskOutcome[], next: LaneTaskOut
|
|
|
61
76
|
}
|
|
62
77
|
|
|
63
78
|
const prev = outcomes[idx];
|
|
79
|
+
const mergedNext: LaneTaskOutcome = {
|
|
80
|
+
...next,
|
|
81
|
+
laneNumber: next.laneNumber ?? prev.laneNumber,
|
|
82
|
+
telemetry: next.telemetry ?? prev.telemetry,
|
|
83
|
+
};
|
|
84
|
+
|
|
64
85
|
const changed =
|
|
65
|
-
prev.status !==
|
|
66
|
-
prev.startTime !==
|
|
67
|
-
prev.endTime !==
|
|
68
|
-
prev.exitReason !==
|
|
69
|
-
prev.sessionName !==
|
|
70
|
-
prev.doneFileFound !==
|
|
71
|
-
prev.
|
|
72
|
-
prev.
|
|
73
|
-
prev.
|
|
86
|
+
prev.status !== mergedNext.status ||
|
|
87
|
+
prev.startTime !== mergedNext.startTime ||
|
|
88
|
+
prev.endTime !== mergedNext.endTime ||
|
|
89
|
+
prev.exitReason !== mergedNext.exitReason ||
|
|
90
|
+
prev.sessionName !== mergedNext.sessionName ||
|
|
91
|
+
prev.doneFileFound !== mergedNext.doneFileFound ||
|
|
92
|
+
prev.laneNumber !== mergedNext.laneNumber ||
|
|
93
|
+
!sameOutcomeTelemetry(prev.telemetry, mergedNext.telemetry) ||
|
|
94
|
+
prev.partialProgressCommits !== mergedNext.partialProgressCommits ||
|
|
95
|
+
prev.partialProgressBranch !== mergedNext.partialProgressBranch ||
|
|
96
|
+
prev.exitDiagnostic !== mergedNext.exitDiagnostic;
|
|
74
97
|
|
|
75
98
|
if (changed) {
|
|
76
|
-
outcomes[idx] =
|
|
99
|
+
outcomes[idx] = mergedNext;
|
|
77
100
|
}
|
|
78
101
|
return changed;
|
|
79
102
|
}
|
|
@@ -130,6 +153,7 @@ export function seedPendingOutcomesForAllocatedLanes(
|
|
|
130
153
|
exitReason: "Pending execution",
|
|
131
154
|
sessionName: lane.tmuxSessionName,
|
|
132
155
|
doneFileFound: false,
|
|
156
|
+
laneNumber: lane.laneNumber,
|
|
133
157
|
}) || changed;
|
|
134
158
|
}
|
|
135
159
|
}
|
|
@@ -163,6 +187,8 @@ export function syncTaskOutcomesFromMonitor(
|
|
|
163
187
|
exitReason: existing?.exitReason || "Pending execution",
|
|
164
188
|
sessionName: existing?.sessionName || lane.sessionName,
|
|
165
189
|
doneFileFound: false,
|
|
190
|
+
laneNumber: existing?.laneNumber ?? lane.laneNumber,
|
|
191
|
+
telemetry: existing?.telemetry,
|
|
166
192
|
partialProgressCommits: existing?.partialProgressCommits,
|
|
167
193
|
partialProgressBranch: existing?.partialProgressBranch,
|
|
168
194
|
exitDiagnostic: existing?.exitDiagnostic,
|
|
@@ -182,6 +208,8 @@ export function syncTaskOutcomesFromMonitor(
|
|
|
182
208
|
exitReason: existing?.exitReason || ".DONE file created by task-runner",
|
|
183
209
|
sessionName: existing?.sessionName || lane.sessionName,
|
|
184
210
|
doneFileFound: true,
|
|
211
|
+
laneNumber: existing?.laneNumber ?? lane.laneNumber,
|
|
212
|
+
telemetry: existing?.telemetry,
|
|
185
213
|
partialProgressCommits: existing?.partialProgressCommits,
|
|
186
214
|
partialProgressBranch: existing?.partialProgressBranch,
|
|
187
215
|
exitDiagnostic: existing?.exitDiagnostic,
|
|
@@ -199,6 +227,8 @@ export function syncTaskOutcomesFromMonitor(
|
|
|
199
227
|
exitReason: existing?.exitReason || "Task failed or stalled",
|
|
200
228
|
sessionName: existing?.sessionName || lane.sessionName,
|
|
201
229
|
doneFileFound: false,
|
|
230
|
+
laneNumber: existing?.laneNumber ?? lane.laneNumber,
|
|
231
|
+
telemetry: existing?.telemetry,
|
|
202
232
|
partialProgressCommits: existing?.partialProgressCommits,
|
|
203
233
|
partialProgressBranch: existing?.partialProgressBranch,
|
|
204
234
|
exitDiagnostic: existing?.exitDiagnostic,
|
|
@@ -233,6 +263,8 @@ export function syncTaskOutcomesFromMonitor(
|
|
|
233
263
|
exitReason: existing?.exitReason || (mappedStatus === "running" ? "Task in progress" : (snap.stallReason || "Task reached terminal state")),
|
|
234
264
|
sessionName: existing?.sessionName || lane.sessionName,
|
|
235
265
|
doneFileFound: snap.doneFileFound,
|
|
266
|
+
laneNumber: existing?.laneNumber ?? lane.laneNumber,
|
|
267
|
+
telemetry: existing?.telemetry,
|
|
236
268
|
partialProgressCommits: existing?.partialProgressCommits,
|
|
237
269
|
partialProgressBranch: existing?.partialProgressBranch,
|
|
238
270
|
exitDiagnostic: existing?.exitDiagnostic,
|
|
@@ -1164,7 +1196,7 @@ export function serializeBatchState(
|
|
|
1164
1196
|
|
|
1165
1197
|
const record: PersistedTaskRecord = {
|
|
1166
1198
|
taskId,
|
|
1167
|
-
laneNumber: lane?.laneNumber ?? 0,
|
|
1199
|
+
laneNumber: lane?.laneNumber ?? outcome?.laneNumber ?? 0,
|
|
1168
1200
|
sessionName: outcome?.sessionName || lane?.tmuxSessionName || "",
|
|
1169
1201
|
status: outcome?.status ?? "pending",
|
|
1170
1202
|
taskFolder: "", // Enriched by caller from discovery
|
|
@@ -1356,6 +1356,7 @@ export async function resumeOrchBatch(
|
|
|
1356
1356
|
exitReason: "Re-executed task completed successfully",
|
|
1357
1357
|
sessionName: lane.tmuxSessionName,
|
|
1358
1358
|
doneFileFound: true,
|
|
1359
|
+
laneNumber: lane.laneNumber,
|
|
1359
1360
|
})),
|
|
1360
1361
|
overallStatus: "succeeded" as const,
|
|
1361
1362
|
startTime: Date.now(),
|
|
@@ -1474,6 +1475,7 @@ export async function resumeOrchBatch(
|
|
|
1474
1475
|
: persistedTask?.exitReason ?? "",
|
|
1475
1476
|
sessionName: persistedTask?.sessionName ?? "",
|
|
1476
1477
|
doneFileFound: status === "succeeded" ? true : task.doneFileFound,
|
|
1478
|
+
laneNumber: persistedTask?.laneNumber,
|
|
1477
1479
|
// Carry forward partial progress from persisted state (TP-028)
|
|
1478
1480
|
partialProgressCommits: persistedTask?.partialProgressCommits,
|
|
1479
1481
|
partialProgressBranch: persistedTask?.partialProgressBranch,
|
|
@@ -1604,6 +1606,7 @@ export async function resumeOrchBatch(
|
|
|
1604
1606
|
: "Task failed (merge retry)",
|
|
1605
1607
|
sessionName: lane.tmuxSessionName,
|
|
1606
1608
|
doneFileFound: status === "succeeded",
|
|
1609
|
+
laneNumber: lane.laneNumber,
|
|
1607
1610
|
};
|
|
1608
1611
|
});
|
|
1609
1612
|
|
|
@@ -649,6 +649,30 @@ export interface AllocatedLane {
|
|
|
649
649
|
*/
|
|
650
650
|
export type LaneTaskStatus = "pending" | "running" | "succeeded" | "failed" | "stalled" | "skipped";
|
|
651
651
|
|
|
652
|
+
/**
|
|
653
|
+
* Embedded telemetry attached to a lane task outcome.
|
|
654
|
+
*
|
|
655
|
+
* Populated by Runtime V2 lane-runner at emission time so downstream
|
|
656
|
+
* consumers (batch history, diagnostics) can read authoritative usage
|
|
657
|
+
* without reconstructing task↔lane joins from snapshot keys.
|
|
658
|
+
*/
|
|
659
|
+
export interface LaneTaskOutcomeTelemetry {
|
|
660
|
+
/** Total input tokens for this task outcome. */
|
|
661
|
+
inputTokens: number;
|
|
662
|
+
/** Total output tokens for this task outcome. */
|
|
663
|
+
outputTokens: number;
|
|
664
|
+
/** Total cache-read tokens for this task outcome. */
|
|
665
|
+
cacheReadTokens: number;
|
|
666
|
+
/** Total cache-write tokens for this task outcome. */
|
|
667
|
+
cacheWriteTokens: number;
|
|
668
|
+
/** Cumulative cost in USD for this task outcome. */
|
|
669
|
+
costUsd: number;
|
|
670
|
+
/** Number of tool calls made while producing this outcome. */
|
|
671
|
+
toolCalls: number;
|
|
672
|
+
/** End-to-end duration in milliseconds for this outcome. */
|
|
673
|
+
durationMs: number;
|
|
674
|
+
}
|
|
675
|
+
|
|
652
676
|
/**
|
|
653
677
|
* Outcome of a single task execution within a lane.
|
|
654
678
|
*
|
|
@@ -670,6 +694,19 @@ export interface LaneTaskOutcome {
|
|
|
670
694
|
sessionName: string;
|
|
671
695
|
/** Whether .DONE file was found */
|
|
672
696
|
doneFileFound: boolean;
|
|
697
|
+
/**
|
|
698
|
+
* Lane number that produced this task outcome (1-indexed).
|
|
699
|
+
*
|
|
700
|
+
* Optional for backward compatibility with pre-TP-116 persisted state.
|
|
701
|
+
*/
|
|
702
|
+
laneNumber?: number;
|
|
703
|
+
/**
|
|
704
|
+
* Embedded task-level telemetry (authoritative for Runtime V2).
|
|
705
|
+
*
|
|
706
|
+
* Optional for backward compatibility and non-agent outcomes
|
|
707
|
+
* (for example skipped tasks).
|
|
708
|
+
*/
|
|
709
|
+
telemetry?: LaneTaskOutcomeTelemetry;
|
|
673
710
|
/**
|
|
674
711
|
* Number of commits preserved as partial progress for a failed task.
|
|
675
712
|
* 0 when no partial progress was saved (succeeded tasks, no commits, etc.).
|