taskplane 0.24.22 → 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/extensions/taskplane/agent-bridge-extension.ts +196 -3
- 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 +6 -0
- package/extensions/taskplane/persistence.ts +12 -0
- package/extensions/taskplane/resume.ts +132 -22
- package/extensions/taskplane/types.ts +81 -16
- 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
|
};
|
|
@@ -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);
|
|
@@ -62,8 +62,13 @@ export interface OrchestratorConfig {
|
|
|
62
62
|
};
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
/**
|
|
66
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Stable segment identifier.
|
|
67
|
+
*
|
|
68
|
+
* SegmentId is opaque — never parse by string-splitting.
|
|
69
|
+
* Use structured node/record fields (`repoId`, `taskId`) instead.
|
|
70
|
+
*/
|
|
71
|
+
export type SegmentId = `${string}::${string}` | `${string}::${string}::${number}`;
|
|
67
72
|
|
|
68
73
|
/** How an intra-task segment edge was produced (for observability/debugging). */
|
|
69
74
|
export type SegmentEdgeProvenance = "explicit" | "inferred";
|
|
@@ -122,11 +127,31 @@ export interface ParsedTask {
|
|
|
122
127
|
activeSegmentId?: string | null;
|
|
123
128
|
}
|
|
124
129
|
|
|
125
|
-
/** Build a stable segment ID from task + repo identity (`<taskId>::<repoId
|
|
126
|
-
export function buildSegmentId(taskId: string, repoId: string): SegmentId {
|
|
130
|
+
/** Build a stable segment ID from task + repo identity (`<taskId>::<repoId>[::N]`). */
|
|
131
|
+
export function buildSegmentId(taskId: string, repoId: string, sequence?: number): SegmentId {
|
|
132
|
+
if (typeof sequence === "number" && Number.isFinite(sequence) && sequence >= 2) {
|
|
133
|
+
return `${taskId}::${repoId}::${Math.floor(sequence)}` as SegmentId;
|
|
134
|
+
}
|
|
127
135
|
return `${taskId}::${repoId}` as SegmentId;
|
|
128
136
|
}
|
|
129
137
|
|
|
138
|
+
/**
|
|
139
|
+
* Read repoId from structured segment metadata.
|
|
140
|
+
*
|
|
141
|
+
* SegmentId is opaque — never parse it by string-splitting.
|
|
142
|
+
*/
|
|
143
|
+
export function parseSegmentIdRepo(segment: { repoId: string }): string {
|
|
144
|
+
return segment.repoId;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Build a dynamic segment expansion request ID (`exp-{timestamp}-{random5}`). */
|
|
148
|
+
export function buildExpansionRequestId(timestamp = Date.now()): string {
|
|
149
|
+
const ts = Number.isFinite(timestamp) ? Math.floor(timestamp) : Date.now();
|
|
150
|
+
const base = Math.random().toString(36).slice(2).toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
151
|
+
const random5 = (base + "00000").slice(0, 5);
|
|
152
|
+
return `exp-${ts}-${random5}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
130
155
|
/** One repo-scoped segment node for a task. */
|
|
131
156
|
export interface TaskSegmentNode {
|
|
132
157
|
segmentId: SegmentId;
|
|
@@ -167,6 +192,36 @@ export interface TaskSegmentPlan {
|
|
|
167
192
|
mode: "explicit-dag" | "inferred-sequential" | "repo-singleton";
|
|
168
193
|
}
|
|
169
194
|
|
|
195
|
+
/** Directed edge between repos requested in a dynamic segment expansion. */
|
|
196
|
+
export interface SegmentExpansionEdge {
|
|
197
|
+
from: string;
|
|
198
|
+
to: string;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* File IPC payload for worker-initiated dynamic segment expansion requests.
|
|
203
|
+
*
|
|
204
|
+
* Written to: `.pi/mailbox/{batchId}/{agentId}/outbox/segment-expansion-{requestId}.json`
|
|
205
|
+
*/
|
|
206
|
+
export interface SegmentExpansionRequest {
|
|
207
|
+
/** Unique request ID: `exp-{timestamp}-{random5}` */
|
|
208
|
+
requestId: string;
|
|
209
|
+
/** Task ID making the expansion request. */
|
|
210
|
+
taskId: string;
|
|
211
|
+
/** Segment active when the request was emitted. */
|
|
212
|
+
fromSegmentId: SegmentId;
|
|
213
|
+
/** Repo IDs the worker is requesting the engine to add. */
|
|
214
|
+
requestedRepoIds: string[];
|
|
215
|
+
/** Human rationale from the worker. */
|
|
216
|
+
rationale: string;
|
|
217
|
+
/** Placement directive for inserting new segments. */
|
|
218
|
+
placement: "after-current" | "end";
|
|
219
|
+
/** Optional inter-request ordering edges. */
|
|
220
|
+
edges: SegmentExpansionEdge[];
|
|
221
|
+
/** Epoch milliseconds when the request was emitted. */
|
|
222
|
+
timestamp: number;
|
|
223
|
+
}
|
|
224
|
+
|
|
170
225
|
/**
|
|
171
226
|
* TaskId-keyed segment plans.
|
|
172
227
|
* Iteration order must be deterministic: sort task IDs lexicographically.
|
|
@@ -1934,17 +1989,27 @@ export type EngineEventCallback = (event: EngineEvent) => void;
|
|
|
1934
1989
|
* Alert category for supervisor notifications.
|
|
1935
1990
|
*
|
|
1936
1991
|
* Matches the alert categories in the autonomous supervisor spec:
|
|
1937
|
-
* - `task-failure`:
|
|
1938
|
-
* - `merge-failure`:
|
|
1939
|
-
* - `batch-complete`:
|
|
1940
|
-
* - `agent-message`:
|
|
1992
|
+
* - `task-failure`: A task failed after deterministic recovery was exhausted
|
|
1993
|
+
* - `merge-failure`: Wave merge failed and batch paused
|
|
1994
|
+
* - `batch-complete`: Batch finished (all waves done)
|
|
1995
|
+
* - `agent-message`: Runtime mailbox reply/escalation from a running agent
|
|
1996
|
+
* - `segment-expansion-requested`: Worker requested dynamic segment expansion
|
|
1997
|
+
* - `segment-expansion-approved`: Engine approved an expansion request
|
|
1998
|
+
* - `segment-expansion-rejected`: Engine rejected/discarded an expansion request
|
|
1941
1999
|
*
|
|
1942
2000
|
* Note: `stall` detection is deferred to a future phase (requires
|
|
1943
2001
|
* last-activity tracking not yet built).
|
|
1944
2002
|
*
|
|
1945
2003
|
* @since TP-076
|
|
1946
2004
|
*/
|
|
1947
|
-
export type SupervisorAlertCategory =
|
|
2005
|
+
export type SupervisorAlertCategory =
|
|
2006
|
+
| "task-failure"
|
|
2007
|
+
| "merge-failure"
|
|
2008
|
+
| "batch-complete"
|
|
2009
|
+
| "agent-message"
|
|
2010
|
+
| "segment-expansion-requested"
|
|
2011
|
+
| "segment-expansion-approved"
|
|
2012
|
+
| "segment-expansion-rejected";
|
|
1948
2013
|
|
|
1949
2014
|
/**
|
|
1950
2015
|
* Structured context payload for supervisor alerts.
|
|
@@ -1993,6 +2058,8 @@ export interface SupervisorAlertContext {
|
|
|
1993
2058
|
agentId?: string;
|
|
1994
2059
|
/** Mailbox message ID (for agent-message alerts) */
|
|
1995
2060
|
messageId?: string;
|
|
2061
|
+
/** Segment expansion request ID (for segment-expansion alerts) */
|
|
2062
|
+
expansionRequestId?: string;
|
|
1996
2063
|
/** Whether partial progress was preserved (for task-failure alerts) */
|
|
1997
2064
|
partialProgress?: boolean;
|
|
1998
2065
|
/** Batch progress summary */
|
|
@@ -2070,12 +2137,6 @@ export function buildBatchProgressSnapshot(
|
|
|
2070
2137
|
};
|
|
2071
2138
|
}
|
|
2072
2139
|
|
|
2073
|
-
function repoIdFromSegmentId(segmentId: string): string {
|
|
2074
|
-
const idx = segmentId.indexOf("::");
|
|
2075
|
-
if (idx <= 0 || idx >= segmentId.length - 2) return "unknown";
|
|
2076
|
-
return segmentId.slice(idx + 2);
|
|
2077
|
-
}
|
|
2078
|
-
|
|
2079
2140
|
/**
|
|
2080
2141
|
* Build a task-level segment frontier snapshot for supervisor failure alerts.
|
|
2081
2142
|
*
|
|
@@ -2112,7 +2173,7 @@ export function buildSupervisorSegmentFrontierSnapshot(
|
|
|
2112
2173
|
?? (resolvedActiveSegmentId === segmentId ? "running" : "pending");
|
|
2113
2174
|
return {
|
|
2114
2175
|
segmentId,
|
|
2115
|
-
repoId: persisted
|
|
2176
|
+
repoId: persisted ? parseSegmentIdRepo(persisted) : "unknown",
|
|
2116
2177
|
status,
|
|
2117
2178
|
dependsOnSegmentIds: persisted?.dependsOnSegmentIds ?? [],
|
|
2118
2179
|
};
|
|
@@ -2692,6 +2753,10 @@ export interface PersistedSegmentRecord {
|
|
|
2692
2753
|
exitDiagnostic?: TaskExitDiagnostic;
|
|
2693
2754
|
/** Human-readable exit reason (legacy compat, same as task-level) */
|
|
2694
2755
|
exitReason: string;
|
|
2756
|
+
/** Anchor segment ID this segment was dynamically expanded from (if any). */
|
|
2757
|
+
expandedFrom?: string;
|
|
2758
|
+
/** Segment expansion request ID that created this segment (if any). */
|
|
2759
|
+
expansionRequestId?: string;
|
|
2695
2760
|
}
|
|
2696
2761
|
|
|
2697
2762
|
/**
|