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.
@@ -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.tmuxSessionName,
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", "tmuxSessionName", "worktreePath", "branch"] as const) {
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?.tmuxSessionName || "",
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
- tmuxSessionName: lane.tmuxSessionName,
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 TMUX sessions and analyze startup state.
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
- * Non-blocking: detection failures (e.g., tmux not running) are handled
1704
- * gracefully and do NOT crash `/orch` startup.
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 - TMUX session prefix to search for (e.g., "orch")
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
- // ── 1. Discover TMUX sessions ────────────────────────────────
1712
- let orphanSessions: string[] = [];
1713
- try {
1714
- const stdout = execSync('tmux list-sessions -F "#{session_name}"', {
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
- // ── 2. Load batch state file ─────────────────────────────────
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
- // ── 3. Check .DONE files for stale state detection ───────────
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
- // ── 4. Analyze and return ────────────────────────────────────
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 TMUX session discovery as the
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 TMUX.
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, pollUntilTaskComplete, resolveCanonicalTaskPaths, spawnLaneSession, tmuxHasSession } from "./execution.ts";
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
- tmuxSessionName: lr.tmuxSessionName,
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 TMUX sessions and .DONE files.
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 TMUX session names currently alive
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: Backend-aware session liveness check.
887
- // V2: check process registry (pid + status). Legacy: check TMUX.
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
- if (resumeBackend === "v2") {
890
- const registry = readRegistrySnapshot(stateRoot, persistedState.batchId);
891
- if (registry) {
892
- for (const manifest of Object.values(registry.agents)) {
893
- if (!isTerminalStatus(manifest.status) && isProcessAlive(manifest.pid)) {
894
- aliveSessions.add(manifest.agentId);
895
- // TP-112: Also add the lane session name (without role suffix)
896
- // so reconciliation matches persisted task.sessionName.
897
- // e.g., "orch-op-lane-1-worker" -> also add "orch-op-lane-1"
898
- const laneSession = manifest.agentId.replace(/-(worker|reviewer)$/, "");
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 the
955
- // `outcome?.sessionName || lane?.tmuxSessionName` fallback path.
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
- tmuxSessionName: laneRecord.tmuxSessionName,
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: Backend-aware reconnect.
1147
- // V2: re-execute via executeLaneV2 (agent-host doesn't survive restart).
1148
- // Per spec §7.3: "detect + terminate + rehydrate".
1149
- // Legacy: poll the still-alive TMUX session.
1150
- if (resumeBackend === "v2") {
1151
- execLog("resume", task.taskId, "V2 reconnect: terminate + rehydrate via lane-runner", {
1152
- repoId: laneRecord.repoId ?? "(default)",
1153
- });
1154
- // TP-112 §7.3: detect + terminate + rehydrate.
1155
- // Kill any alive V2 agent before re-executing to prevent duplicates.
1156
- terminateAliveV2Agents(stateRoot, persistedState.batchId, laneRecord.tmuxSessionName);
1157
- try {
1158
- const laneResult = await executeLaneV2(
1159
- lane, orchConfig, laneRepoRoot, batchState.pauseSignal,
1160
- workspaceRoot, !!workspaceConfig,
1161
- { ORCH_BATCH_ID: batchState.batchId },
1162
- emitAlert,
1163
- );
1164
- const taskResult = laneResult.tasks.find(t => t.taskId === task.taskId);
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.failedTasks++;
1184
- execLog("resume", task.taskId, `V2 reconnect error: ${err instanceof Error ? err.message : String(err)}`);
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
- tmuxSessionName: laneRecord.tmuxSessionName,
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.tmuxSessionName,
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: Backend-aware re-execution.
1276
- let pollResult: { status: LaneTaskStatus; exitReason: string; doneFileFound: boolean };
1277
- if (resumeBackend === "v2") {
1278
- // TP-112: terminate any alive V2 agent before re-execute
1279
- terminateAliveV2Agents(stateRoot, batchState.batchId, laneRecord.tmuxSessionName);
1280
- const laneResult = await executeLaneV2(
1281
- lane, orchConfig, reExecRepoRoot, batchState.pauseSignal,
1282
- workspaceRoot, !!workspaceConfig,
1283
- { ORCH_BATCH_ID: batchState.batchId },
1284
- emitAlert,
1285
- );
1286
- const taskResult = laneResult.tasks.find(t => t.taskId === task.taskId);
1287
- pollResult = {
1288
- status: taskResult?.status ?? "failed",
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.tmuxSessionName,
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.tmuxSessionName,
1542
+ sessionName: lane.laneSessionId,
1608
1543
  doneFileFound: status === "succeeded",
1609
1544
  laneNumber: lane.laneNumber,
1610
1545
  };
@@ -1,92 +1,57 @@
1
- /**
2
- * TMUX session discovery and formatting
3
- * @module orch/sessions
4
- */
5
- import { execSync } from "child_process";
6
- import { join } from "path";
7
-
8
- import { tmuxHasSession } from "./execution.ts";
9
- import { ORCH_MESSAGES } from "./messages.ts";
10
- import type { OrchBatchRuntimeState, OrchestratorSessionEntry } from "./types.ts";
11
-
12
- // ── Session Discovery ────────────────────────────────────────────────
13
-
14
- /**
15
- * List all TMUX sessions matching the orchestrator prefix.
16
- *
17
- * Parses `tmux list-sessions` output and filters by prefix.
18
- * Returns entries sorted alphabetically by session name.
19
- *
20
- * @param tmuxPrefix - Prefix to match (e.g., "orch")
21
- * @param batchState - Current batch state for enrichment (optional)
22
- * @returns Array of session entries
23
- */
24
- export function listOrchSessions(
25
- tmuxPrefix: string,
26
- batchState?: OrchBatchRuntimeState,
27
- ): OrchestratorSessionEntry[] {
28
- let stdout = "";
29
- try {
30
- stdout = execSync('tmux list-sessions -F "#{session_name}"', {
31
- encoding: "utf-8",
32
- timeout: 5000,
33
- });
34
- } catch {
35
- // No tmux server running or no sessions
36
- return [];
37
- }
38
-
39
- const sessionNames = stdout
40
- .trim()
41
- .split("\n")
42
- .filter(Boolean)
43
- .filter(name => name.startsWith(`${tmuxPrefix}-`))
44
- .sort();
45
-
46
- if (sessionNames.length === 0) return [];
47
-
48
- // Build a lookup from current batch state for enrichment
49
- const laneLookup = new Map<string, { laneId: string; taskId: string | null; worktreePath: string }>();
50
- if (batchState && batchState.currentLanes.length > 0) {
51
- for (const lane of batchState.currentLanes) {
52
- laneLookup.set(lane.tmuxSessionName, {
53
- laneId: lane.laneId,
54
- taskId: lane.tasks.length > 0 ? lane.tasks[0].taskId : null,
55
- worktreePath: lane.worktreePath,
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
+ }