taskplane 0.24.21 → 0.24.23
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/README.md +1 -1
- package/bin/taskplane.mjs +239 -103
- package/extensions/task-runner.ts +2 -110
- package/extensions/taskplane/agent-bridge-extension.ts +196 -3
- package/extensions/taskplane/config-loader.ts +306 -269
- package/extensions/taskplane/config-schema.ts +49 -32
- package/extensions/taskplane/engine-worker.ts +4 -0
- package/extensions/taskplane/engine.ts +884 -12
- package/extensions/taskplane/execution.ts +12 -1
- package/extensions/taskplane/extension.ts +4 -0
- package/extensions/taskplane/lane-runner.ts +5 -0
- package/extensions/taskplane/merge.ts +7 -1
- package/extensions/taskplane/persistence.ts +12 -0
- package/extensions/taskplane/resume.ts +132 -22
- package/extensions/taskplane/settings-tui.ts +149 -172
- package/extensions/taskplane/supervisor.ts +2 -2
- package/extensions/taskplane/types.ts +82 -17
- package/package.json +1 -1
|
@@ -1541,6 +1541,7 @@ export async function executeWave(
|
|
|
1541
1541
|
workspaceConfig?: WorkspaceConfig | null,
|
|
1542
1542
|
runtimeBackend?: RuntimeBackend,
|
|
1543
1543
|
onSupervisorAlert?: SupervisorAlertCallback,
|
|
1544
|
+
supervisorAutonomy: "interactive" | "supervised" | "autonomous" = "autonomous",
|
|
1544
1545
|
): Promise<WaveExecutionResult> {
|
|
1545
1546
|
const startedAt = Date.now();
|
|
1546
1547
|
const policy = config.failure.on_task_failure;
|
|
@@ -1642,7 +1643,10 @@ export async function executeWave(
|
|
|
1642
1643
|
}
|
|
1643
1644
|
|
|
1644
1645
|
const lanePromises = lanes.map(lane =>
|
|
1645
|
-
executeLaneV2(lane, config, repoRoot, wavePauseSignal, wsRoot, isWsMode, {
|
|
1646
|
+
executeLaneV2(lane, config, repoRoot, wavePauseSignal, wsRoot, isWsMode, {
|
|
1647
|
+
ORCH_BATCH_ID: batchId,
|
|
1648
|
+
TASKPLANE_SUPERVISOR_AUTONOMY: supervisorAutonomy,
|
|
1649
|
+
}, onSupervisorAlert),
|
|
1646
1650
|
);
|
|
1647
1651
|
|
|
1648
1652
|
// Start monitoring as a sibling async loop
|
|
@@ -2235,6 +2239,12 @@ export async function executeLaneV2(
|
|
|
2235
2239
|
// Build execution unit
|
|
2236
2240
|
const unit = buildExecutionUnit(lane, task, repoRoot, isWorkspaceMode);
|
|
2237
2241
|
|
|
2242
|
+
const rawAutonomy = String(extraEnvVars?.TASKPLANE_SUPERVISOR_AUTONOMY ?? "autonomous").toLowerCase();
|
|
2243
|
+
const supervisorAutonomy: LaneRunnerConfig["supervisorAutonomy"] =
|
|
2244
|
+
(rawAutonomy === "interactive" || rawAutonomy === "supervised" || rawAutonomy === "autonomous")
|
|
2245
|
+
? rawAutonomy as LaneRunnerConfig["supervisorAutonomy"]
|
|
2246
|
+
: "autonomous";
|
|
2247
|
+
|
|
2238
2248
|
const laneRunnerConfig: LaneRunnerConfig = {
|
|
2239
2249
|
batchId,
|
|
2240
2250
|
agentIdPrefix,
|
|
@@ -2247,6 +2257,7 @@ export async function executeLaneV2(
|
|
|
2247
2257
|
workerTools: "read,write,edit,bash,grep,find,ls",
|
|
2248
2258
|
workerThinking: "",
|
|
2249
2259
|
workerSystemPrompt,
|
|
2260
|
+
supervisorAutonomy,
|
|
2250
2261
|
projectName: config.project?.name || "project",
|
|
2251
2262
|
maxIterations: 20,
|
|
2252
2263
|
noProgressLimit: 3,
|
|
@@ -1032,6 +1032,7 @@ export function startBatchInWorker(
|
|
|
1032
1032
|
wkData.agentRoot,
|
|
1033
1033
|
wkData.force ?? false,
|
|
1034
1034
|
onSupervisorAlert ?? null,
|
|
1035
|
+
wkData.supervisorAutonomy ?? "autonomous",
|
|
1035
1036
|
)
|
|
1036
1037
|
: () => executeOrchBatch(
|
|
1037
1038
|
wkData.args ?? "",
|
|
@@ -1046,6 +1047,7 @@ export function startBatchInWorker(
|
|
|
1046
1047
|
wkData.agentRoot,
|
|
1047
1048
|
null, // onEngineEvent
|
|
1048
1049
|
onSupervisorAlert ?? null,
|
|
1050
|
+
wkData.supervisorAutonomy ?? "autonomous",
|
|
1049
1051
|
);
|
|
1050
1052
|
startBatchAsync(fallbackFn, batchState, ctx, updateWidget, onTerminal);
|
|
1051
1053
|
return null;
|
|
@@ -2052,6 +2054,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2052
2054
|
: null,
|
|
2053
2055
|
workspaceRoot: execCtx!.workspaceRoot,
|
|
2054
2056
|
agentRoot: execCtx!.pointer?.agentRoot,
|
|
2057
|
+
supervisorAutonomy: supervisorConfig.autonomy,
|
|
2055
2058
|
},
|
|
2056
2059
|
orchBatchState,
|
|
2057
2060
|
ctx,
|
|
@@ -2396,6 +2399,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2396
2399
|
workspaceRoot: execCtx!.workspaceRoot,
|
|
2397
2400
|
agentRoot: execCtx!.pointer?.agentRoot,
|
|
2398
2401
|
force,
|
|
2402
|
+
supervisorAutonomy: supervisorConfig.autonomy,
|
|
2399
2403
|
},
|
|
2400
2404
|
orchBatchState,
|
|
2401
2405
|
ctx,
|
|
@@ -92,6 +92,8 @@ export interface LaneRunnerConfig {
|
|
|
92
92
|
workerThinking: string;
|
|
93
93
|
/** Worker system prompt */
|
|
94
94
|
workerSystemPrompt: string;
|
|
95
|
+
/** Supervisor autonomy level for bridge-tool guards. */
|
|
96
|
+
supervisorAutonomy?: "interactive" | "supervised" | "autonomous";
|
|
95
97
|
/** Project name (for review request context) */
|
|
96
98
|
projectName?: string;
|
|
97
99
|
/** Max worker iterations before giving up */
|
|
@@ -312,6 +314,9 @@ export async function executeTaskV2(
|
|
|
312
314
|
TASKPLANE_REVIEWS_DIR: unit.packet.reviewsDir,
|
|
313
315
|
TASKPLANE_REVIEWER_STATE_PATH: reviewerStatePath,
|
|
314
316
|
TASKPLANE_PROJECT_NAME: config.projectName || "project",
|
|
317
|
+
TASKPLANE_TASK_ID: taskId,
|
|
318
|
+
TASKPLANE_ACTIVE_SEGMENT_ID: segmentId ?? "",
|
|
319
|
+
TASKPLANE_SUPERVISOR_AUTONOMY: config.supervisorAutonomy || "autonomous",
|
|
315
320
|
ORCH_BATCH_ID: config.batchId,
|
|
316
321
|
},
|
|
317
322
|
};
|
|
@@ -611,7 +611,7 @@ export async function spawnMergeAgentV2(
|
|
|
611
611
|
packet: null,
|
|
612
612
|
env: {
|
|
613
613
|
ORCH_BATCH_ID: bid,
|
|
614
|
-
// Isolate merge agent from operator's
|
|
614
|
+
// Isolate merge agent from operator's global preferences to prevent
|
|
615
615
|
// verification test contamination (e.g., stale reviewerModel pref
|
|
616
616
|
// overriding schema defaults in deep-equal assertions).
|
|
617
617
|
PI_CODING_AGENT_DIR: join(sidecarRoot, "runtime", bid, "merge-agent-env"),
|
|
@@ -1849,6 +1849,12 @@ export async function mergeWave(
|
|
|
1849
1849
|
|
|
1850
1850
|
for (const lane of orderedLanes) {
|
|
1851
1851
|
for (const allocTask of lane.tasks) {
|
|
1852
|
+
if (!allocTask.task.taskFolder?.trim()) {
|
|
1853
|
+
execLog("merge", `W${waveIndex}`, `skipping task with missing taskFolder (possibly dynamically expanded)`, {
|
|
1854
|
+
taskId: allocTask.taskId,
|
|
1855
|
+
});
|
|
1856
|
+
continue;
|
|
1857
|
+
}
|
|
1852
1858
|
const absFolder = resolve(allocTask.task.taskFolder);
|
|
1853
1859
|
const relFolder = relative(resolvedRepoRoot, absFolder).replace(/\\/g, "/");
|
|
1854
1860
|
|
|
@@ -1139,6 +1139,18 @@ export function validatePersistedState(data: unknown): PersistedBatchState {
|
|
|
1139
1139
|
);
|
|
1140
1140
|
}
|
|
1141
1141
|
}
|
|
1142
|
+
if (s.expandedFrom !== undefined && typeof s.expandedFrom !== "string") {
|
|
1143
|
+
throw new StateFileError(
|
|
1144
|
+
"STATE_SCHEMA_INVALID",
|
|
1145
|
+
`segments[${i}].expandedFrom is not a string when present (got ${typeof s.expandedFrom})`,
|
|
1146
|
+
);
|
|
1147
|
+
}
|
|
1148
|
+
if (s.expansionRequestId !== undefined && typeof s.expansionRequestId !== "string") {
|
|
1149
|
+
throw new StateFileError(
|
|
1150
|
+
"STATE_SCHEMA_INVALID",
|
|
1151
|
+
`segments[${i}].expansionRequestId is not a string when present (got ${typeof s.expansionRequestId})`,
|
|
1152
|
+
);
|
|
1153
|
+
}
|
|
1142
1154
|
// Optional exitDiagnostic
|
|
1143
1155
|
if (s.exitDiagnostic !== undefined) {
|
|
1144
1156
|
if (!s.exitDiagnostic || typeof s.exitDiagnostic !== "object" || Array.isArray(s.exitDiagnostic)) {
|
|
@@ -641,6 +641,84 @@ export function getMergeStatusForWave(
|
|
|
641
641
|
return null;
|
|
642
642
|
}
|
|
643
643
|
|
|
644
|
+
/**
|
|
645
|
+
* Expand persisted wave plan with continuation rounds required by segment counts.
|
|
646
|
+
*
|
|
647
|
+
* Groups missing rounds by the original last-occurrence wave so resumed execution
|
|
648
|
+
* preserves multi-task round concurrency semantics (`[A,B]`, then `[A]`, etc.).
|
|
649
|
+
*/
|
|
650
|
+
export function buildResumeRuntimeWavePlan(persistedState: PersistedBatchState): string[][] {
|
|
651
|
+
const baseWavePlan = persistedState.wavePlan.map((wave) => [...wave]);
|
|
652
|
+
const runtimeWavePlan = [...baseWavePlan];
|
|
653
|
+
const segmentCountByTaskId = new Map<string, number>();
|
|
654
|
+
for (const task of persistedState.tasks) {
|
|
655
|
+
if (Array.isArray(task.segmentIds) && task.segmentIds.length > 0) {
|
|
656
|
+
segmentCountByTaskId.set(task.taskId, task.segmentIds.length);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
const scheduledCountByTaskId = new Map<string, number>();
|
|
661
|
+
const lastWaveIndexByTaskId = new Map<string, number>();
|
|
662
|
+
for (let waveIdx = 0; waveIdx < baseWavePlan.length; waveIdx++) {
|
|
663
|
+
for (const taskId of baseWavePlan[waveIdx]) {
|
|
664
|
+
scheduledCountByTaskId.set(taskId, (scheduledCountByTaskId.get(taskId) ?? 0) + 1);
|
|
665
|
+
lastWaveIndexByTaskId.set(taskId, waveIdx);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const missingByLastWaveIndex = new Map<number, Map<string, number>>();
|
|
670
|
+
for (const [taskId, segmentCount] of segmentCountByTaskId.entries()) {
|
|
671
|
+
const scheduledCount = scheduledCountByTaskId.get(taskId) ?? 0;
|
|
672
|
+
if (segmentCount <= scheduledCount) continue;
|
|
673
|
+
const lastWaveIndex = lastWaveIndexByTaskId.get(taskId) ?? -1;
|
|
674
|
+
if (!missingByLastWaveIndex.has(lastWaveIndex)) {
|
|
675
|
+
missingByLastWaveIndex.set(lastWaveIndex, new Map<string, number>());
|
|
676
|
+
}
|
|
677
|
+
missingByLastWaveIndex.get(lastWaveIndex)!.set(taskId, segmentCount - scheduledCount);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
let offset = 0;
|
|
681
|
+
for (let baseWaveIdx = 0; baseWaveIdx < baseWavePlan.length; baseWaveIdx++) {
|
|
682
|
+
const missingForWave = missingByLastWaveIndex.get(baseWaveIdx);
|
|
683
|
+
if (!missingForWave || missingForWave.size === 0) continue;
|
|
684
|
+
const rounds: string[][] = [];
|
|
685
|
+
const remaining = new Map(missingForWave);
|
|
686
|
+
while ([...remaining.values()].some((count) => count > 0)) {
|
|
687
|
+
const roundTaskIds = [...remaining.entries()]
|
|
688
|
+
.filter(([, count]) => count > 0)
|
|
689
|
+
.map(([taskId]) => taskId)
|
|
690
|
+
.sort((a, b) => a.localeCompare(b));
|
|
691
|
+
if (roundTaskIds.length === 0) break;
|
|
692
|
+
rounds.push(roundTaskIds);
|
|
693
|
+
for (const taskId of roundTaskIds) {
|
|
694
|
+
remaining.set(taskId, (remaining.get(taskId) ?? 0) - 1);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
if (rounds.length > 0) {
|
|
698
|
+
runtimeWavePlan.splice(baseWaveIdx + 1 + offset, 0, ...rounds);
|
|
699
|
+
offset += rounds.length;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const dangling = missingByLastWaveIndex.get(-1);
|
|
704
|
+
if (dangling && dangling.size > 0) {
|
|
705
|
+
const remaining = new Map(dangling);
|
|
706
|
+
while ([...remaining.values()].some((count) => count > 0)) {
|
|
707
|
+
const roundTaskIds = [...remaining.entries()]
|
|
708
|
+
.filter(([, count]) => count > 0)
|
|
709
|
+
.map(([taskId]) => taskId)
|
|
710
|
+
.sort((a, b) => a.localeCompare(b));
|
|
711
|
+
if (roundTaskIds.length === 0) break;
|
|
712
|
+
runtimeWavePlan.push(roundTaskIds);
|
|
713
|
+
for (const taskId of roundTaskIds) {
|
|
714
|
+
remaining.set(taskId, (remaining.get(taskId) ?? 0) - 1);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
return runtimeWavePlan;
|
|
720
|
+
}
|
|
721
|
+
|
|
644
722
|
/**
|
|
645
723
|
* Compute the resume point from reconciled task states and wave plan.
|
|
646
724
|
*
|
|
@@ -662,6 +740,7 @@ export function getMergeStatusForWave(
|
|
|
662
740
|
export function computeResumePoint(
|
|
663
741
|
persistedState: PersistedBatchState,
|
|
664
742
|
reconciledTasks: ReconciledTaskState[],
|
|
743
|
+
wavePlan: string[][] = persistedState.wavePlan,
|
|
665
744
|
): ResumePoint {
|
|
666
745
|
// Build lookup: taskId → reconciled state
|
|
667
746
|
const reconciledMap = new Map<string, ReconciledTaskState>();
|
|
@@ -684,8 +763,8 @@ export function computeResumePoint(
|
|
|
684
763
|
}
|
|
685
764
|
const waveSegmentIdByTaskOccurrence = new Map<string, string>();
|
|
686
765
|
const occurrenceByTaskId = new Map<string, number>();
|
|
687
|
-
for (let waveIdx = 0; waveIdx <
|
|
688
|
-
for (const taskId of
|
|
766
|
+
for (let waveIdx = 0; waveIdx < wavePlan.length; waveIdx++) {
|
|
767
|
+
for (const taskId of wavePlan[waveIdx]) {
|
|
689
768
|
const segmentIds = segmentIdsByTaskId.get(taskId);
|
|
690
769
|
if (!segmentIds || segmentIds.length === 0) continue;
|
|
691
770
|
const occurrence = occurrenceByTaskId.get(taskId) ?? 0;
|
|
@@ -737,11 +816,11 @@ export function computeResumePoint(
|
|
|
737
816
|
// Find resume wave: first wave with any non-completed tasks OR missing/failed merge.
|
|
738
817
|
// TP-037 (Bug #102): A wave where all tasks are terminal but the merge
|
|
739
818
|
// hasn't succeeded is flagged for merge retry, not skipped.
|
|
740
|
-
let resumeWaveIndex =
|
|
819
|
+
let resumeWaveIndex = wavePlan.length; // default: past end = all done
|
|
741
820
|
const mergeRetryWaveIndexes: number[] = [];
|
|
742
821
|
|
|
743
|
-
for (let i = 0; i <
|
|
744
|
-
const waveTasks =
|
|
822
|
+
for (let i = 0; i < wavePlan.length; i++) {
|
|
823
|
+
const waveTasks = wavePlan[i];
|
|
745
824
|
const allDone = waveTasks.every((taskId) => {
|
|
746
825
|
const waveSegmentId = waveSegmentIdByTaskOccurrence.get(`${i}:${taskId}`);
|
|
747
826
|
if (waveSegmentId && segmentStatusBySegmentId.has(waveSegmentId)) {
|
|
@@ -769,7 +848,7 @@ export function computeResumePoint(
|
|
|
769
848
|
if (!allDone) {
|
|
770
849
|
// Only set resumeWaveIndex if not already set by a merge retry
|
|
771
850
|
// (merge retry at an earlier wave takes precedence)
|
|
772
|
-
if (resumeWaveIndex ===
|
|
851
|
+
if (resumeWaveIndex === wavePlan.length) {
|
|
773
852
|
resumeWaveIndex = i;
|
|
774
853
|
}
|
|
775
854
|
break;
|
|
@@ -795,7 +874,7 @@ export function computeResumePoint(
|
|
|
795
874
|
if (mergeStatus !== "succeeded") {
|
|
796
875
|
// Merge missing or failed — flag for retry, don't skip past this wave
|
|
797
876
|
mergeRetryWaveIndexes.push(i);
|
|
798
|
-
if (resumeWaveIndex ===
|
|
877
|
+
if (resumeWaveIndex === wavePlan.length) {
|
|
799
878
|
// This is the first wave needing attention — set resume point here
|
|
800
879
|
resumeWaveIndex = i;
|
|
801
880
|
}
|
|
@@ -805,8 +884,8 @@ export function computeResumePoint(
|
|
|
805
884
|
|
|
806
885
|
// Determine pending tasks: tasks in resume wave and later that need execution
|
|
807
886
|
const actualPendingTaskIds: string[] = [];
|
|
808
|
-
for (let i = resumeWaveIndex; i <
|
|
809
|
-
for (const taskId of
|
|
887
|
+
for (let i = resumeWaveIndex; i < wavePlan.length; i++) {
|
|
888
|
+
for (const taskId of wavePlan[i]) {
|
|
810
889
|
const waveSegmentId = waveSegmentIdByTaskOccurrence.get(`${i}:${taskId}`);
|
|
811
890
|
if (waveSegmentId && segmentStatusBySegmentId.has(waveSegmentId)) {
|
|
812
891
|
const segmentStatus = segmentStatusBySegmentId.get(waveSegmentId)!;
|
|
@@ -985,6 +1064,7 @@ export async function resumeOrchBatch(
|
|
|
985
1064
|
agentRoot?: string,
|
|
986
1065
|
force: boolean = false,
|
|
987
1066
|
onSupervisorAlert?: import("./types.ts").SupervisorAlertCallback | null,
|
|
1067
|
+
supervisorAutonomy: "interactive" | "supervised" | "autonomous" = "autonomous",
|
|
988
1068
|
): Promise<void> {
|
|
989
1069
|
const repoRoot = cwd;
|
|
990
1070
|
// State files (.pi/batch-state.json, lane-state, etc.) belong in the workspace root,
|
|
@@ -1103,11 +1183,12 @@ export async function resumeOrchBatch(
|
|
|
1103
1183
|
});
|
|
1104
1184
|
}
|
|
1105
1185
|
|
|
1186
|
+
const runtimeWavePlan = buildResumeRuntimeWavePlan(persistedState);
|
|
1106
1187
|
// TP-108/112: Runtime V2 backend selection for resumed batches.
|
|
1107
1188
|
// MUST be computed before any backend-aware branch (section 3+).
|
|
1108
1189
|
const resumeBackend: RuntimeBackend = selectRuntimeBackend(
|
|
1109
1190
|
"all",
|
|
1110
|
-
|
|
1191
|
+
runtimeWavePlan,
|
|
1111
1192
|
workspaceConfig,
|
|
1112
1193
|
).backend;
|
|
1113
1194
|
execLog("resume", batchState.batchId, `runtime backend for resumed execution: ${resumeBackend}`);
|
|
@@ -1176,7 +1257,7 @@ export async function resumeOrchBatch(
|
|
|
1176
1257
|
}
|
|
1177
1258
|
|
|
1178
1259
|
// ── 5. Compute resume point ──────────────────────────────────
|
|
1179
|
-
const resumePoint = computeResumePoint(persistedState, reconciledTasks);
|
|
1260
|
+
const resumePoint = computeResumePoint(persistedState, reconciledTasks, runtimeWavePlan);
|
|
1180
1261
|
const completedTaskSet = new Set(resumePoint.completedTaskIds);
|
|
1181
1262
|
const failedTaskSet = new Set(resumePoint.failedTaskIds);
|
|
1182
1263
|
const reconnectTaskSet = new Set(resumePoint.reconnectTaskIds);
|
|
@@ -1263,8 +1344,8 @@ export async function resumeOrchBatch(
|
|
|
1263
1344
|
// entered, so they were never counted in blockedTasks.
|
|
1264
1345
|
if (persistedBlockedTaskIds.size > 0) {
|
|
1265
1346
|
let uncountedBlocked = 0;
|
|
1266
|
-
for (let wi = resumePoint.resumeWaveIndex; wi <
|
|
1267
|
-
for (const taskId of
|
|
1347
|
+
for (let wi = resumePoint.resumeWaveIndex; wi < runtimeWavePlan.length; wi++) {
|
|
1348
|
+
for (const taskId of runtimeWavePlan[wi]) {
|
|
1268
1349
|
if (persistedBlockedTaskIds.has(taskId)) {
|
|
1269
1350
|
uncountedBlocked++;
|
|
1270
1351
|
}
|
|
@@ -1284,6 +1365,8 @@ export async function resumeOrchBatch(
|
|
|
1284
1365
|
// v3: Carry forward resilience and diagnostics from persisted state
|
|
1285
1366
|
batchState.resilience = persistedState.resilience;
|
|
1286
1367
|
batchState.diagnostics = persistedState.diagnostics;
|
|
1368
|
+
// v4: Carry forward segment records (including dynamically expanded segments)
|
|
1369
|
+
batchState.segments = [...(persistedState.segments ?? [])];
|
|
1287
1370
|
// Carry forward unknown fields for roundtrip preservation
|
|
1288
1371
|
if (persistedState._extraFields) {
|
|
1289
1372
|
batchState._extraFields = persistedState._extraFields;
|
|
@@ -1303,6 +1386,28 @@ export async function resumeOrchBatch(
|
|
|
1303
1386
|
const depGraph = buildDependencyGraph(discovery.pending, discovery.completed);
|
|
1304
1387
|
batchState.dependencyGraph = depGraph;
|
|
1305
1388
|
|
|
1389
|
+
// Rehydrate discovered tasks with persisted segment metadata.
|
|
1390
|
+
// Dynamically expanded segments may reference tasks that have segment-level
|
|
1391
|
+
// fields (segmentIds, activeSegmentId, packetRepoId, packetTaskPath) set
|
|
1392
|
+
// during the prior run. Merge these back into discovered ParsedTask records
|
|
1393
|
+
// so execution can resume with correct segment context.
|
|
1394
|
+
for (const persistedTask of persistedState.tasks) {
|
|
1395
|
+
const parsed = discovery.pending.get(persistedTask.taskId);
|
|
1396
|
+
if (!parsed) continue;
|
|
1397
|
+
if (persistedTask.segmentIds?.length) {
|
|
1398
|
+
parsed.segmentIds = persistedTask.segmentIds;
|
|
1399
|
+
}
|
|
1400
|
+
if (persistedTask.activeSegmentId !== undefined) {
|
|
1401
|
+
parsed.activeSegmentId = persistedTask.activeSegmentId;
|
|
1402
|
+
}
|
|
1403
|
+
if (persistedTask.packetRepoId) {
|
|
1404
|
+
parsed.packetRepoId = persistedTask.packetRepoId;
|
|
1405
|
+
}
|
|
1406
|
+
if (persistedTask.packetTaskPath) {
|
|
1407
|
+
parsed.packetTaskPath = persistedTask.packetTaskPath;
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1306
1411
|
|
|
1307
1412
|
// ── 8. Handle alive sessions (reconnect) ─────────────────────
|
|
1308
1413
|
// For tasks with alive sessions, we need to wait for them to complete.
|
|
@@ -1578,7 +1683,11 @@ export async function resumeOrchBatch(
|
|
|
1578
1683
|
|
|
1579
1684
|
// ── 9. Persist state after reconciliation ────────────────────
|
|
1580
1685
|
// Track state for persistence
|
|
1581
|
-
const wavePlan =
|
|
1686
|
+
const wavePlan = runtimeWavePlan;
|
|
1687
|
+
persistedState.wavePlan = wavePlan;
|
|
1688
|
+
if (batchState.totalWaves < wavePlan.length) {
|
|
1689
|
+
batchState.totalWaves = wavePlan.length;
|
|
1690
|
+
}
|
|
1582
1691
|
const allTaskOutcomes: LaneTaskOutcome[] = [];
|
|
1583
1692
|
|
|
1584
1693
|
// Initialize latestAllocatedLanes from persisted lane records so that
|
|
@@ -1658,7 +1767,7 @@ export async function resumeOrchBatch(
|
|
|
1658
1767
|
persistedState.tasks.map((task) => [task.taskId, task.status] as const),
|
|
1659
1768
|
);
|
|
1660
1769
|
|
|
1661
|
-
for (let waveIdx = resumePoint.resumeWaveIndex; waveIdx <
|
|
1770
|
+
for (let waveIdx = resumePoint.resumeWaveIndex; waveIdx < wavePlan.length; waveIdx++) {
|
|
1662
1771
|
// Check pause signal
|
|
1663
1772
|
if (batchState.pauseSignal.paused) {
|
|
1664
1773
|
batchState.phase = "paused";
|
|
@@ -1672,7 +1781,7 @@ export async function resumeOrchBatch(
|
|
|
1672
1781
|
|
|
1673
1782
|
// Get wave tasks, filtering out completed/failed/skipped/blocked ones.
|
|
1674
1783
|
// Persisted "skipped" tasks are terminal and must never be re-executed.
|
|
1675
|
-
let waveTasks =
|
|
1784
|
+
let waveTasks = wavePlan[waveIdx].filter(
|
|
1676
1785
|
taskId => !completedTaskSet.has(taskId) &&
|
|
1677
1786
|
!failedTaskSet.has(taskId) &&
|
|
1678
1787
|
persistedStatusByTaskId.get(taskId) !== "skipped" &&
|
|
@@ -1685,7 +1794,7 @@ export async function resumeOrchBatch(
|
|
|
1685
1794
|
// Count only newly blocked tasks (not already persisted) to avoid double-counting.
|
|
1686
1795
|
// persistedState.blockedTaskIds were already counted in persistedState.blockedTasks
|
|
1687
1796
|
// which initialized batchState.blockedTasks.
|
|
1688
|
-
const blockedInWave =
|
|
1797
|
+
const blockedInWave = wavePlan[waveIdx].filter(
|
|
1689
1798
|
taskId => batchState.blockedTaskIds.has(taskId) &&
|
|
1690
1799
|
!persistedBlockedTaskIds.has(taskId),
|
|
1691
1800
|
);
|
|
@@ -1701,7 +1810,7 @@ export async function resumeOrchBatch(
|
|
|
1701
1810
|
onNotify(`🔀 Wave ${waveIdx + 1}: retrying merge (tasks already complete, merge was missing/failed)`, "info");
|
|
1702
1811
|
|
|
1703
1812
|
// Reconstruct lanes for this wave from persisted state
|
|
1704
|
-
const waveTaskIds = new Set(
|
|
1813
|
+
const waveTaskIds = new Set(wavePlan[waveIdx]);
|
|
1705
1814
|
const waveLaneRecords = persistedState.lanes.filter(
|
|
1706
1815
|
lane => lane.taskIds.some(tid => waveTaskIds.has(tid)),
|
|
1707
1816
|
);
|
|
@@ -1711,13 +1820,13 @@ export async function resumeOrchBatch(
|
|
|
1711
1820
|
// Crucial for orch_force_merge: tasks intentionally marked "skipped" must
|
|
1712
1821
|
// remain skipped here (not failed), otherwise mixed-outcome detection would
|
|
1713
1822
|
// trigger again and block the forced merge recovery path.
|
|
1714
|
-
const succeededTaskIds =
|
|
1823
|
+
const succeededTaskIds = wavePlan[waveIdx].filter(
|
|
1715
1824
|
taskId => completedTaskSet.has(taskId),
|
|
1716
1825
|
);
|
|
1717
|
-
const skippedTaskIds =
|
|
1826
|
+
const skippedTaskIds = wavePlan[waveIdx].filter(
|
|
1718
1827
|
taskId => persistedStatusByTaskId.get(taskId) === "skipped",
|
|
1719
1828
|
);
|
|
1720
|
-
const failedTaskIds =
|
|
1829
|
+
const failedTaskIds = wavePlan[waveIdx].filter(
|
|
1721
1830
|
taskId => {
|
|
1722
1831
|
const status = persistedStatusByTaskId.get(taskId);
|
|
1723
1832
|
return status === "failed" || status === "stalled";
|
|
@@ -1846,7 +1955,7 @@ export async function resumeOrchBatch(
|
|
|
1846
1955
|
}
|
|
1847
1956
|
|
|
1848
1957
|
onNotify(
|
|
1849
|
-
ORCH_MESSAGES.orchWaveStart(waveIdx + 1,
|
|
1958
|
+
ORCH_MESSAGES.orchWaveStart(waveIdx + 1, wavePlan.length, waveTasks.length, Math.min(waveTasks.length, orchConfig.orchestrator.max_lanes)),
|
|
1850
1959
|
"info",
|
|
1851
1960
|
);
|
|
1852
1961
|
|
|
@@ -1884,6 +1993,7 @@ export async function resumeOrchBatch(
|
|
|
1884
1993
|
workspaceConfig,
|
|
1885
1994
|
resumeBackend,
|
|
1886
1995
|
emitAlert,
|
|
1996
|
+
supervisorAutonomy,
|
|
1887
1997
|
);
|
|
1888
1998
|
|
|
1889
1999
|
batchState.waveResults.push(waveResult);
|