taskplane 0.22.18 → 0.23.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/dashboard/public/app.js +365 -7
- package/dashboard/public/index.html +16 -0
- package/dashboard/public/style.css +105 -0
- package/dashboard/server.cjs +199 -0
- package/extensions/task-runner.ts +40 -286
- package/extensions/taskplane/abort.ts +11 -1
- package/extensions/taskplane/agent-bridge-extension.ts +159 -0
- package/extensions/taskplane/agent-host.ts +686 -0
- package/extensions/taskplane/engine.ts +75 -3
- package/extensions/taskplane/execution.ts +403 -9
- package/extensions/taskplane/extension.ts +322 -28
- package/extensions/taskplane/lane-runner.ts +567 -0
- package/extensions/taskplane/mailbox.ts +349 -1
- package/extensions/taskplane/merge.ts +208 -51
- package/extensions/taskplane/process-registry.ts +345 -0
- package/extensions/taskplane/resume.ts +185 -47
- package/extensions/taskplane/supervisor.ts +16 -12
- package/extensions/taskplane/task-executor-core.ts +553 -0
- package/extensions/taskplane/types.ts +517 -1
- package/package.json +1 -1
- package/skills/create-taskplane-task/SKILL.md +41 -33
- package/skills/create-taskplane-task/references/prompt-template.md +3 -3
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process Registry — Runtime V2 agent lifecycle management
|
|
3
|
+
*
|
|
4
|
+
* File-backed registry that replaces TMUX session discovery as the
|
|
5
|
+
* authoritative source of truth for agent liveness, identity, and
|
|
6
|
+
* attribution.
|
|
7
|
+
*
|
|
8
|
+
* Key design rules:
|
|
9
|
+
* 1. Parent writes manifest BEFORE child is considered visible.
|
|
10
|
+
* 2. Parent updates manifest on every status transition.
|
|
11
|
+
* 3. Operator tools read the registry, not TMUX.
|
|
12
|
+
* 4. Resume/cleanup validates pid + startedAt for orphan detection.
|
|
13
|
+
*
|
|
14
|
+
* File locations:
|
|
15
|
+
* .pi/runtime/{batchId}/registry.json — batch-level snapshot
|
|
16
|
+
* .pi/runtime/{batchId}/agents/{agentId}/manifest.json — per-agent
|
|
17
|
+
*
|
|
18
|
+
* @module taskplane/process-registry
|
|
19
|
+
* @since TP-104
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, rmSync, appendFileSync, renameSync } from "fs";
|
|
23
|
+
import { join, dirname } from "path";
|
|
24
|
+
|
|
25
|
+
import {
|
|
26
|
+
TERMINAL_AGENT_STATUSES,
|
|
27
|
+
runtimeRoot,
|
|
28
|
+
runtimeAgentDir,
|
|
29
|
+
runtimeManifestPath,
|
|
30
|
+
runtimeRegistryPath,
|
|
31
|
+
runtimeAgentEventsPath,
|
|
32
|
+
runtimeLaneSnapshotPath,
|
|
33
|
+
validateAgentManifest,
|
|
34
|
+
type RuntimeAgentId,
|
|
35
|
+
type RuntimeAgentManifest,
|
|
36
|
+
type RuntimeAgentRole,
|
|
37
|
+
type RuntimeAgentStatus,
|
|
38
|
+
type RuntimeRegistry,
|
|
39
|
+
type PacketPaths,
|
|
40
|
+
} from "./types.ts";
|
|
41
|
+
|
|
42
|
+
// ── Manifest Lifecycle ───────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Write or update an agent manifest atomically.
|
|
46
|
+
*
|
|
47
|
+
* Uses write-to-temp + rename for crash safety. Creates parent
|
|
48
|
+
* directories if they don't exist.
|
|
49
|
+
*
|
|
50
|
+
* @since TP-104
|
|
51
|
+
*/
|
|
52
|
+
export function writeManifest(stateRoot: string, manifest: RuntimeAgentManifest): void {
|
|
53
|
+
const dir = runtimeAgentDir(stateRoot, manifest.batchId, manifest.agentId);
|
|
54
|
+
mkdirSync(dir, { recursive: true });
|
|
55
|
+
const path = runtimeManifestPath(stateRoot, manifest.batchId, manifest.agentId);
|
|
56
|
+
const tmpPath = path + ".tmp";
|
|
57
|
+
writeFileSync(tmpPath, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
|
|
58
|
+
// Atomic rename (same directory = safe on all platforms)
|
|
59
|
+
renameSync(tmpPath, path);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Read an agent manifest. Returns null if not found or malformed.
|
|
64
|
+
*
|
|
65
|
+
* @since TP-104
|
|
66
|
+
*/
|
|
67
|
+
export function readManifest(stateRoot: string, batchId: string, agentId: RuntimeAgentId): RuntimeAgentManifest | null {
|
|
68
|
+
const path = runtimeManifestPath(stateRoot, batchId, agentId);
|
|
69
|
+
if (!existsSync(path)) return null;
|
|
70
|
+
try {
|
|
71
|
+
const raw = readFileSync(path, "utf-8");
|
|
72
|
+
const parsed = JSON.parse(raw);
|
|
73
|
+
const errors = validateAgentManifest(parsed);
|
|
74
|
+
if (errors.length > 0) {
|
|
75
|
+
console.error(`[process-registry] invalid manifest ${agentId}: ${errors.join(", ")}`);
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
return parsed as RuntimeAgentManifest;
|
|
79
|
+
} catch (err: any) {
|
|
80
|
+
console.error(`[process-registry] failed to read manifest ${agentId}: ${err?.message}`);
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Update an agent's status in its manifest.
|
|
87
|
+
*
|
|
88
|
+
* Reads the current manifest, updates the status field, and writes
|
|
89
|
+
* it back atomically. No-op if manifest doesn't exist.
|
|
90
|
+
*
|
|
91
|
+
* @since TP-104
|
|
92
|
+
*/
|
|
93
|
+
export function updateManifestStatus(
|
|
94
|
+
stateRoot: string,
|
|
95
|
+
batchId: string,
|
|
96
|
+
agentId: RuntimeAgentId,
|
|
97
|
+
status: RuntimeAgentStatus,
|
|
98
|
+
): void {
|
|
99
|
+
const manifest = readManifest(stateRoot, batchId, agentId);
|
|
100
|
+
if (!manifest) return;
|
|
101
|
+
manifest.status = status;
|
|
102
|
+
writeManifest(stateRoot, manifest);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Create a fresh RuntimeAgentManifest with required fields.
|
|
107
|
+
*
|
|
108
|
+
* @since TP-104
|
|
109
|
+
*/
|
|
110
|
+
export function createManifest(opts: {
|
|
111
|
+
batchId: string;
|
|
112
|
+
agentId: RuntimeAgentId;
|
|
113
|
+
role: RuntimeAgentRole;
|
|
114
|
+
laneNumber: number | null;
|
|
115
|
+
taskId: string | null;
|
|
116
|
+
repoId: string;
|
|
117
|
+
pid: number;
|
|
118
|
+
parentPid: number;
|
|
119
|
+
cwd: string;
|
|
120
|
+
packet: PacketPaths | null;
|
|
121
|
+
}): RuntimeAgentManifest {
|
|
122
|
+
return {
|
|
123
|
+
batchId: opts.batchId,
|
|
124
|
+
agentId: opts.agentId,
|
|
125
|
+
role: opts.role,
|
|
126
|
+
laneNumber: opts.laneNumber,
|
|
127
|
+
taskId: opts.taskId,
|
|
128
|
+
repoId: opts.repoId,
|
|
129
|
+
pid: opts.pid,
|
|
130
|
+
parentPid: opts.parentPid,
|
|
131
|
+
startedAt: Date.now(),
|
|
132
|
+
status: "spawning",
|
|
133
|
+
cwd: opts.cwd,
|
|
134
|
+
packet: opts.packet,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ── Registry Snapshot ────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Build a registry snapshot from all agent manifests in a batch.
|
|
142
|
+
*
|
|
143
|
+
* Scans the agents/ directory under the runtime root and reads all
|
|
144
|
+
* valid manifests.
|
|
145
|
+
*
|
|
146
|
+
* @since TP-104
|
|
147
|
+
*/
|
|
148
|
+
export function buildRegistrySnapshot(stateRoot: string, batchId: string): RuntimeRegistry {
|
|
149
|
+
const agentsDir = join(runtimeRoot(stateRoot, batchId), "agents");
|
|
150
|
+
const agents: Record<RuntimeAgentId, RuntimeAgentManifest> = {};
|
|
151
|
+
|
|
152
|
+
if (existsSync(agentsDir)) {
|
|
153
|
+
try {
|
|
154
|
+
const entries = readdirSync(agentsDir, { withFileTypes: true });
|
|
155
|
+
for (const entry of entries) {
|
|
156
|
+
if (!entry.isDirectory()) continue;
|
|
157
|
+
const agentId = entry.name;
|
|
158
|
+
const manifest = readManifest(stateRoot, batchId, agentId);
|
|
159
|
+
if (manifest) {
|
|
160
|
+
agents[agentId] = manifest;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
} catch (err: any) {
|
|
164
|
+
console.error(`[process-registry] failed to scan agents dir: ${err?.message}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
batchId,
|
|
170
|
+
updatedAt: Date.now(),
|
|
171
|
+
agents,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Write the registry snapshot to disk.
|
|
177
|
+
*
|
|
178
|
+
* @since TP-104
|
|
179
|
+
*/
|
|
180
|
+
export function writeRegistrySnapshot(stateRoot: string, registry: RuntimeRegistry): void {
|
|
181
|
+
const path = runtimeRegistryPath(stateRoot, registry.batchId);
|
|
182
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
183
|
+
const tmpPath = path + ".tmp";
|
|
184
|
+
writeFileSync(tmpPath, JSON.stringify(registry, null, 2) + "\n", "utf-8");
|
|
185
|
+
renameSync(tmpPath, path);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Read the registry snapshot from disk. Returns null if not found.
|
|
190
|
+
*
|
|
191
|
+
* @since TP-104
|
|
192
|
+
*/
|
|
193
|
+
export function readRegistrySnapshot(stateRoot: string, batchId: string): RuntimeRegistry | null {
|
|
194
|
+
const path = runtimeRegistryPath(stateRoot, batchId);
|
|
195
|
+
if (!existsSync(path)) return null;
|
|
196
|
+
try {
|
|
197
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
198
|
+
} catch {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ── Liveness Checks ──────────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Check whether a process with the given PID is still alive.
|
|
207
|
+
*
|
|
208
|
+
* Uses `process.kill(pid, 0)` which sends no signal but checks existence.
|
|
209
|
+
* Returns false for PID 0, negative PIDs, and dead processes.
|
|
210
|
+
*
|
|
211
|
+
* @since TP-104
|
|
212
|
+
*/
|
|
213
|
+
export function isProcessAlive(pid: number): boolean {
|
|
214
|
+
if (!pid || pid <= 0 || !Number.isFinite(pid)) return false;
|
|
215
|
+
try {
|
|
216
|
+
process.kill(pid, 0);
|
|
217
|
+
return true;
|
|
218
|
+
} catch {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Determine if an agent is in a terminal (non-alive) state.
|
|
225
|
+
*
|
|
226
|
+
* @since TP-104
|
|
227
|
+
*/
|
|
228
|
+
export function isTerminalStatus(status: RuntimeAgentStatus): boolean {
|
|
229
|
+
return TERMINAL_AGENT_STATUSES.has(status);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Get all live (non-terminal) agents from a registry snapshot.
|
|
234
|
+
*
|
|
235
|
+
* @since TP-104
|
|
236
|
+
*/
|
|
237
|
+
export function getLiveAgents(registry: RuntimeRegistry): RuntimeAgentManifest[] {
|
|
238
|
+
return Object.values(registry.agents).filter(m => !isTerminalStatus(m.status));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Get all agents matching a specific role from a registry snapshot.
|
|
243
|
+
*
|
|
244
|
+
* @since TP-104
|
|
245
|
+
*/
|
|
246
|
+
export function getAgentsByRole(registry: RuntimeRegistry, role: RuntimeAgentRole): RuntimeAgentManifest[] {
|
|
247
|
+
return Object.values(registry.agents).filter(m => m.role === role);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ── Orphan Detection ─────────────────────────────────────────────────
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Detect orphaned agents — manifests that claim to be running but whose
|
|
254
|
+
* process is no longer alive.
|
|
255
|
+
*
|
|
256
|
+
* Returns agent IDs of orphans. Caller decides whether to terminate,
|
|
257
|
+
* update manifest status, or log.
|
|
258
|
+
*
|
|
259
|
+
* @since TP-104
|
|
260
|
+
*/
|
|
261
|
+
export function detectOrphans(registry: RuntimeRegistry): RuntimeAgentId[] {
|
|
262
|
+
const orphans: RuntimeAgentId[] = [];
|
|
263
|
+
for (const manifest of Object.values(registry.agents)) {
|
|
264
|
+
if (isTerminalStatus(manifest.status)) continue;
|
|
265
|
+
if (!isProcessAlive(manifest.pid)) {
|
|
266
|
+
orphans.push(manifest.agentId);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return orphans;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Mark detected orphans as crashed in their manifests.
|
|
274
|
+
*
|
|
275
|
+
* @since TP-104
|
|
276
|
+
*/
|
|
277
|
+
export function markOrphansCrashed(stateRoot: string, batchId: string, orphanIds: RuntimeAgentId[]): void {
|
|
278
|
+
for (const agentId of orphanIds) {
|
|
279
|
+
updateManifestStatus(stateRoot, batchId, agentId, "crashed");
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ── Cleanup ──────────────────────────────────────────────────────────
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Remove all runtime artifacts for a batch.
|
|
287
|
+
*
|
|
288
|
+
* Best-effort: logs errors but doesn't throw.
|
|
289
|
+
*
|
|
290
|
+
* @since TP-104
|
|
291
|
+
*/
|
|
292
|
+
export function cleanupBatchRuntime(stateRoot: string, batchId: string): { removed: boolean; error?: string } {
|
|
293
|
+
const root = runtimeRoot(stateRoot, batchId);
|
|
294
|
+
if (!existsSync(root)) return { removed: false };
|
|
295
|
+
try {
|
|
296
|
+
rmSync(root, { recursive: true, force: true });
|
|
297
|
+
return { removed: true };
|
|
298
|
+
} catch (err: any) {
|
|
299
|
+
console.error(`[process-registry] failed to cleanup batch runtime: ${err?.message}`);
|
|
300
|
+
return { removed: false, error: err?.message };
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ── Normalized Event Helpers ─────────────────────────────────────────
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Append a normalized event to an agent's event log.
|
|
308
|
+
*
|
|
309
|
+
* Creates the events file and parent directories if they don't exist.
|
|
310
|
+
* Best-effort: logs errors but doesn't throw.
|
|
311
|
+
*
|
|
312
|
+
* @since TP-104
|
|
313
|
+
*/
|
|
314
|
+
export function appendAgentEvent(
|
|
315
|
+
stateRoot: string,
|
|
316
|
+
batchId: string,
|
|
317
|
+
agentId: RuntimeAgentId,
|
|
318
|
+
event: Record<string, unknown>,
|
|
319
|
+
): void {
|
|
320
|
+
const path = runtimeAgentEventsPath(stateRoot, batchId, agentId);
|
|
321
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
322
|
+
try {
|
|
323
|
+
appendFileSync(path, JSON.stringify(event) + "\n", "utf-8");
|
|
324
|
+
} catch (err: any) {
|
|
325
|
+
console.error(`[process-registry] failed to append event for ${agentId}: ${err?.message}`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Write a lane snapshot to disk.
|
|
331
|
+
*
|
|
332
|
+
* @since TP-104
|
|
333
|
+
*/
|
|
334
|
+
export function writeLaneSnapshot(
|
|
335
|
+
stateRoot: string,
|
|
336
|
+
batchId: string,
|
|
337
|
+
laneNumber: number,
|
|
338
|
+
snapshot: Record<string, unknown>,
|
|
339
|
+
): void {
|
|
340
|
+
const path = runtimeLaneSnapshotPath(stateRoot, batchId, laneNumber);
|
|
341
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
342
|
+
const tmpPath = path + ".tmp";
|
|
343
|
+
writeFileSync(tmpPath, JSON.stringify(snapshot, null, 2) + "\n", "utf-8");
|
|
344
|
+
renameSync(tmpPath, path);
|
|
345
|
+
}
|
|
@@ -8,8 +8,30 @@ 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, executeWave, pollUntilTaskComplete, spawnLaneSession, tmuxHasSession } from "./execution.ts";
|
|
12
|
-
import type { MonitorUpdateCallback } from "./execution.ts";
|
|
11
|
+
import { computeTransitiveDependents, execLog, executeLaneV2, executeWave, pollUntilTaskComplete, resolveCanonicalTaskPaths, spawnLaneSession, tmuxHasSession } from "./execution.ts";
|
|
12
|
+
import type { MonitorUpdateCallback, RuntimeBackend } from "./execution.ts";
|
|
13
|
+
import { selectRuntimeBackend } from "./engine.ts";
|
|
14
|
+
import { readRegistrySnapshot, isTerminalStatus, isProcessAlive } from "./process-registry.ts";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* TP-112: Terminate any alive V2 agents for a lane before re-execution.
|
|
18
|
+
* Per Runtime V2 spec §7.3: detect + terminate + rehydrate.
|
|
19
|
+
* Prevents duplicate concurrent agents for the same lane/task on resume.
|
|
20
|
+
*/
|
|
21
|
+
function terminateAliveV2Agents(stateRoot: string, batchId: string, sessionName: string): void {
|
|
22
|
+
const registry = readRegistrySnapshot(stateRoot, batchId);
|
|
23
|
+
if (!registry) return;
|
|
24
|
+
for (const suffix of ["-worker", "-reviewer", ""]) {
|
|
25
|
+
const key = `${sessionName}${suffix}`;
|
|
26
|
+
const manifest = registry.agents[key];
|
|
27
|
+
if (manifest && !isTerminalStatus(manifest.status) && isProcessAlive(manifest.pid)) {
|
|
28
|
+
try {
|
|
29
|
+
process.kill(manifest.pid, "SIGTERM");
|
|
30
|
+
execLog("resume", key, `terminated alive V2 agent (PID ${manifest.pid}) before re-execute`);
|
|
31
|
+
} catch { /* already dead */ }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
13
35
|
import { getCurrentBranch, runGit } from "./git.ts";
|
|
14
36
|
import { mergeWaveByRepo } from "./merge.ts";
|
|
15
37
|
import { applyMergeRetryLoop, computeCleanupGatePolicy, computeMergeFailurePolicy, formatRepoMergeSummary, ORCH_MESSAGES } from "./messages.ts";
|
|
@@ -851,20 +873,64 @@ export async function resumeOrchBatch(
|
|
|
851
873
|
"info",
|
|
852
874
|
);
|
|
853
875
|
|
|
876
|
+
// TP-108/112: Runtime V2 backend selection for resumed batches.
|
|
877
|
+
// MUST be computed before any backend-aware branch (section 3+).
|
|
878
|
+
const resumeBackend: RuntimeBackend = selectRuntimeBackend(
|
|
879
|
+
"all",
|
|
880
|
+
persistedState.wavePlan,
|
|
881
|
+
workspaceConfig,
|
|
882
|
+
).backend;
|
|
883
|
+
execLog("resume", batchState.batchId, `runtime backend for resumed execution: ${resumeBackend}`);
|
|
884
|
+
|
|
854
885
|
// ── 3. Discover live signals ─────────────────────────────────
|
|
855
|
-
//
|
|
886
|
+
// TP-112: Backend-aware session liveness check.
|
|
887
|
+
// V2: check process registry (pid + status). Legacy: check TMUX.
|
|
856
888
|
const aliveSessions = new Set<string>();
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
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);
|
|
907
|
+
}
|
|
860
908
|
}
|
|
861
909
|
}
|
|
862
910
|
|
|
863
|
-
// Check .DONE files
|
|
911
|
+
// Check .DONE files — check both original path and worktree-relative path.
|
|
912
|
+
// TP-109: In workspace mode or V2 execution, .DONE is written in the worktree
|
|
913
|
+
// at the resolved packet path, not the original discovery path. Resume must
|
|
914
|
+
// check both locations for authoritative completion detection.
|
|
864
915
|
const doneTaskIds = new Set<string>();
|
|
865
916
|
for (const task of persistedState.tasks) {
|
|
917
|
+
// Check original task folder path
|
|
866
918
|
if (task.taskFolder && hasTaskDoneMarker(task.taskFolder)) {
|
|
867
919
|
doneTaskIds.add(task.taskId);
|
|
920
|
+
continue;
|
|
921
|
+
}
|
|
922
|
+
// Check worktree-relative path (packet-home authority)
|
|
923
|
+
const laneRec = persistedState.lanes.find(l => l.taskIds.includes(task.taskId));
|
|
924
|
+
if (laneRec?.worktreePath && task.taskFolder) {
|
|
925
|
+
const resolved = resolveCanonicalTaskPaths(
|
|
926
|
+
task.taskFolder,
|
|
927
|
+
laneRec.worktreePath,
|
|
928
|
+
repoRoot,
|
|
929
|
+
!!workspaceConfig,
|
|
930
|
+
);
|
|
931
|
+
if (existsSync(resolved.donePath)) {
|
|
932
|
+
doneTaskIds.add(task.taskId);
|
|
933
|
+
}
|
|
868
934
|
}
|
|
869
935
|
}
|
|
870
936
|
|
|
@@ -1035,6 +1101,7 @@ export async function resumeOrchBatch(
|
|
|
1035
1101
|
const depGraph = buildDependencyGraph(discovery.pending, discovery.completed);
|
|
1036
1102
|
batchState.dependencyGraph = depGraph;
|
|
1037
1103
|
|
|
1104
|
+
|
|
1038
1105
|
// ── 8. Handle alive sessions (reconnect) ─────────────────────
|
|
1039
1106
|
// For tasks with alive sessions, we need to wait for them to complete.
|
|
1040
1107
|
// We poll each alive session's .DONE file.
|
|
@@ -1076,44 +1143,82 @@ export async function resumeOrchBatch(
|
|
|
1076
1143
|
// Resolve per-lane repo root for workspace mode (v1/repo mode: falls back to repoRoot)
|
|
1077
1144
|
const laneRepoRoot = resolveRepoRoot(laneRecord.repoId, repoRoot, workspaceConfig);
|
|
1078
1145
|
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
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);
|
|
1182
|
+
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
|
+
});
|
|
1083
1191
|
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
);
|
|
1192
|
+
try {
|
|
1193
|
+
const pollResult = await pollUntilTaskComplete(
|
|
1194
|
+
lane,
|
|
1195
|
+
allocatedTask,
|
|
1196
|
+
orchConfig,
|
|
1197
|
+
laneRepoRoot,
|
|
1198
|
+
batchState.pauseSignal,
|
|
1199
|
+
);
|
|
1093
1200
|
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
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) {
|
|
1102
1215
|
reconnectFinalStatus.set(task.taskId, "failed");
|
|
1103
1216
|
failedTaskSet.add(task.taskId);
|
|
1104
1217
|
completedTaskSet.delete(task.taskId);
|
|
1105
1218
|
reconnectTaskSet.delete(task.taskId);
|
|
1106
1219
|
batchState.failedTasks++;
|
|
1107
|
-
execLog("resume", task.taskId, `
|
|
1220
|
+
execLog("resume", task.taskId, `reconnection error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1108
1221
|
}
|
|
1109
|
-
} catch (err: unknown) {
|
|
1110
|
-
reconnectFinalStatus.set(task.taskId, "failed");
|
|
1111
|
-
failedTaskSet.add(task.taskId);
|
|
1112
|
-
completedTaskSet.delete(task.taskId);
|
|
1113
|
-
reconnectTaskSet.delete(task.taskId);
|
|
1114
|
-
batchState.failedTasks++;
|
|
1115
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1116
|
-
execLog("resume", task.taskId, `reconnection error: ${msg}`);
|
|
1117
1222
|
}
|
|
1118
1223
|
}
|
|
1119
1224
|
}
|
|
@@ -1167,16 +1272,35 @@ export async function resumeOrchBatch(
|
|
|
1167
1272
|
});
|
|
1168
1273
|
|
|
1169
1274
|
try {
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
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
|
+
}
|
|
1180
1304
|
|
|
1181
1305
|
if (pollResult.status === "succeeded") {
|
|
1182
1306
|
reExecuteFinalStatus.set(task.taskId, "succeeded");
|
|
@@ -1275,6 +1399,9 @@ export async function resumeOrchBatch(
|
|
|
1275
1399
|
stateRoot,
|
|
1276
1400
|
agentRoot,
|
|
1277
1401
|
runnerConfig.testing_commands,
|
|
1402
|
+
undefined, // healthMonitor
|
|
1403
|
+
undefined, // forceMixedOutcome
|
|
1404
|
+
resumeBackend,
|
|
1278
1405
|
);
|
|
1279
1406
|
|
|
1280
1407
|
if (reExecMergeResult.status === "succeeded") {
|
|
@@ -1530,6 +1657,9 @@ export async function resumeOrchBatch(
|
|
|
1530
1657
|
stateRoot,
|
|
1531
1658
|
agentRoot,
|
|
1532
1659
|
runnerConfig.testing_commands,
|
|
1660
|
+
undefined, // healthMonitor
|
|
1661
|
+
undefined, // forceMixedOutcome
|
|
1662
|
+
resumeBackend,
|
|
1533
1663
|
);
|
|
1534
1664
|
batchState.mergeResults.push(mergeRetryResult);
|
|
1535
1665
|
|
|
@@ -1603,6 +1733,8 @@ export async function resumeOrchBatch(
|
|
|
1603
1733
|
}
|
|
1604
1734
|
},
|
|
1605
1735
|
workspaceConfig,
|
|
1736
|
+
resumeBackend,
|
|
1737
|
+
emitAlert,
|
|
1606
1738
|
);
|
|
1607
1739
|
|
|
1608
1740
|
batchState.waveResults.push(waveResult);
|
|
@@ -1740,6 +1872,9 @@ export async function resumeOrchBatch(
|
|
|
1740
1872
|
stateRoot,
|
|
1741
1873
|
agentRoot,
|
|
1742
1874
|
runnerConfig.testing_commands,
|
|
1875
|
+
undefined, // healthMonitor
|
|
1876
|
+
undefined, // forceMixedOutcome
|
|
1877
|
+
resumeBackend,
|
|
1743
1878
|
);
|
|
1744
1879
|
batchState.mergeResults.push(mergeResult);
|
|
1745
1880
|
|
|
@@ -1904,6 +2039,9 @@ export async function resumeOrchBatch(
|
|
|
1904
2039
|
stateRoot,
|
|
1905
2040
|
agentRoot,
|
|
1906
2041
|
runnerConfig.testing_commands,
|
|
2042
|
+
undefined, // healthMonitor
|
|
2043
|
+
undefined, // forceMixedOutcome
|
|
2044
|
+
resumeBackend,
|
|
1907
2045
|
);
|
|
1908
2046
|
},
|
|
1909
2047
|
persist: (trigger) => persistRuntimeState(trigger, batchState, wavePlan, latestAllocatedLanes, allTaskOutcomes, discovery, stateRoot),
|