taskplane 0.23.16 → 0.24.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/bin/taskplane.mjs +8 -41
- package/dashboard/public/app.js +24 -24
- package/dashboard/server.cjs +29 -7
- package/extensions/task-runner.ts +7 -3
- package/extensions/taskplane/abort.ts +93 -81
- package/extensions/taskplane/agent-host.ts +4 -5
- package/extensions/taskplane/config-loader.ts +86 -11
- package/extensions/taskplane/config-schema.ts +13 -13
- package/extensions/taskplane/diagnostic-reports.ts +1 -1
- package/extensions/taskplane/diagnostics.ts +3 -3
- package/extensions/taskplane/engine.ts +37 -16
- package/extensions/taskplane/execution.ts +86 -1000
- package/extensions/taskplane/extension.ts +60 -189
- package/extensions/taskplane/formatting.ts +5 -5
- package/extensions/taskplane/merge.ts +63 -371
- package/extensions/taskplane/messages.ts +1 -1
- package/extensions/taskplane/naming.ts +4 -4
- package/extensions/taskplane/persistence.ts +53 -26
- package/extensions/taskplane/process-registry.ts +2 -2
- package/extensions/taskplane/resume.ts +65 -130
- package/extensions/taskplane/sessions.ts +57 -92
- package/extensions/taskplane/settings-tui.ts +4 -4
- package/extensions/taskplane/tmux-compat.ts +37 -0
- package/extensions/taskplane/types.ts +43 -43
- package/extensions/taskplane/waves.ts +12 -10
- package/extensions/taskplane/worktree.ts +8 -66
- package/package.json +1 -1
- package/templates/config/task-orchestrator.yaml +3 -4
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
* @module orch/persistence
|
|
4
4
|
*/
|
|
5
5
|
import { readFileSync, writeFileSync, existsSync, unlinkSync, renameSync, mkdirSync, appendFileSync } from "fs";
|
|
6
|
-
import { execSync } from "child_process";
|
|
7
6
|
import { join, dirname, basename } from "path";
|
|
8
7
|
|
|
9
8
|
import { execLog } from "./execution.ts";
|
|
@@ -12,6 +11,7 @@ import type { BatchHistorySummary } from "./types.ts";
|
|
|
12
11
|
import type { AllocatedLane, DiscoveryResult, EngineEvent, EscalationContext, LaneTaskOutcome, LaneTaskStatus, MonitorState, OrchBatchPhase, OrchBatchRuntimeState, PersistedBatchState, PersistedLaneRecord, PersistedMergeResult, PersistedSegmentRecord, PersistedTaskRecord, TaskMonitorSnapshot, Tier0RecoveryPattern, WorkspaceMode } from "./types.ts";
|
|
13
12
|
import { sleepSync } from "./worktree.ts";
|
|
14
13
|
import type { PreserveFailedLaneProgressResult } from "./worktree.ts";
|
|
14
|
+
import { normalizeLaneSessionAlias, readLaneSessionAliases } from "./tmux-compat.ts";
|
|
15
15
|
|
|
16
16
|
// ── State Persistence Helper (TS-009 Step 2) ────────────────────────
|
|
17
17
|
|
|
@@ -151,7 +151,7 @@ export function seedPendingOutcomesForAllocatedLanes(
|
|
|
151
151
|
startTime: null,
|
|
152
152
|
endTime: null,
|
|
153
153
|
exitReason: "Pending execution",
|
|
154
|
-
sessionName: lane.
|
|
154
|
+
sessionName: lane.laneSessionId,
|
|
155
155
|
doneFileFound: false,
|
|
156
156
|
laneNumber: lane.laneNumber,
|
|
157
157
|
}) || changed;
|
|
@@ -681,6 +681,7 @@ export function validatePersistedState(data: unknown): PersistedBatchState {
|
|
|
681
681
|
|
|
682
682
|
// ── Validate lane records ────────────────────────────────────
|
|
683
683
|
const lanes = obj.lanes as unknown[];
|
|
684
|
+
const legacyTmuxSessionLaneIndexes: number[] = [];
|
|
684
685
|
for (let i = 0; i < lanes.length; i++) {
|
|
685
686
|
const l = lanes[i] as Record<string, unknown>;
|
|
686
687
|
if (!l || typeof l !== "object") {
|
|
@@ -689,7 +690,7 @@ export function validatePersistedState(data: unknown): PersistedBatchState {
|
|
|
689
690
|
`lanes[${i}] is not an object`,
|
|
690
691
|
);
|
|
691
692
|
}
|
|
692
|
-
for (const field of ["laneId", "
|
|
693
|
+
for (const field of ["laneId", "worktreePath", "branch"] as const) {
|
|
693
694
|
if (typeof l[field] !== "string") {
|
|
694
695
|
throw new StateFileError(
|
|
695
696
|
"STATE_SCHEMA_INVALID",
|
|
@@ -697,6 +698,35 @@ export function validatePersistedState(data: unknown): PersistedBatchState {
|
|
|
697
698
|
);
|
|
698
699
|
}
|
|
699
700
|
}
|
|
701
|
+
|
|
702
|
+
const { laneSessionId, tmuxSessionName } = readLaneSessionAliases(l);
|
|
703
|
+
if (laneSessionId !== undefined && typeof laneSessionId !== "string") {
|
|
704
|
+
throw new StateFileError(
|
|
705
|
+
"STATE_SCHEMA_INVALID",
|
|
706
|
+
`lanes[${i}].laneSessionId is not a string (got ${typeof laneSessionId})`,
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
if (tmuxSessionName !== undefined && typeof tmuxSessionName !== "string") {
|
|
711
|
+
throw new StateFileError(
|
|
712
|
+
"STATE_SCHEMA_INVALID",
|
|
713
|
+
`lanes[${i}].tmuxSessionName is not a string (got ${typeof tmuxSessionName})`,
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
if (typeof laneSessionId !== "string" && typeof tmuxSessionName !== "string") {
|
|
718
|
+
throw new StateFileError(
|
|
719
|
+
"STATE_SCHEMA_INVALID",
|
|
720
|
+
`lanes[${i}] must include either laneSessionId or tmuxSessionName as a string`,
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
if (typeof tmuxSessionName === "string") {
|
|
725
|
+
legacyTmuxSessionLaneIndexes.push(i);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
normalizeLaneSessionAlias(l);
|
|
729
|
+
|
|
700
730
|
if (typeof l.laneNumber !== "number") {
|
|
701
731
|
throw new StateFileError(
|
|
702
732
|
"STATE_SCHEMA_INVALID",
|
|
@@ -718,6 +748,13 @@ export function validatePersistedState(data: unknown): PersistedBatchState {
|
|
|
718
748
|
}
|
|
719
749
|
}
|
|
720
750
|
|
|
751
|
+
if (legacyTmuxSessionLaneIndexes.length > 0) {
|
|
752
|
+
console.error(
|
|
753
|
+
"[taskplane] migration: detected legacy lanes[].tmuxSessionName in .pi/batch-state.json; " +
|
|
754
|
+
"normalized to lanes[].laneSessionId for this release. Re-save state (or re-run /orch-resume) to persist canonical fields.",
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
|
|
721
758
|
// ── Validate merge results ───────────────────────────────────
|
|
722
759
|
const mergeResults = obj.mergeResults as unknown[];
|
|
723
760
|
for (let i = 0; i < mergeResults.length; i++) {
|
|
@@ -1197,7 +1234,7 @@ export function serializeBatchState(
|
|
|
1197
1234
|
const record: PersistedTaskRecord = {
|
|
1198
1235
|
taskId,
|
|
1199
1236
|
laneNumber: lane?.laneNumber ?? outcome?.laneNumber ?? 0,
|
|
1200
|
-
sessionName: outcome?.sessionName || lane?.
|
|
1237
|
+
sessionName: outcome?.sessionName || lane?.laneSessionId || "",
|
|
1201
1238
|
status: outcome?.status ?? "pending",
|
|
1202
1239
|
taskFolder: "", // Enriched by caller from discovery
|
|
1203
1240
|
startedAt: outcome?.startTime ?? null,
|
|
@@ -1249,7 +1286,7 @@ export function serializeBatchState(
|
|
|
1249
1286
|
const record: PersistedLaneRecord = {
|
|
1250
1287
|
laneNumber: lane.laneNumber,
|
|
1251
1288
|
laneId: lane.laneId,
|
|
1252
|
-
|
|
1289
|
+
laneSessionId: lane.laneSessionId,
|
|
1253
1290
|
worktreePath: lane.worktreePath,
|
|
1254
1291
|
branch: lane.branch,
|
|
1255
1292
|
taskIds: lane.tasks.map((t) => t.taskId),
|
|
@@ -1695,32 +1732,22 @@ export function analyzeOrchestratorStartupState(
|
|
|
1695
1732
|
}
|
|
1696
1733
|
|
|
1697
1734
|
/**
|
|
1698
|
-
* Detect orphan
|
|
1699
|
-
*
|
|
1700
|
-
* Combines session discovery (via tmux), state file loading (with typed
|
|
1701
|
-
* error handling), and .DONE file checking into a single result.
|
|
1735
|
+
* Detect orphan orchestrator state and analyze startup recovery action.
|
|
1702
1736
|
*
|
|
1703
|
-
*
|
|
1704
|
-
*
|
|
1737
|
+
* Runtime V2 no longer relies on TMUX session discovery. Startup decisions
|
|
1738
|
+
* are based on persisted batch state plus task .DONE markers.
|
|
1705
1739
|
*
|
|
1706
|
-
* @param prefix -
|
|
1740
|
+
* @param prefix - Legacy orchestrator session prefix (unused in Runtime V2)
|
|
1707
1741
|
* @param repoRoot - Absolute path to the repository root
|
|
1708
1742
|
* @returns OrphanDetectionResult with recommended action
|
|
1709
1743
|
*/
|
|
1710
1744
|
export function detectOrphanSessions(prefix: string, repoRoot: string): OrphanDetectionResult {
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
encoding: "utf-8",
|
|
1716
|
-
timeout: 5000,
|
|
1717
|
-
});
|
|
1718
|
-
orphanSessions = parseOrchSessionNames(stdout, prefix);
|
|
1719
|
-
} catch {
|
|
1720
|
-
// tmux not available or no sessions — proceed with empty orphan list
|
|
1721
|
-
}
|
|
1745
|
+
void prefix;
|
|
1746
|
+
|
|
1747
|
+
// Runtime V2 uses persisted state as the source of truth for orphan analysis.
|
|
1748
|
+
const orphanSessions: string[] = [];
|
|
1722
1749
|
|
|
1723
|
-
// ──
|
|
1750
|
+
// ── 1. Load batch state file ─────────────────────────────────
|
|
1724
1751
|
let stateStatus: OrphanStateStatus = "missing";
|
|
1725
1752
|
let loadedState: PersistedBatchState | null = null;
|
|
1726
1753
|
let stateError: string | null = null;
|
|
@@ -1747,7 +1774,7 @@ export function detectOrphanSessions(prefix: string, repoRoot: string): OrphanDe
|
|
|
1747
1774
|
}
|
|
1748
1775
|
}
|
|
1749
1776
|
|
|
1750
|
-
// ──
|
|
1777
|
+
// ── 2. Check .DONE files for stale state detection ───────────
|
|
1751
1778
|
const doneTaskIds = new Set<string>();
|
|
1752
1779
|
if (loadedState && orphanSessions.length === 0) {
|
|
1753
1780
|
// Only check .DONE files when we have state but no orphans
|
|
@@ -1759,7 +1786,7 @@ export function detectOrphanSessions(prefix: string, repoRoot: string): OrphanDe
|
|
|
1759
1786
|
}
|
|
1760
1787
|
}
|
|
1761
1788
|
|
|
1762
|
-
// ──
|
|
1789
|
+
// ── 3. Analyze and return ────────────────────────────────────
|
|
1763
1790
|
return analyzeOrchestratorStartupState(
|
|
1764
1791
|
orphanSessions,
|
|
1765
1792
|
stateStatus,
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Process Registry — Runtime V2 agent lifecycle management
|
|
3
3
|
*
|
|
4
|
-
* File-backed registry that replaces
|
|
4
|
+
* File-backed registry that replaces legacy session discovery as the
|
|
5
5
|
* authoritative source of truth for agent liveness, identity, and
|
|
6
6
|
* attribution.
|
|
7
7
|
*
|
|
8
8
|
* Key design rules:
|
|
9
9
|
* 1. Parent writes manifest BEFORE child is considered visible.
|
|
10
10
|
* 2. Parent updates manifest on every status transition.
|
|
11
|
-
* 3. Operator tools read the registry, not
|
|
11
|
+
* 3. Operator tools read the registry, not terminal-session probes.
|
|
12
12
|
* 4. Resume/cleanup validates pid + startedAt for orphan detection.
|
|
13
13
|
*
|
|
14
14
|
* File locations:
|
|
@@ -8,7 +8,7 @@ import { join } from "path";
|
|
|
8
8
|
import { assembleDiagnosticInput, emitDiagnosticReports } from "./diagnostic-reports.ts";
|
|
9
9
|
import { runDiscovery } from "./discovery.ts";
|
|
10
10
|
import { executeOrchBatch } from "./engine.ts";
|
|
11
|
-
import { computeTransitiveDependents, execLog, executeLaneV2, executeWave,
|
|
11
|
+
import { computeTransitiveDependents, execLog, executeLaneV2, executeWave, resolveCanonicalTaskPaths } from "./execution.ts";
|
|
12
12
|
import type { MonitorUpdateCallback, RuntimeBackend } from "./execution.ts";
|
|
13
13
|
import { selectRuntimeBackend } from "./engine.ts";
|
|
14
14
|
import { readRegistrySnapshot, isTerminalStatus, isProcessAlive } from "./process-registry.ts";
|
|
@@ -143,7 +143,7 @@ export function reconstructAllocatedLanes(
|
|
|
143
143
|
return persistedLanes.map((lr) => ({
|
|
144
144
|
laneNumber: lr.laneNumber,
|
|
145
145
|
laneId: lr.laneId,
|
|
146
|
-
|
|
146
|
+
laneSessionId: lr.laneSessionId,
|
|
147
147
|
worktreePath: lr.worktreePath,
|
|
148
148
|
branch: lr.branch,
|
|
149
149
|
tasks: lr.taskIds.map((taskId) => {
|
|
@@ -337,7 +337,7 @@ export function checkResumeEligibility(state: PersistedBatchState, force: boolea
|
|
|
337
337
|
* Reconcile persisted task states against live signals.
|
|
338
338
|
*
|
|
339
339
|
* For each task in the persisted state, determines the correct action
|
|
340
|
-
* based on the current state of
|
|
340
|
+
* based on the current state of lane-session liveness and .DONE files.
|
|
341
341
|
*
|
|
342
342
|
* Precedence rules (applied per-task):
|
|
343
343
|
* 1. .DONE file found → "mark-complete" (even if session is alive — task is done)
|
|
@@ -350,7 +350,7 @@ export function checkResumeEligibility(state: PersistedBatchState, force: boolea
|
|
|
350
350
|
* Pure function — no process or filesystem access.
|
|
351
351
|
*
|
|
352
352
|
* @param persistedState - Loaded and validated batch state
|
|
353
|
-
* @param aliveSessions - Set of
|
|
353
|
+
* @param aliveSessions - Set of lane session names currently alive
|
|
354
354
|
* @param doneTaskIds - Set of task IDs whose .DONE files exist
|
|
355
355
|
* @returns Array of reconciled task states in persisted order
|
|
356
356
|
*/
|
|
@@ -777,7 +777,6 @@ export async function resumeOrchBatch(
|
|
|
777
777
|
// State files (.pi/batch-state.json, lane-state, etc.) belong in the workspace root,
|
|
778
778
|
// which is where .pi/ config lives. In repo mode, stateRoot === repoRoot.
|
|
779
779
|
const stateRoot = workspaceRoot ?? cwd;
|
|
780
|
-
const prefix = orchConfig.orchestrator.tmux_prefix;
|
|
781
780
|
|
|
782
781
|
// ── TP-076: Supervisor alert emission helper ─────────────────
|
|
783
782
|
const emitAlert = (alert: import("./types.ts").SupervisorAlert): void => {
|
|
@@ -883,27 +882,19 @@ export async function resumeOrchBatch(
|
|
|
883
882
|
execLog("resume", batchState.batchId, `runtime backend for resumed execution: ${resumeBackend}`);
|
|
884
883
|
|
|
885
884
|
// ── 3. Discover live signals ─────────────────────────────────
|
|
886
|
-
// TP-112:
|
|
887
|
-
//
|
|
885
|
+
// TP-112/119: Runtime V2 session liveness check only.
|
|
886
|
+
// Alive sessions are discovered from the process registry.
|
|
888
887
|
const aliveSessions = new Set<string>();
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
if (laneSession !== manifest.agentId) aliveSessions.add(laneSession);
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
|
-
}
|
|
903
|
-
} else {
|
|
904
|
-
for (const task of persistedState.tasks) {
|
|
905
|
-
if (task.sessionName && tmuxHasSession(task.sessionName)) {
|
|
906
|
-
aliveSessions.add(task.sessionName);
|
|
888
|
+
const registry = readRegistrySnapshot(stateRoot, persistedState.batchId);
|
|
889
|
+
if (registry) {
|
|
890
|
+
for (const manifest of Object.values(registry.agents)) {
|
|
891
|
+
if (!isTerminalStatus(manifest.status) && isProcessAlive(manifest.pid)) {
|
|
892
|
+
aliveSessions.add(manifest.agentId);
|
|
893
|
+
// Also add lane session name (without role suffix) so reconciliation
|
|
894
|
+
// matches persisted task.sessionName.
|
|
895
|
+
// e.g., "orch-op-lane-1-worker" -> also add "orch-op-lane-1"
|
|
896
|
+
const laneSession = manifest.agentId.replace(/-(worker|reviewer)$/, "");
|
|
897
|
+
if (laneSession !== manifest.agentId) aliveSessions.add(laneSession);
|
|
907
898
|
}
|
|
908
899
|
}
|
|
909
900
|
}
|
|
@@ -951,8 +942,8 @@ export async function resumeOrchBatch(
|
|
|
951
942
|
// failed resume but were never actually started need their allocation
|
|
952
943
|
// metadata cleared so they can be freshly assigned to new lanes.
|
|
953
944
|
// We also prune these tasks from persisted lane records so that
|
|
954
|
-
// serializeBatchState() doesn't reintroduce stale sessionName via
|
|
955
|
-
//
|
|
945
|
+
// serializeBatchState() doesn't reintroduce stale sessionName via lane
|
|
946
|
+
// fallback paths when outcome.sessionName is absent.
|
|
956
947
|
const stalePendingTaskIds = new Set<string>();
|
|
957
948
|
for (const reconciled of reconciledTasks) {
|
|
958
949
|
if (reconciled.action === "pending") {
|
|
@@ -1130,7 +1121,7 @@ export async function resumeOrchBatch(
|
|
|
1130
1121
|
const lane: AllocatedLane = {
|
|
1131
1122
|
laneNumber: laneRecord.laneNumber,
|
|
1132
1123
|
laneId: laneRecord.laneId,
|
|
1133
|
-
|
|
1124
|
+
laneSessionId: laneRecord.laneSessionId,
|
|
1134
1125
|
worktreePath: laneRecord.worktreePath,
|
|
1135
1126
|
branch: laneRecord.branch,
|
|
1136
1127
|
tasks: [allocatedTask],
|
|
@@ -1143,82 +1134,41 @@ export async function resumeOrchBatch(
|
|
|
1143
1134
|
// Resolve per-lane repo root for workspace mode (v1/repo mode: falls back to repoRoot)
|
|
1144
1135
|
const laneRepoRoot = resolveRepoRoot(laneRecord.repoId, repoRoot, workspaceConfig);
|
|
1145
1136
|
|
|
1146
|
-
// TP-112:
|
|
1147
|
-
//
|
|
1148
|
-
//
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
);
|
|
1164
|
-
|
|
1165
|
-
if (taskResult?.status === "succeeded") {
|
|
1166
|
-
reconnectFinalStatus.set(task.taskId, "succeeded");
|
|
1167
|
-
completedTaskSet.add(task.taskId);
|
|
1168
|
-
failedTaskSet.delete(task.taskId);
|
|
1169
|
-
reconnectTaskSet.delete(task.taskId);
|
|
1170
|
-
batchState.succeededTasks++;
|
|
1171
|
-
} else {
|
|
1172
|
-
reconnectFinalStatus.set(task.taskId, "failed");
|
|
1173
|
-
failedTaskSet.add(task.taskId);
|
|
1174
|
-
completedTaskSet.delete(task.taskId);
|
|
1175
|
-
reconnectTaskSet.delete(task.taskId);
|
|
1176
|
-
batchState.failedTasks++;
|
|
1177
|
-
}
|
|
1178
|
-
} catch (err: unknown) {
|
|
1179
|
-
reconnectFinalStatus.set(task.taskId, "failed");
|
|
1180
|
-
failedTaskSet.add(task.taskId);
|
|
1181
|
-
completedTaskSet.delete(task.taskId);
|
|
1137
|
+
// TP-112: Runtime V2 reconnect.
|
|
1138
|
+
// Agent-host processes do not survive supervisor restart, so reconnect
|
|
1139
|
+
// uses terminate + rehydrate via executeLaneV2.
|
|
1140
|
+
execLog("resume", task.taskId, "V2 reconnect: terminate + rehydrate via lane-runner", {
|
|
1141
|
+
repoId: laneRecord.repoId ?? "(default)",
|
|
1142
|
+
});
|
|
1143
|
+
terminateAliveV2Agents(stateRoot, persistedState.batchId, laneRecord.laneSessionId);
|
|
1144
|
+
try {
|
|
1145
|
+
const laneResult = await executeLaneV2(
|
|
1146
|
+
lane, orchConfig, laneRepoRoot, batchState.pauseSignal,
|
|
1147
|
+
workspaceRoot, !!workspaceConfig,
|
|
1148
|
+
{ ORCH_BATCH_ID: batchState.batchId },
|
|
1149
|
+
emitAlert,
|
|
1150
|
+
);
|
|
1151
|
+
const taskResult = laneResult.tasks.find(t => t.taskId === task.taskId);
|
|
1152
|
+
if (taskResult?.status === "succeeded") {
|
|
1153
|
+
reconnectFinalStatus.set(task.taskId, "succeeded");
|
|
1154
|
+
completedTaskSet.add(task.taskId);
|
|
1155
|
+
failedTaskSet.delete(task.taskId);
|
|
1182
1156
|
reconnectTaskSet.delete(task.taskId);
|
|
1183
|
-
batchState.
|
|
1184
|
-
|
|
1185
|
-
}
|
|
1186
|
-
} else {
|
|
1187
|
-
execLog("resume", task.taskId, "reconnecting to alive session", {
|
|
1188
|
-
session: laneRecord.tmuxSessionName,
|
|
1189
|
-
repoId: laneRecord.repoId ?? "(default)",
|
|
1190
|
-
});
|
|
1191
|
-
|
|
1192
|
-
try {
|
|
1193
|
-
const pollResult = await pollUntilTaskComplete(
|
|
1194
|
-
lane,
|
|
1195
|
-
allocatedTask,
|
|
1196
|
-
orchConfig,
|
|
1197
|
-
laneRepoRoot,
|
|
1198
|
-
batchState.pauseSignal,
|
|
1199
|
-
);
|
|
1200
|
-
|
|
1201
|
-
if (pollResult.status === "succeeded") {
|
|
1202
|
-
reconnectFinalStatus.set(task.taskId, "succeeded");
|
|
1203
|
-
completedTaskSet.add(task.taskId);
|
|
1204
|
-
failedTaskSet.delete(task.taskId);
|
|
1205
|
-
reconnectTaskSet.delete(task.taskId);
|
|
1206
|
-
batchState.succeededTasks++;
|
|
1207
|
-
} else {
|
|
1208
|
-
reconnectFinalStatus.set(task.taskId, "failed");
|
|
1209
|
-
failedTaskSet.add(task.taskId);
|
|
1210
|
-
completedTaskSet.delete(task.taskId);
|
|
1211
|
-
reconnectTaskSet.delete(task.taskId);
|
|
1212
|
-
batchState.failedTasks++;
|
|
1213
|
-
}
|
|
1214
|
-
} catch (err: unknown) {
|
|
1157
|
+
batchState.succeededTasks++;
|
|
1158
|
+
} else {
|
|
1215
1159
|
reconnectFinalStatus.set(task.taskId, "failed");
|
|
1216
1160
|
failedTaskSet.add(task.taskId);
|
|
1217
1161
|
completedTaskSet.delete(task.taskId);
|
|
1218
1162
|
reconnectTaskSet.delete(task.taskId);
|
|
1219
1163
|
batchState.failedTasks++;
|
|
1220
|
-
execLog("resume", task.taskId, `reconnection error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1221
1164
|
}
|
|
1165
|
+
} catch (err: unknown) {
|
|
1166
|
+
reconnectFinalStatus.set(task.taskId, "failed");
|
|
1167
|
+
failedTaskSet.add(task.taskId);
|
|
1168
|
+
completedTaskSet.delete(task.taskId);
|
|
1169
|
+
reconnectTaskSet.delete(task.taskId);
|
|
1170
|
+
batchState.failedTasks++;
|
|
1171
|
+
execLog("resume", task.taskId, `V2 reconnect error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1222
1172
|
}
|
|
1223
1173
|
}
|
|
1224
1174
|
}
|
|
@@ -1252,7 +1202,7 @@ export async function resumeOrchBatch(
|
|
|
1252
1202
|
const lane: AllocatedLane = {
|
|
1253
1203
|
laneNumber: laneRecord.laneNumber,
|
|
1254
1204
|
laneId: laneRecord.laneId,
|
|
1255
|
-
|
|
1205
|
+
laneSessionId: laneRecord.laneSessionId,
|
|
1256
1206
|
worktreePath: laneRecord.worktreePath,
|
|
1257
1207
|
branch: laneRecord.branch,
|
|
1258
1208
|
tasks: [allocatedTask],
|
|
@@ -1266,41 +1216,26 @@ export async function resumeOrchBatch(
|
|
|
1266
1216
|
const reExecRepoRoot = resolveRepoRoot(laneRecord.repoId, repoRoot, workspaceConfig);
|
|
1267
1217
|
|
|
1268
1218
|
execLog("resume", task.taskId, "re-executing interrupted task in existing worktree", {
|
|
1269
|
-
session: laneRecord.
|
|
1219
|
+
session: laneRecord.laneSessionId,
|
|
1270
1220
|
worktree: laneRecord.worktreePath,
|
|
1271
1221
|
repoId: laneRecord.repoId ?? "(default)",
|
|
1272
1222
|
});
|
|
1273
1223
|
|
|
1274
1224
|
try {
|
|
1275
|
-
// TP-112:
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
exitReason: taskResult?.exitReason ?? "V2 re-execution completed",
|
|
1290
|
-
doneFileFound: taskResult?.doneFileFound ?? false,
|
|
1291
|
-
};
|
|
1292
|
-
} else {
|
|
1293
|
-
spawnLaneSession(lane, allocatedTask, orchConfig, reExecRepoRoot, undefined, {
|
|
1294
|
-
ORCH_BATCH_ID: batchState.batchId,
|
|
1295
|
-
});
|
|
1296
|
-
pollResult = await pollUntilTaskComplete(
|
|
1297
|
-
lane,
|
|
1298
|
-
allocatedTask,
|
|
1299
|
-
orchConfig,
|
|
1300
|
-
reExecRepoRoot,
|
|
1301
|
-
batchState.pauseSignal,
|
|
1302
|
-
);
|
|
1303
|
-
}
|
|
1225
|
+
// TP-112: Runtime V2 re-execution.
|
|
1226
|
+
terminateAliveV2Agents(stateRoot, batchState.batchId, laneRecord.laneSessionId);
|
|
1227
|
+
const laneResult = await executeLaneV2(
|
|
1228
|
+
lane, orchConfig, reExecRepoRoot, batchState.pauseSignal,
|
|
1229
|
+
workspaceRoot, !!workspaceConfig,
|
|
1230
|
+
{ ORCH_BATCH_ID: batchState.batchId },
|
|
1231
|
+
emitAlert,
|
|
1232
|
+
);
|
|
1233
|
+
const taskResult = laneResult.tasks.find(t => t.taskId === task.taskId);
|
|
1234
|
+
const pollResult: { status: LaneTaskStatus; exitReason: string; doneFileFound: boolean } = {
|
|
1235
|
+
status: taskResult?.status ?? "failed",
|
|
1236
|
+
exitReason: taskResult?.exitReason ?? "V2 re-execution completed",
|
|
1237
|
+
doneFileFound: taskResult?.doneFileFound ?? false,
|
|
1238
|
+
};
|
|
1304
1239
|
|
|
1305
1240
|
if (pollResult.status === "succeeded") {
|
|
1306
1241
|
reExecuteFinalStatus.set(task.taskId, "succeeded");
|
|
@@ -1354,7 +1289,7 @@ export async function resumeOrchBatch(
|
|
|
1354
1289
|
startTime: Date.now(),
|
|
1355
1290
|
endTime: Date.now(),
|
|
1356
1291
|
exitReason: "Re-executed task completed successfully",
|
|
1357
|
-
sessionName: lane.
|
|
1292
|
+
sessionName: lane.laneSessionId,
|
|
1358
1293
|
doneFileFound: true,
|
|
1359
1294
|
laneNumber: lane.laneNumber,
|
|
1360
1295
|
})),
|
|
@@ -1604,7 +1539,7 @@ export async function resumeOrchBatch(
|
|
|
1604
1539
|
: status === "skipped" ? "Task skipped (merge retry)"
|
|
1605
1540
|
: status === "stalled" ? "Task stalled (merge retry)"
|
|
1606
1541
|
: "Task failed (merge retry)",
|
|
1607
|
-
sessionName: lane.
|
|
1542
|
+
sessionName: lane.laneSessionId,
|
|
1608
1543
|
doneFileFound: status === "succeeded",
|
|
1609
1544
|
laneNumber: lane.laneNumber,
|
|
1610
1545
|
};
|
|
@@ -1,92 +1,57 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
* @module orch/sessions
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return sessionNames.map(name => {
|
|
61
|
-
const laneInfo = laneLookup.get(name);
|
|
62
|
-
return {
|
|
63
|
-
sessionName: name,
|
|
64
|
-
laneId: laneInfo?.laneId || "unknown",
|
|
65
|
-
taskId: laneInfo?.taskId || null,
|
|
66
|
-
status: tmuxHasSession(name) ? "alive" as const : "dead" as const,
|
|
67
|
-
worktreePath: laneInfo?.worktreePath || "",
|
|
68
|
-
attachCmd: `tmux attach -t ${name}`,
|
|
69
|
-
};
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Format session listing for display.
|
|
75
|
-
*/
|
|
76
|
-
export function formatOrchSessions(sessions: OrchestratorSessionEntry[]): string {
|
|
77
|
-
if (sessions.length === 0) {
|
|
78
|
-
return ORCH_MESSAGES.sessionsNone();
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const lines: string[] = [ORCH_MESSAGES.sessionsHeader(sessions.length), ""];
|
|
82
|
-
|
|
83
|
-
for (const s of sessions) {
|
|
84
|
-
const statusIcon = s.status === "alive" ? "🟢" : "🔴";
|
|
85
|
-
const taskInfo = s.taskId ? ` (${s.taskId})` : "";
|
|
86
|
-
lines.push(` ${statusIcon} ${s.sessionName} [${s.laneId}]${taskInfo}`);
|
|
87
|
-
lines.push(` ${s.attachCmd}`);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return lines.join("\n");
|
|
91
|
-
}
|
|
92
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Runtime V2 session discovery and formatting
|
|
3
|
+
* @module orch/sessions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { ORCH_MESSAGES } from "./messages.ts";
|
|
7
|
+
import type { OrchBatchRuntimeState, OrchestratorSessionEntry } from "./types.ts";
|
|
8
|
+
|
|
9
|
+
// ── Session Discovery ────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* List active orchestrator sessions from in-memory batch state.
|
|
13
|
+
*
|
|
14
|
+
* Runtime V2 no longer uses TMUX as the execution owner. Session rows are
|
|
15
|
+
* derived from canonical lane session IDs in runtime state.
|
|
16
|
+
*
|
|
17
|
+
* @param _tmuxPrefix - Legacy parameter kept for API compatibility
|
|
18
|
+
* @param batchState - Current batch state for lane/task enrichment
|
|
19
|
+
* @returns Array of session entries
|
|
20
|
+
*/
|
|
21
|
+
export function listOrchSessions(
|
|
22
|
+
_tmuxPrefix: string,
|
|
23
|
+
batchState?: OrchBatchRuntimeState,
|
|
24
|
+
): OrchestratorSessionEntry[] {
|
|
25
|
+
if (!batchState || batchState.currentLanes.length === 0) return [];
|
|
26
|
+
|
|
27
|
+
return batchState.currentLanes
|
|
28
|
+
.map(lane => ({
|
|
29
|
+
sessionName: lane.laneSessionId,
|
|
30
|
+
laneId: lane.laneId,
|
|
31
|
+
taskId: lane.tasks.length > 0 ? lane.tasks[0].taskId : null,
|
|
32
|
+
status: "alive" as const,
|
|
33
|
+
worktreePath: lane.worktreePath,
|
|
34
|
+
attachCmd: "Runtime V2 (no tmux attach)",
|
|
35
|
+
}))
|
|
36
|
+
.sort((a, b) => a.sessionName.localeCompare(b.sessionName));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Format session listing for display.
|
|
41
|
+
*/
|
|
42
|
+
export function formatOrchSessions(sessions: OrchestratorSessionEntry[]): string {
|
|
43
|
+
if (sessions.length === 0) {
|
|
44
|
+
return ORCH_MESSAGES.sessionsNone();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const lines: string[] = [ORCH_MESSAGES.sessionsHeader(sessions.length), ""];
|
|
48
|
+
|
|
49
|
+
for (const s of sessions) {
|
|
50
|
+
const statusIcon = s.status === "alive" ? "🟢" : "🔴";
|
|
51
|
+
const taskInfo = s.taskId ? ` (${s.taskId})` : "";
|
|
52
|
+
lines.push(` ${statusIcon} ${s.sessionName} [${s.laneId}]${taskInfo}`);
|
|
53
|
+
lines.push(` ${s.attachCmd}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return lines.join("\n");
|
|
57
|
+
}
|