taskplane 0.24.14 → 0.24.16
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.
|
@@ -868,8 +868,17 @@ export async function resolveTaskMonitorState(
|
|
|
868
868
|
// to avoid indefinite false "running" if the lane-runner died.
|
|
869
869
|
const staleMs = snap?.updatedAt ? (now - snap.updatedAt) : 0;
|
|
870
870
|
if (staleMs > 30_000) {
|
|
871
|
-
// Snapshot hasn't been updated for 30s+ — check registry as fallback
|
|
872
|
-
|
|
871
|
+
// Snapshot hasn't been updated for 30s+ — check registry as fallback.
|
|
872
|
+
// But also check if the tracker just started (firstObservedAt within
|
|
873
|
+
// last 60s) — wave transitions can leave stale snapshots from the
|
|
874
|
+
// prior wave/task while the new worker is still spawning.
|
|
875
|
+
const trackerAgeMs = now - tracker.firstObservedAt;
|
|
876
|
+
if (trackerAgeMs < 60_000) {
|
|
877
|
+
// New task, stale snapshot — give the worker startup grace period
|
|
878
|
+
sessionAlive = true;
|
|
879
|
+
} else {
|
|
880
|
+
sessionAlive = isV2AgentAlive(sessionName, runtimeBackend);
|
|
881
|
+
}
|
|
873
882
|
} else {
|
|
874
883
|
sessionAlive = true;
|
|
875
884
|
}
|
|
@@ -1620,6 +1629,18 @@ export async function executeWave(
|
|
|
1620
1629
|
}
|
|
1621
1630
|
execLog("wave", `W${waveIndex}`, "using Runtime V2 backend (executeLaneV2)");
|
|
1622
1631
|
|
|
1632
|
+
// Clear stale lane snapshots from prior waves before launching new workers.
|
|
1633
|
+
// Without this, the monitor reads a snapshot from wave N-1 (different taskId,
|
|
1634
|
+
// staleMs > 30s) and may falsely mark the new task as failed before the
|
|
1635
|
+
// worker has time to write its first snapshot.
|
|
1636
|
+
const snapshotStateRoot = resolveRuntimeStateRoot(repoRoot, wsRoot);
|
|
1637
|
+
for (const lane of lanes) {
|
|
1638
|
+
try {
|
|
1639
|
+
const snapPath = join(snapshotStateRoot, ".pi", "runtime", batchId, "lanes", `lane-${lane.laneNumber}.json`);
|
|
1640
|
+
if (existsSync(snapPath)) unlinkSync(snapPath);
|
|
1641
|
+
} catch { /* best effort */ }
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1623
1644
|
const lanePromises = lanes.map(lane =>
|
|
1624
1645
|
executeLaneV2(lane, config, repoRoot, wavePauseSignal, wsRoot, isWsMode, { ORCH_BATCH_ID: batchId }, onSupervisorAlert),
|
|
1625
1646
|
);
|
|
@@ -2,7 +2,7 @@ import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-age
|
|
|
2
2
|
import { Type } from "@mariozechner/pi-ai";
|
|
3
3
|
|
|
4
4
|
import { execSync, execFileSync } from "child_process";
|
|
5
|
-
import { writeFileSync, unlinkSync, mkdirSync, existsSync, readdirSync, readFileSync, statSync, createWriteStream } from "fs";
|
|
5
|
+
import { writeFileSync, unlinkSync, mkdirSync, existsSync, readdirSync, readFileSync, statSync, createWriteStream, renameSync } from "fs";
|
|
6
6
|
import { join, dirname } from "path";
|
|
7
7
|
import { fileURLToPath } from "url";
|
|
8
8
|
import { fork, type ChildProcess } from "child_process";
|
|
@@ -365,6 +365,49 @@ export interface IntegrationExecDeps {
|
|
|
365
365
|
deleteBatchState: () => void;
|
|
366
366
|
}
|
|
367
367
|
|
|
368
|
+
interface BatchHistorySnapshot {
|
|
369
|
+
filePath: string;
|
|
370
|
+
raw: string;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Preserve `.pi/batch-history.json` across integration merges.
|
|
375
|
+
*
|
|
376
|
+
* Runtime history is sidecar state, not source-controlled content. In environments
|
|
377
|
+
* where `.pi/batch-history.json` was previously tracked, merge/checkouts can
|
|
378
|
+
* replace newer runtime history with stale branch snapshots. This helper snapshots
|
|
379
|
+
* the file before integration and restores it afterward (best effort).
|
|
380
|
+
*/
|
|
381
|
+
export function withPreservedBatchHistory<T>(stateRoot: string, operation: () => T): T {
|
|
382
|
+
const historyPath = join(stateRoot, ".pi", "batch-history.json");
|
|
383
|
+
let snapshot: BatchHistorySnapshot | null = null;
|
|
384
|
+
try {
|
|
385
|
+
if (existsSync(historyPath)) {
|
|
386
|
+
snapshot = {
|
|
387
|
+
filePath: historyPath,
|
|
388
|
+
raw: readFileSync(historyPath, "utf-8"),
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
} catch {
|
|
392
|
+
// Best effort only — integration should never fail due to snapshot capture.
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
try {
|
|
396
|
+
return operation();
|
|
397
|
+
} finally {
|
|
398
|
+
if (!snapshot) return;
|
|
399
|
+
try {
|
|
400
|
+
const dir = dirname(snapshot.filePath);
|
|
401
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
402
|
+
const tmpPath = snapshot.filePath + ".tmp";
|
|
403
|
+
writeFileSync(tmpPath, snapshot.raw);
|
|
404
|
+
renameSync(tmpPath, snapshot.filePath);
|
|
405
|
+
} catch {
|
|
406
|
+
// Best effort only — never block integration completion.
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
368
411
|
/**
|
|
369
412
|
* Execute the integration operation for the resolved context.
|
|
370
413
|
*
|
|
@@ -1324,10 +1367,13 @@ export function buildIntegrationExecutor(repoRoot: string, opId?: string, stateR
|
|
|
1324
1367
|
},
|
|
1325
1368
|
};
|
|
1326
1369
|
|
|
1327
|
-
const
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1370
|
+
const effectiveStateRoot = stateRoot ?? repoRoot;
|
|
1371
|
+
const result = withPreservedBatchHistory(effectiveStateRoot, () =>
|
|
1372
|
+
executeIntegration(mode as IntegrateMode, {
|
|
1373
|
+
...context,
|
|
1374
|
+
currentBranch: context.baseBranch,
|
|
1375
|
+
}, deps),
|
|
1376
|
+
);
|
|
1331
1377
|
|
|
1332
1378
|
// TP-051: Clean up stale task/* and saved/* branches after successful integration.
|
|
1333
1379
|
// This ensures auto-mode integration (supervisor path) gets the same cleanup
|
|
@@ -3116,44 +3162,51 @@ export default function (pi: ExtensionAPI) {
|
|
|
3116
3162
|
reposToIntegrate.push({ id: "(default)", root: repoRoot });
|
|
3117
3163
|
}
|
|
3118
3164
|
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
const integrationResult = executeIntegration(parsed.mode, resolution as IntegrationContext, {
|
|
3128
|
-
runGit: (gitArgs: string[]) => runGit(gitArgs, repo.root),
|
|
3129
|
-
runCommand: (cmd: string, cmdArgs: string[]) => {
|
|
3130
|
-
try {
|
|
3131
|
-
const stdout = execFileSync(cmd, cmdArgs, {
|
|
3132
|
-
encoding: "utf-8",
|
|
3133
|
-
timeout: 60_000,
|
|
3134
|
-
cwd: repo.root,
|
|
3135
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
3136
|
-
}).trim();
|
|
3137
|
-
return { ok: true, stdout, stderr: "" };
|
|
3138
|
-
} catch (err: unknown) {
|
|
3139
|
-
const e = err as { stdout?: string; stderr?: string; message?: string };
|
|
3140
|
-
return {
|
|
3141
|
-
ok: false,
|
|
3142
|
-
stdout: (e.stdout ?? "").toString().trim(),
|
|
3143
|
-
stderr: (e.stderr ?? e.message ?? "unknown error").toString().trim(),
|
|
3144
|
-
};
|
|
3145
|
-
}
|
|
3146
|
-
},
|
|
3147
|
-
deleteBatchState: () => { /* handled once after all repos */ },
|
|
3148
|
-
});
|
|
3165
|
+
const integrationRun = withPreservedBatchHistory(stateRoot, () => {
|
|
3166
|
+
let totalCommits = 0;
|
|
3167
|
+
const repoMessages: string[] = [];
|
|
3168
|
+
|
|
3169
|
+
for (const repo of reposToIntegrate) {
|
|
3170
|
+
const preCountResult = runGit(["rev-list", "--count", `HEAD..${resolvedOrchBranch}`], repo.root);
|
|
3171
|
+
const repoCommitsBefore = preCountResult.ok ? parseInt(preCountResult.stdout) || 0 : 0;
|
|
3149
3172
|
|
|
3150
|
-
|
|
3151
|
-
|
|
3173
|
+
const integrationResult = executeIntegration(parsed.mode, resolution as IntegrationContext, {
|
|
3174
|
+
runGit: (gitArgs: string[]) => runGit(gitArgs, repo.root),
|
|
3175
|
+
runCommand: (cmd: string, cmdArgs: string[]) => {
|
|
3176
|
+
try {
|
|
3177
|
+
const stdout = execFileSync(cmd, cmdArgs, {
|
|
3178
|
+
encoding: "utf-8",
|
|
3179
|
+
timeout: 60_000,
|
|
3180
|
+
cwd: repo.root,
|
|
3181
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
3182
|
+
}).trim();
|
|
3183
|
+
return { ok: true, stdout, stderr: "" };
|
|
3184
|
+
} catch (err: unknown) {
|
|
3185
|
+
const e = err as { stdout?: string; stderr?: string; message?: string };
|
|
3186
|
+
return {
|
|
3187
|
+
ok: false,
|
|
3188
|
+
stdout: (e.stdout ?? "").toString().trim(),
|
|
3189
|
+
stderr: (e.stderr ?? e.message ?? "unknown error").toString().trim(),
|
|
3190
|
+
};
|
|
3191
|
+
}
|
|
3192
|
+
},
|
|
3193
|
+
deleteBatchState: () => { /* handled once after all repos */ },
|
|
3194
|
+
});
|
|
3195
|
+
|
|
3196
|
+
if (!integrationResult.success) {
|
|
3197
|
+
return { ok: false as const, error: `❌ Integration failed in ${repo.id}:\n${integrationResult.error}` };
|
|
3198
|
+
}
|
|
3199
|
+
|
|
3200
|
+
totalCommits += repoCommitsBefore;
|
|
3201
|
+
repoMessages.push(` ${repo.id}: ${integrationResult.message}`);
|
|
3152
3202
|
}
|
|
3153
3203
|
|
|
3154
|
-
totalCommits
|
|
3155
|
-
|
|
3204
|
+
return { ok: true as const, totalCommits, repoMessages };
|
|
3205
|
+
});
|
|
3206
|
+
if (!integrationRun.ok) {
|
|
3207
|
+
return { message: integrationRun.error, error: true };
|
|
3156
3208
|
}
|
|
3209
|
+
const { totalCommits, repoMessages } = integrationRun;
|
|
3157
3210
|
|
|
3158
3211
|
// Post-integration cleanup & acceptance
|
|
3159
3212
|
const allRepos: { id: string; root: string }[] = [];
|
|
@@ -1841,18 +1841,21 @@ export function saveBatchHistory(repoRoot: string, summary: BatchHistorySummary)
|
|
|
1841
1841
|
const filePath = batchHistoryPath(repoRoot);
|
|
1842
1842
|
try {
|
|
1843
1843
|
const history = loadBatchHistory(repoRoot);
|
|
1844
|
+
// Upsert by batchId so resumed batches replace their earlier partial entry
|
|
1845
|
+
// instead of creating duplicates.
|
|
1846
|
+
const nextHistory = history.filter(entry => entry.batchId !== summary.batchId);
|
|
1844
1847
|
// Prepend newest first
|
|
1845
|
-
|
|
1848
|
+
nextHistory.unshift(summary);
|
|
1846
1849
|
// Trim to max
|
|
1847
|
-
if (
|
|
1848
|
-
|
|
1850
|
+
if (nextHistory.length > BATCH_HISTORY_MAX_ENTRIES) {
|
|
1851
|
+
nextHistory.length = BATCH_HISTORY_MAX_ENTRIES;
|
|
1849
1852
|
}
|
|
1850
1853
|
const dir = dirname(filePath);
|
|
1851
1854
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
1852
1855
|
const tmpPath = filePath + ".tmp";
|
|
1853
|
-
writeFileSync(tmpPath, JSON.stringify(
|
|
1856
|
+
writeFileSync(tmpPath, JSON.stringify(nextHistory, null, 2));
|
|
1854
1857
|
renameSync(tmpPath, filePath);
|
|
1855
|
-
execLog("batch", "history", `saved batch summary (${
|
|
1858
|
+
execLog("batch", "history", `saved batch summary (${nextHistory.length} entries)`);
|
|
1856
1859
|
} catch (err) {
|
|
1857
1860
|
execLog("batch", "history", `failed to save batch history: ${err}`);
|
|
1858
1861
|
}
|