taskplane 0.24.30 → 0.25.0
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 +41 -48
- package/bin/taskplane.mjs +51 -147
- package/dashboard/public/app.js +32 -18
- package/dashboard/public/style.css +4 -5
- package/dashboard/server.cjs +3 -0
- package/extensions/taskplane/engine-worker.ts +5 -0
- package/extensions/taskplane/engine.ts +160 -6
- package/extensions/taskplane/execution.ts +38 -9
- package/extensions/taskplane/extension.ts +33 -1
- package/extensions/taskplane/lane-runner.ts +46 -1
- package/extensions/taskplane/settings-tui.ts +81 -48
- package/extensions/taskplane/supervisor.ts +74 -40
- package/extensions/taskplane/types.ts +1 -1
- package/extensions/taskplane/waves.ts +108 -2
- package/extensions/taskplane/worktree.ts +114 -0
- package/package.json +1 -1
- package/templates/agents/supervisor.md +1 -1
- package/templates/agents/task-worker.md +7 -0
- package/templates/config/task-orchestrator.yaml +0 -93
- package/templates/config/task-runner.yaml +0 -94
package/dashboard/server.cjs
CHANGED
|
@@ -1118,6 +1118,9 @@ function buildDashboardState() {
|
|
|
1118
1118
|
// Workspace mode: "repo" (default/v1) or "workspace" (v2 multi-repo).
|
|
1119
1119
|
// Additive field — absent in v1 state files, frontend must default to "repo".
|
|
1120
1120
|
mode: state.mode || "repo",
|
|
1121
|
+
// TP-148: Segment records for wave display context (v4+).
|
|
1122
|
+
// Each record has taskId, segmentId, repoId, status.
|
|
1123
|
+
segments: state.segments || [],
|
|
1121
1124
|
},
|
|
1122
1125
|
sessions,
|
|
1123
1126
|
tmuxSessions: sessions, // Legacy compatibility field for older dashboard clients
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* @module orch/engine-worker
|
|
17
17
|
*/
|
|
18
18
|
import type {
|
|
19
|
+
AllocatedLane,
|
|
19
20
|
EngineEvent,
|
|
20
21
|
MonitorState,
|
|
21
22
|
OrchBatchPhase,
|
|
@@ -71,6 +72,8 @@ export interface SerializedBatchState {
|
|
|
71
72
|
startedAt: number;
|
|
72
73
|
endedAt: number | null;
|
|
73
74
|
errors: string[];
|
|
75
|
+
/** Active lanes for the current wave (synced so /orch-sessions works). */
|
|
76
|
+
currentLanes: AllocatedLane[];
|
|
74
77
|
}
|
|
75
78
|
|
|
76
79
|
/**
|
|
@@ -164,6 +167,7 @@ function serializeBatchState(state: OrchBatchRuntimeState): SerializedBatchState
|
|
|
164
167
|
startedAt: state.startedAt,
|
|
165
168
|
endedAt: state.endedAt,
|
|
166
169
|
errors: [...state.errors],
|
|
170
|
+
currentLanes: state.currentLanes,
|
|
167
171
|
};
|
|
168
172
|
}
|
|
169
173
|
|
|
@@ -192,6 +196,7 @@ export function applySerializedState(
|
|
|
192
196
|
batchState.startedAt = serialized.startedAt;
|
|
193
197
|
batchState.endedAt = serialized.endedAt;
|
|
194
198
|
batchState.errors = [...serialized.errors];
|
|
199
|
+
batchState.currentLanes = serialized.currentLanes ?? [];
|
|
195
200
|
}
|
|
196
201
|
|
|
197
202
|
// ── Engine main (runs when launched as a forked child process) ───────
|
|
@@ -22,7 +22,7 @@ import { readRegistrySnapshot, isTerminalStatus, isProcessAlive as registryIsPro
|
|
|
22
22
|
import { buildBatchProgressSnapshot, buildEngineEventBase, buildSegmentId, buildSupervisorSegmentFrontierSnapshot, defaultResilienceState, FATAL_DISCOVERY_CODES, generateBatchId, TIER0_RETRYABLE_CLASSIFICATIONS, TIER0_RETRY_BUDGETS, tier0ScopeKey, tier0WaveScopeKey } from "./types.ts";
|
|
23
23
|
import type { AllocatedLane, AllocatedTask, BatchHistorySummary, BatchTaskSummary, BatchWaveSummary, DiscoveryResult, EngineEventCallback, EscalationContext, LaneExecutionResult, LaneTaskOutcome, MergeWaveResult, OrchBatchPhase, OrchBatchRuntimeState, OrchestratorConfig, ParsedTask, PersistedSegmentRecord, SegmentExpansionRequest, SupervisorAlert, SupervisorAlertCallback, TaskRunnerConfig, TaskSegmentPlan, TaskSegmentPlanMap, TaskSegmentNode, Tier0EscalationPattern, Tier0RecoveryPattern, TokenCounts, WaveExecutionResult, WorkspaceConfig } from "./types.ts";
|
|
24
24
|
import { buildDependencyGraph, computeWaveAssignments, resolveBaseBranch, resolveRepoRoot, validateGraph } from "./waves.ts";
|
|
25
|
-
import { deleteBranchBestEffort, forceCleanupWorktree, formatPreflightResults, listWorktrees, preserveFailedLaneProgress, removeAllWorktrees, removeWorktree, runPreflight, safeResetWorktree, sleepSync } from "./worktree.ts";
|
|
25
|
+
import { deleteBranchBestEffort, forceCleanupWorktree, formatPreflightResults, listWorktrees, preserveFailedLaneProgress, preserveSkippedLaneProgress, removeAllWorktrees, removeWorktree, runPreflight, safeResetWorktree, sleepSync } from "./worktree.ts";
|
|
26
26
|
import { runPreflightCleanup, formatPreflightCleanup } from "./cleanup.ts";
|
|
27
27
|
|
|
28
28
|
// ── Tier 0: Automatic Recovery Helpers (TP-039) ─────────────────────
|
|
@@ -346,9 +346,28 @@ export function validateSegmentExpansionRequestAtBoundary(
|
|
|
346
346
|
if (requestedRepoSet.size !== request.requestedRepoIds.length) {
|
|
347
347
|
return "duplicate repoIds in requestedRepoIds";
|
|
348
348
|
}
|
|
349
|
+
|
|
350
|
+
// TP-145: Build a set of known repo IDs that edge endpoints may reference.
|
|
351
|
+
// This includes all requestedRepoIds plus the anchor segment's repo and
|
|
352
|
+
// any already-completed segments' repos. Workers commonly reference the
|
|
353
|
+
// anchor repo in edges (e.g., { from: "shared-libs", to: "web-client" })
|
|
354
|
+
// which is valid — the dependency is implicit for after-current placement.
|
|
355
|
+
const knownEdgeRepoIds = new Set(requestedRepoSet);
|
|
356
|
+
const orderedSegments = segmentState.orderedSegments ?? [];
|
|
357
|
+
const anchorSegment = orderedSegments.find((seg) => seg.segmentId === segmentId);
|
|
358
|
+
if (anchorSegment) {
|
|
359
|
+
knownEdgeRepoIds.add(anchorSegment.repoId);
|
|
360
|
+
}
|
|
361
|
+
for (const seg of orderedSegments) {
|
|
362
|
+
const status = segmentState.statusBySegmentId?.get(seg.segmentId);
|
|
363
|
+
if (status === "succeeded" || status === "failed" || status === "skipped") {
|
|
364
|
+
knownEdgeRepoIds.add(seg.repoId);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
349
368
|
for (const edge of request.edges) {
|
|
350
|
-
if (!
|
|
351
|
-
return "edge references a repo outside requestedRepoIds";
|
|
369
|
+
if (!knownEdgeRepoIds.has(edge.from) || !knownEdgeRepoIds.has(edge.to)) {
|
|
370
|
+
return "edge references a repo outside requestedRepoIds and known segments";
|
|
352
371
|
}
|
|
353
372
|
}
|
|
354
373
|
|
|
@@ -2289,10 +2308,29 @@ export async function executeOrchBatch(
|
|
|
2289
2308
|
ORCH_MESSAGES.orchWaveStart(waveIdx + 1, runtimeSegmentRounds.length, waveTasks.length, lanes.length),
|
|
2290
2309
|
"info",
|
|
2291
2310
|
);
|
|
2311
|
+
// TP-148: Build per-task segment context for the wave_start event
|
|
2312
|
+
const waveSegmentContext: Array<{ taskId: string; segmentIndex: number; totalSegments: number; repoId: string; segmentId: string }> = [];
|
|
2313
|
+
for (const taskId of waveTasks) {
|
|
2314
|
+
const segState = segmentStateByTask.get(taskId);
|
|
2315
|
+
if (segState && segState.orderedSegments.length > 1) {
|
|
2316
|
+
const idx = segState.nextSegmentIndex;
|
|
2317
|
+
const seg = segState.orderedSegments[idx];
|
|
2318
|
+
if (seg) {
|
|
2319
|
+
waveSegmentContext.push({
|
|
2320
|
+
taskId,
|
|
2321
|
+
segmentIndex: idx + 1,
|
|
2322
|
+
totalSegments: segState.orderedSegments.length,
|
|
2323
|
+
repoId: seg.repoId,
|
|
2324
|
+
segmentId: seg.segmentId,
|
|
2325
|
+
});
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2292
2329
|
emitEvent(stateRoot, {
|
|
2293
2330
|
...buildEngineEventBase("wave_start", batchState.batchId, waveIdx, batchState.phase),
|
|
2294
2331
|
taskIds: waveTasks,
|
|
2295
2332
|
laneCount: lanes.length,
|
|
2333
|
+
...(waveSegmentContext.length > 0 ? { segmentContext: waveSegmentContext } : {}),
|
|
2296
2334
|
}, onEngineEvent);
|
|
2297
2335
|
// TP-029: Track repos from newly allocated lanes for cleanup coverage
|
|
2298
2336
|
for (const lane of lanes) {
|
|
@@ -2612,6 +2650,26 @@ export async function executeOrchBatch(
|
|
|
2612
2650
|
batchState.orchBranch,
|
|
2613
2651
|
);
|
|
2614
2652
|
const recordedRequestId = recordProcessedSegmentExpansionRequestId(batchState, requestId, "succeeded");
|
|
2653
|
+
|
|
2654
|
+
// TP-145 hardening: if .DONE was prematurely created by the
|
|
2655
|
+
// completing segment (because it was the last segment at that
|
|
2656
|
+
// time), remove it now. The task is no longer complete — new
|
|
2657
|
+
// segments have been added and must execute first.
|
|
2658
|
+
// Only delete if segments were actually inserted (avoid
|
|
2659
|
+
// reopening a completed task on no-op mutations).
|
|
2660
|
+
const doneDir = task.packetTaskPath || task.taskFolder;
|
|
2661
|
+
if (doneDir && mutation.insertedSegmentIds.length > 0) {
|
|
2662
|
+
const donePath = join(doneDir, ".DONE");
|
|
2663
|
+
if (existsSync(donePath)) {
|
|
2664
|
+
try {
|
|
2665
|
+
unlinkSync(donePath);
|
|
2666
|
+
execLog("batch", batchState.batchId, "removed premature .DONE after segment expansion", {
|
|
2667
|
+
taskId, donePath, requestId,
|
|
2668
|
+
});
|
|
2669
|
+
} catch { /* non-fatal */ }
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
|
|
2615
2673
|
if (persistedInsertedSegments || recordedRequestId || mutation.insertedSegmentIds.length > 0) {
|
|
2616
2674
|
persistRuntimeState("segment-expansion-approved", batchState, wavePlan, latestAllocatedLanes, allTaskOutcomes, discoveryRef, stateRoot);
|
|
2617
2675
|
}
|
|
@@ -2915,13 +2973,16 @@ export async function executeOrchBatch(
|
|
|
2915
2973
|
// files (especially for level-0 / fast tasks). Check each merge-candidate
|
|
2916
2974
|
// lane worktree and auto-commit any remaining changes so they're included
|
|
2917
2975
|
// in the merge. Skips lanes with only failed/stalled tasks (no merge).
|
|
2976
|
+
// TP-147: Also auto-commit skipped-task lanes so partial progress
|
|
2977
|
+
// (STATUS.md updates, partial code) is preserved on their branch.
|
|
2918
2978
|
for (const lane of waveResult.allocatedLanes) {
|
|
2919
2979
|
if (!lane.worktreePath || !existsSync(lane.worktreePath)) continue;
|
|
2920
|
-
// Only check lanes that have at least one succeeded task (merge candidates)
|
|
2921
2980
|
const laneOutcome = laneOutcomeByNumber.get(lane.laneNumber);
|
|
2922
2981
|
if (!laneOutcome) continue;
|
|
2923
2982
|
const hasSucceeded = laneOutcome.tasks.some(t => t.status === "succeeded");
|
|
2924
|
-
|
|
2983
|
+
const hasSkipped = laneOutcome.tasks.some(t => t.status === "skipped");
|
|
2984
|
+
// Auto-commit merge candidates (succeeded) and skipped-task lanes
|
|
2985
|
+
if (!hasSucceeded && !hasSkipped) continue;
|
|
2925
2986
|
try {
|
|
2926
2987
|
const addResult = runGit(["add", "-A"], lane.worktreePath);
|
|
2927
2988
|
if (!addResult.ok) {
|
|
@@ -3465,6 +3526,34 @@ export async function executeOrchBatch(
|
|
|
3465
3526
|
}
|
|
3466
3527
|
// TP-028: Stamp task outcomes with partial progress data for persistence
|
|
3467
3528
|
applyPartialProgressToOutcomes(ppResult, allTaskOutcomes);
|
|
3529
|
+
|
|
3530
|
+
// TP-147: Also preserve skipped task branches before inter-wave reset
|
|
3531
|
+
const skippedPpResult = preserveSkippedLaneProgress(
|
|
3532
|
+
latestAllocatedLanes,
|
|
3533
|
+
allTaskOutcomes,
|
|
3534
|
+
ppOpId,
|
|
3535
|
+
batchState.batchId,
|
|
3536
|
+
(repoId) => {
|
|
3537
|
+
const perRepoRoot = resolveRepoRoot(repoId, repoRoot, workspaceConfig);
|
|
3538
|
+
let targetBranch = batchState.orchBranch;
|
|
3539
|
+
if (repoId && perRepoRoot !== repoRoot) {
|
|
3540
|
+
try {
|
|
3541
|
+
targetBranch = resolveBaseBranch(repoId, perRepoRoot, batchState.orchBranch, workspaceConfig);
|
|
3542
|
+
} catch { /* fall back to orchBranch */ }
|
|
3543
|
+
}
|
|
3544
|
+
return { repoRoot: perRepoRoot, targetBranch };
|
|
3545
|
+
},
|
|
3546
|
+
);
|
|
3547
|
+
// Merge unsafe branches from skipped tasks into the main set
|
|
3548
|
+
for (const branch of skippedPpResult.unsafeBranches) {
|
|
3549
|
+
ppUnsafeBranches.add(branch);
|
|
3550
|
+
}
|
|
3551
|
+
if (skippedPpResult.results.some(r => r.saved)) {
|
|
3552
|
+
execLog("batch", batchState.batchId,
|
|
3553
|
+
`preserved partial progress for ${skippedPpResult.results.filter(r => r.saved).length} skipped task(s) before inter-wave reset`);
|
|
3554
|
+
}
|
|
3555
|
+
// Stamp skipped task outcomes with partial progress data
|
|
3556
|
+
applyPartialProgressToOutcomes(skippedPpResult, allTaskOutcomes);
|
|
3468
3557
|
}
|
|
3469
3558
|
|
|
3470
3559
|
// ── Post-merge: Reset worktrees for next wave ────────────
|
|
@@ -3811,6 +3900,30 @@ export async function executeOrchBatch(
|
|
|
3811
3900
|
};
|
|
3812
3901
|
});
|
|
3813
3902
|
|
|
3903
|
+
// TP-147: Ensure ALL tasks from the wave plan are represented in history.
|
|
3904
|
+
// Tasks that never got allocated (blocked by upstream failures, never started)
|
|
3905
|
+
// won't have entries in allTaskOutcomes. Add them with appropriate status.
|
|
3906
|
+
const coveredTaskIds = new Set(taskSummaries.map(t => t.taskId));
|
|
3907
|
+
for (let wi = 0; wi < wavePlan.length; wi++) {
|
|
3908
|
+
for (const taskId of wavePlan[wi]) {
|
|
3909
|
+
if (coveredTaskIds.has(taskId)) continue;
|
|
3910
|
+
// Determine the appropriate status for uncovered tasks
|
|
3911
|
+
const isBlocked = batchState.blockedTaskIds.has(taskId);
|
|
3912
|
+
const status: BatchTaskSummary["status"] = isBlocked ? "blocked" : "pending";
|
|
3913
|
+
taskSummaries.push({
|
|
3914
|
+
taskId,
|
|
3915
|
+
taskName: taskId,
|
|
3916
|
+
status,
|
|
3917
|
+
wave: wi + 1,
|
|
3918
|
+
lane: 0,
|
|
3919
|
+
durationMs: 0,
|
|
3920
|
+
tokens: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, costUsd: 0 },
|
|
3921
|
+
exitReason: isBlocked ? "Blocked by upstream failure" : null,
|
|
3922
|
+
});
|
|
3923
|
+
coveredTaskIds.add(taskId);
|
|
3924
|
+
}
|
|
3925
|
+
}
|
|
3926
|
+
|
|
3814
3927
|
// Build per-wave summaries
|
|
3815
3928
|
const waveSummaries: BatchWaveSummary[] = wavePlan.map((taskIds, wi) => {
|
|
3816
3929
|
const waveTasks = taskSummaries.filter(t => t.wave === wi + 1);
|
|
@@ -3852,6 +3965,16 @@ export async function executeOrchBatch(
|
|
|
3852
3965
|
? "completed"
|
|
3853
3966
|
: "aborted";
|
|
3854
3967
|
|
|
3968
|
+
// TP-147: Ensure totalTasks matches actual task array length.
|
|
3969
|
+
// Use taskSummaries.length as authoritative (includes gap-filled tasks)
|
|
3970
|
+
// and log a warning if it diverges from batchState.totalTasks.
|
|
3971
|
+
const actualTotalTasks = taskSummaries.length;
|
|
3972
|
+
if (actualTotalTasks !== batchState.totalTasks) {
|
|
3973
|
+
execLog("batch", batchState.batchId,
|
|
3974
|
+
`WARNING: totalTasks mismatch — batchState.totalTasks=${batchState.totalTasks}, ` +
|
|
3975
|
+
`taskSummaries.length=${actualTotalTasks}. Using taskSummaries.length for history.`);
|
|
3976
|
+
}
|
|
3977
|
+
|
|
3855
3978
|
const summary: BatchHistorySummary = {
|
|
3856
3979
|
batchId: batchState.batchId,
|
|
3857
3980
|
status: historyStatus,
|
|
@@ -3859,7 +3982,7 @@ export async function executeOrchBatch(
|
|
|
3859
3982
|
endedAt: Date.now(),
|
|
3860
3983
|
durationMs: Date.now() - batchState.startedAt,
|
|
3861
3984
|
totalWaves: wavePlan.length,
|
|
3862
|
-
totalTasks:
|
|
3985
|
+
totalTasks: actualTotalTasks,
|
|
3863
3986
|
succeededTasks: batchState.succeededTasks,
|
|
3864
3987
|
failedTasks: batchState.failedTasks,
|
|
3865
3988
|
skippedTasks: batchState.skippedTasks,
|
|
@@ -3983,6 +4106,37 @@ export async function executeOrchBatch(
|
|
|
3983
4106
|
}
|
|
3984
4107
|
// TP-028: Stamp task outcomes with partial progress data for persistence
|
|
3985
4108
|
applyPartialProgressToOutcomes(ppResult, allTaskOutcomes);
|
|
4109
|
+
|
|
4110
|
+
// TP-147: Also preserve skipped task branches before terminal cleanup
|
|
4111
|
+
const skippedPpResult = preserveSkippedLaneProgress(
|
|
4112
|
+
latestAllocatedLanes,
|
|
4113
|
+
allTaskOutcomes,
|
|
4114
|
+
ppOpId,
|
|
4115
|
+
batchState.batchId,
|
|
4116
|
+
(repoId) => {
|
|
4117
|
+
const perRepoRoot = resolveRepoRoot(repoId, repoRoot, workspaceConfig);
|
|
4118
|
+
let targetBranch = batchState.orchBranch;
|
|
4119
|
+
if (repoId && perRepoRoot !== repoRoot) {
|
|
4120
|
+
try {
|
|
4121
|
+
targetBranch = resolveBaseBranch(repoId, perRepoRoot, batchState.orchBranch, workspaceConfig);
|
|
4122
|
+
} catch { /* fall back to orchBranch */ }
|
|
4123
|
+
}
|
|
4124
|
+
return { repoRoot: perRepoRoot, targetBranch };
|
|
4125
|
+
},
|
|
4126
|
+
);
|
|
4127
|
+
if (skippedPpResult.results.some(r => r.saved)) {
|
|
4128
|
+
execLog("batch", batchState.batchId,
|
|
4129
|
+
`preserved partial progress for ${skippedPpResult.results.filter(r => r.saved).length} skipped task(s) before terminal cleanup`);
|
|
4130
|
+
}
|
|
4131
|
+
for (const r of skippedPpResult.results) {
|
|
4132
|
+
if (!r.saved && (r.commitCount > 0 || r.error)) {
|
|
4133
|
+
execLog("batch", batchState.batchId,
|
|
4134
|
+
`WARNING: Failed to preserve partial progress for skipped task ${r.taskId} ` +
|
|
4135
|
+
`(${r.commitCount} commit(s) may become unreachable after cleanup)`,
|
|
4136
|
+
{ taskId: r.taskId, commitCount: r.commitCount, error: r.error ?? "unknown" });
|
|
4137
|
+
}
|
|
4138
|
+
}
|
|
4139
|
+
applyPartialProgressToOutcomes(skippedPpResult, allTaskOutcomes);
|
|
3986
4140
|
}
|
|
3987
4141
|
|
|
3988
4142
|
// TP-029: Clean up worktrees across ALL encountered repos (not just primary).
|
|
@@ -164,7 +164,7 @@ export function execLog(
|
|
|
164
164
|
* @returns true if agent is alive
|
|
165
165
|
* @since TP-112
|
|
166
166
|
*/
|
|
167
|
-
export function isV2AgentAlive(agentIdOrSessionName: string, _runtimeBackend?: RuntimeBackend): boolean {
|
|
167
|
+
export function isV2AgentAlive(agentIdOrSessionName: string, _runtimeBackend?: RuntimeBackend, laneNumber?: number): boolean {
|
|
168
168
|
// Read the registry from the global state root.
|
|
169
169
|
// Since this is a pure liveness check, we scan for matching agentId
|
|
170
170
|
// patterns: direct match, or lane-session + "-worker" suffix.
|
|
@@ -176,6 +176,18 @@ export function isV2AgentAlive(agentIdOrSessionName: string, _runtimeBackend?: R
|
|
|
176
176
|
// Try worker suffix (monitor uses lane session name, registry uses agentId)
|
|
177
177
|
const workerManifest = agents[`${agentIdOrSessionName}-worker`];
|
|
178
178
|
if (workerManifest && !isTerminalStatus(workerManifest.status) && isProcessAlive(workerManifest.pid)) return true;
|
|
179
|
+
// TP-148: In workspace mode, laneSessionId includes repoId and uses a local
|
|
180
|
+
// lane number (e.g., "orch-henry-api-lane-1") while the V2 registry uses
|
|
181
|
+
// global lane numbers without repoId (e.g., "orch-henry-lane-3-worker").
|
|
182
|
+
// Fall back to scanning the registry by global lane number when provided.
|
|
183
|
+
if (laneNumber != null) {
|
|
184
|
+
for (const agent of Object.values(agents)) {
|
|
185
|
+
if (agent.laneNumber === laneNumber && agent.role === "worker" &&
|
|
186
|
+
!isTerminalStatus(agent.status) && isProcessAlive(agent.pid)) {
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
179
191
|
return false;
|
|
180
192
|
}
|
|
181
193
|
|
|
@@ -201,7 +213,7 @@ export function setV2LivenessRegistryCache(registry: import("./process-registry.
|
|
|
201
213
|
*/
|
|
202
214
|
export function killV2LaneAgents(
|
|
203
215
|
sessionName: string,
|
|
204
|
-
options?: { stateRoot?: string; batchId?: string; logContext?: string },
|
|
216
|
+
options?: { stateRoot?: string; batchId?: string; logContext?: string; laneNumber?: number },
|
|
205
217
|
): void {
|
|
206
218
|
const registry = _v2LivenessRegistryCache ?? (
|
|
207
219
|
options?.stateRoot && options?.batchId
|
|
@@ -212,16 +224,32 @@ export function killV2LaneAgents(
|
|
|
212
224
|
|
|
213
225
|
const agents = registry.agents;
|
|
214
226
|
const logContext = options?.logContext ?? "monitor";
|
|
227
|
+
const killedPids = new Set<number>();
|
|
215
228
|
for (const suffix of ["-worker", "-reviewer", ""]) {
|
|
216
229
|
const key = `${sessionName}${suffix}`;
|
|
217
230
|
const manifest = agents[key];
|
|
218
|
-
if (manifest && !isTerminalStatus(manifest.status) && isProcessAlive(manifest.pid)) {
|
|
231
|
+
if (manifest && !isTerminalStatus(manifest.status) && isProcessAlive(manifest.pid) && !killedPids.has(manifest.pid)) {
|
|
219
232
|
try {
|
|
220
233
|
process.kill(manifest.pid, "SIGTERM");
|
|
234
|
+
killedPids.add(manifest.pid);
|
|
221
235
|
execLog(logContext, key, `killed V2 agent (PID ${manifest.pid})`);
|
|
222
236
|
} catch { /* already dead */ }
|
|
223
237
|
}
|
|
224
238
|
}
|
|
239
|
+
// TP-148: Workspace-mode fallback — match by global lane number when
|
|
240
|
+
// session name lookup misses (repoId/local-vs-global lane mismatch).
|
|
241
|
+
if (options?.laneNumber != null) {
|
|
242
|
+
for (const agent of Object.values(agents)) {
|
|
243
|
+
if (agent.laneNumber === options.laneNumber &&
|
|
244
|
+
!isTerminalStatus(agent.status) && isProcessAlive(agent.pid) && !killedPids.has(agent.pid)) {
|
|
245
|
+
try {
|
|
246
|
+
process.kill(agent.pid, "SIGTERM");
|
|
247
|
+
killedPids.add(agent.pid);
|
|
248
|
+
execLog(logContext, agent.agentId, `killed V2 agent by lane number (PID ${agent.pid})`);
|
|
249
|
+
} catch { /* already dead */ }
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
225
253
|
}
|
|
226
254
|
|
|
227
255
|
// ── Async File/Status Helpers (TP-070) ───────────────────────────────
|
|
@@ -877,7 +905,7 @@ export async function resolveTaskMonitorState(
|
|
|
877
905
|
// New task, stale snapshot — give the worker startup grace period
|
|
878
906
|
sessionAlive = true;
|
|
879
907
|
} else {
|
|
880
|
-
sessionAlive = isV2AgentAlive(sessionName, runtimeBackend);
|
|
908
|
+
sessionAlive = isV2AgentAlive(sessionName, runtimeBackend, v2Context?.laneNumber);
|
|
881
909
|
}
|
|
882
910
|
} else {
|
|
883
911
|
sessionAlive = true;
|
|
@@ -886,7 +914,7 @@ export async function resolveTaskMonitorState(
|
|
|
886
914
|
sessionAlive = snap.status === "running";
|
|
887
915
|
}
|
|
888
916
|
} else {
|
|
889
|
-
sessionAlive = isV2AgentAlive(sessionName, "v2");
|
|
917
|
+
sessionAlive = isV2AgentAlive(sessionName, "v2", v2Context?.laneNumber);
|
|
890
918
|
}
|
|
891
919
|
const doneFileFound = await fileExistsAsync(donePath);
|
|
892
920
|
|
|
@@ -984,7 +1012,7 @@ export async function resolveTaskMonitorState(
|
|
|
984
1012
|
stallMinutes,
|
|
985
1013
|
backend: runtimeBackend ?? "legacy",
|
|
986
1014
|
});
|
|
987
|
-
killV2LaneAgents(sessionName);
|
|
1015
|
+
killV2LaneAgents(sessionName, { laneNumber: v2Context?.laneNumber });
|
|
988
1016
|
|
|
989
1017
|
return {
|
|
990
1018
|
taskId,
|
|
@@ -1249,7 +1277,8 @@ export async function monitorLanes(
|
|
|
1249
1277
|
}
|
|
1250
1278
|
|
|
1251
1279
|
// TP-112: Backend-aware lane liveness for snapshot
|
|
1252
|
-
|
|
1280
|
+
// TP-148: Pass global laneNumber for workspace-mode fallback lookup
|
|
1281
|
+
const sessionAlive = isV2AgentAlive(laneSessionIdOf(lane), "v2", lane.laneNumber);
|
|
1253
1282
|
|
|
1254
1283
|
laneSnapshots.push({
|
|
1255
1284
|
laneId: lane.laneId,
|
|
@@ -1874,7 +1903,7 @@ export async function executeWithStopAll(
|
|
|
1874
1903
|
|
|
1875
1904
|
// Kill ALL lane sessions immediately
|
|
1876
1905
|
for (const lane of lanes) {
|
|
1877
|
-
killV2LaneAgents(laneSessionIdOf(lane));
|
|
1906
|
+
killV2LaneAgents(laneSessionIdOf(lane), { laneNumber: lane.laneNumber });
|
|
1878
1907
|
}
|
|
1879
1908
|
}
|
|
1880
1909
|
}
|
|
@@ -1888,7 +1917,7 @@ export async function executeWithStopAll(
|
|
|
1888
1917
|
pauseSignal.paused = true;
|
|
1889
1918
|
execLog("wave", `W${waveIndex}`, `stop-all triggered by lane error in ${lanes[idx].laneId}: ${errMsg}`);
|
|
1890
1919
|
for (const lane of lanes) {
|
|
1891
|
-
killV2LaneAgents(laneSessionIdOf(lane));
|
|
1920
|
+
killV2LaneAgents(laneSessionIdOf(lane), { laneNumber: lane.laneNumber });
|
|
1892
1921
|
}
|
|
1893
1922
|
}
|
|
1894
1923
|
|
|
@@ -4720,7 +4720,39 @@ export default function (pi: ExtensionAPI) {
|
|
|
4720
4720
|
if (!requireExecCtx(ctx)) return;
|
|
4721
4721
|
|
|
4722
4722
|
try {
|
|
4723
|
-
|
|
4723
|
+
// Capture the workspace root for reload consistency — use the same
|
|
4724
|
+
// root the settings TUI writes to, not ctx.cwd which may differ.
|
|
4725
|
+
const reloadCwd = execCtx!.workspaceRoot;
|
|
4726
|
+
await openSettingsTui(ctx, execCtx!.workspaceRoot, execCtx!.pointer?.configRoot, () => {
|
|
4727
|
+
// Reload live config from disk so changes take effect immediately
|
|
4728
|
+
// without requiring a session restart.
|
|
4729
|
+
// Build everything into temporaries first, then commit atomically
|
|
4730
|
+
// so a partial failure doesn't leave mixed-generation state.
|
|
4731
|
+
try {
|
|
4732
|
+
const freshCtx = buildExecutionContext(reloadCwd, loadOrchestratorConfig, loadTaskRunnerConfig);
|
|
4733
|
+
let freshSupervisor: SupervisorConfig;
|
|
4734
|
+
try {
|
|
4735
|
+
freshSupervisor = loadSupervisorConfig(
|
|
4736
|
+
freshCtx.repoRoot,
|
|
4737
|
+
freshCtx.pointer?.configRoot,
|
|
4738
|
+
);
|
|
4739
|
+
} catch {
|
|
4740
|
+
freshSupervisor = { ...DEFAULT_SUPERVISOR_CONFIG };
|
|
4741
|
+
}
|
|
4742
|
+
// Atomic commit — all or nothing
|
|
4743
|
+
execCtx = freshCtx;
|
|
4744
|
+
orchConfig = freshCtx.orchestratorConfig;
|
|
4745
|
+
runnerConfig = freshCtx.taskRunnerConfig;
|
|
4746
|
+
supervisorConfig = freshSupervisor;
|
|
4747
|
+
} catch {
|
|
4748
|
+
// Non-fatal — config was saved to disk but live reload failed.
|
|
4749
|
+
// Existing in-memory config is preserved unchanged.
|
|
4750
|
+
ctx.ui.notify(
|
|
4751
|
+
"⚠️ Saved to disk but live reload failed. Restart to apply.",
|
|
4752
|
+
"warn",
|
|
4753
|
+
);
|
|
4754
|
+
}
|
|
4755
|
+
});
|
|
4724
4756
|
} catch (err: any) {
|
|
4725
4757
|
ctx.ui.notify(`❌ Failed to load settings: ${err.message}`, "error");
|
|
4726
4758
|
}
|
|
@@ -176,6 +176,19 @@ export async function executeTaskV2(
|
|
|
176
176
|
updateStatusField(statusPath, "Last Updated", new Date().toISOString().slice(0, 10));
|
|
177
177
|
logExecution(statusPath, "Task started", "Runtime V2 lane-runner execution");
|
|
178
178
|
|
|
179
|
+
// Pre-segment guard: remove any stale .DONE from a prior segment or prior run.
|
|
180
|
+
// This closes the race window where the monitor sees .DONE before lane-runner
|
|
181
|
+
// can suppress it at segment end. For non-final segments, .DONE must not exist
|
|
182
|
+
// at any point during execution.
|
|
183
|
+
const isNonFinalAtStart = segmentId != null
|
|
184
|
+
&& Array.isArray(unit.task.segmentIds)
|
|
185
|
+
&& unit.task.segmentIds.length > 1
|
|
186
|
+
&& unit.task.segmentIds[unit.task.segmentIds.length - 1] !== segmentId;
|
|
187
|
+
if (isNonFinalAtStart && existsSync(donePath)) {
|
|
188
|
+
try { unlinkSync(donePath); } catch { /* best effort */ }
|
|
189
|
+
logExecution(statusPath, "Segment start", `Removed stale .DONE before non-final segment ${segmentId}`);
|
|
190
|
+
}
|
|
191
|
+
|
|
179
192
|
// ── 2. Iteration loop ───────────────────────────────────────────
|
|
180
193
|
let noProgressCount = 0;
|
|
181
194
|
let totalIterations = 0;
|
|
@@ -528,7 +541,39 @@ export async function executeTaskV2(
|
|
|
528
541
|
false, totalIterations, cumulativeCostUsd, cumulativeTokens, config, statusPath, reviewerStatePath, lastTelemetry);
|
|
529
542
|
}
|
|
530
543
|
|
|
531
|
-
//
|
|
544
|
+
// TP-145: Determine if this is a non-final segment of a multi-segment task.
|
|
545
|
+
// If more segments remain after this one, suppress .DONE creation so that
|
|
546
|
+
// the engine can advance the segment frontier and execute subsequent segments.
|
|
547
|
+
// .DONE must only exist when ALL segments of a multi-segment task are complete.
|
|
548
|
+
const isNonFinalSegment = segmentId != null
|
|
549
|
+
&& Array.isArray(unit.task.segmentIds)
|
|
550
|
+
&& unit.task.segmentIds.length > 1
|
|
551
|
+
&& unit.task.segmentIds[unit.task.segmentIds.length - 1] !== segmentId;
|
|
552
|
+
|
|
553
|
+
if (isNonFinalSegment) {
|
|
554
|
+
// Segment succeeded but more segments remain — suppress .DONE and "✅ Complete" status.
|
|
555
|
+
// The engine will advance the frontier and dispatch the next segment.
|
|
556
|
+
// Also delete any .DONE the worker may have created directly (workers have
|
|
557
|
+
// write access and sometimes create .DONE on their own, bypassing this gate).
|
|
558
|
+
if (existsSync(donePath)) {
|
|
559
|
+
let deleted = false;
|
|
560
|
+
try { unlinkSync(donePath); deleted = true; } catch { /* best effort */ }
|
|
561
|
+
if (deleted) {
|
|
562
|
+
logExecution(statusPath, "Segment complete",
|
|
563
|
+
`Segment ${segmentId} succeeded (non-final — removed premature worker-created .DONE)`);
|
|
564
|
+
} else {
|
|
565
|
+
logExecution(statusPath, "Segment complete",
|
|
566
|
+
`⚠️ Segment ${segmentId} succeeded but FAILED to remove premature .DONE — downstream segments may be skipped`);
|
|
567
|
+
}
|
|
568
|
+
} else {
|
|
569
|
+
logExecution(statusPath, "Segment complete",
|
|
570
|
+
`Segment ${segmentId} succeeded (not final — .DONE suppressed)`);
|
|
571
|
+
}
|
|
572
|
+
return makeResult(taskId, segmentId, workerAgentId, "succeeded", startTime,
|
|
573
|
+
"Segment completed (non-final — .DONE suppressed)", false, totalIterations, cumulativeCostUsd, cumulativeTokens, config, statusPath, reviewerStatePath, lastTelemetry);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Create .DONE if not already present (final segment or single-segment/whole-task execution)
|
|
532
577
|
if (!existsSync(donePath)) {
|
|
533
578
|
writeFileSync(donePath, `Completed: ${new Date().toISOString()}\nTask: ${taskId}\n`);
|
|
534
579
|
}
|