taskplane 0.1.7 → 0.1.8
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.
|
@@ -480,6 +480,12 @@ export async function pollUntilTaskComplete(
|
|
|
480
480
|
|
|
481
481
|
let lastPaneTail = "";
|
|
482
482
|
|
|
483
|
+
// Abort signal file path — checked each poll cycle.
|
|
484
|
+
// Any process can create this file to trigger abort (belt-and-suspenders
|
|
485
|
+
// alongside the in-memory pauseSignal, since /orch-abort may not be able
|
|
486
|
+
// to run concurrently with the /orch command handler).
|
|
487
|
+
const abortSignalFile = join(repoRoot, ".pi", "orch-abort-signal");
|
|
488
|
+
|
|
483
489
|
// Main polling loop
|
|
484
490
|
while (true) {
|
|
485
491
|
// Check pause signal
|
|
@@ -494,6 +500,20 @@ export async function pollUntilTaskComplete(
|
|
|
494
500
|
};
|
|
495
501
|
}
|
|
496
502
|
|
|
503
|
+
// Check file-based abort signal
|
|
504
|
+
if (existsSync(abortSignalFile)) {
|
|
505
|
+
execLog(laneId, task.taskId, "abort signal file detected — killing session and aborting");
|
|
506
|
+
tmuxKillSession(sessionName);
|
|
507
|
+
// Also kill child sessions (worker, reviewer)
|
|
508
|
+
tmuxKillSession(`${sessionName}-worker`);
|
|
509
|
+
tmuxKillSession(`${sessionName}-reviewer`);
|
|
510
|
+
return {
|
|
511
|
+
status: "failed",
|
|
512
|
+
exitReason: "Aborted by signal file (.pi/orch-abort-signal)",
|
|
513
|
+
doneFileFound: false,
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
497
517
|
// Capture live pane output for diagnostics (best effort).
|
|
498
518
|
const paneTail = captureTmuxPaneTail(sessionName);
|
|
499
519
|
if (paneTail) {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
|
|
3
3
|
import { execSync } from "child_process";
|
|
4
|
+
import { writeFileSync, unlinkSync, mkdirSync } from "fs";
|
|
5
|
+
import { join } from "path";
|
|
4
6
|
|
|
5
7
|
import {
|
|
6
8
|
DEFAULT_ORCHESTRATOR_CONFIG,
|
|
@@ -10,7 +12,6 @@ import {
|
|
|
10
12
|
createOrchWidget,
|
|
11
13
|
deleteBatchState,
|
|
12
14
|
detectOrphanSessions,
|
|
13
|
-
executeAbort,
|
|
14
15
|
executeLane,
|
|
15
16
|
executeOrchBatch,
|
|
16
17
|
formatDependencyGraph,
|
|
@@ -347,113 +348,144 @@ export default function (pi: ExtensionAPI) {
|
|
|
347
348
|
pi.registerCommand("orch-abort", {
|
|
348
349
|
description: "Abort batch: /orch-abort [--hard]",
|
|
349
350
|
handler: async (args, ctx) => {
|
|
350
|
-
const hard = args?.trim() === "--hard";
|
|
351
|
-
const mode: AbortMode = hard ? "hard" : "graceful";
|
|
352
|
-
const prefix = orchConfig.orchestrator.tmux_prefix;
|
|
353
|
-
const gracePeriodMs = orchConfig.orchestrator.abort_grace_period * 1000;
|
|
354
|
-
|
|
355
|
-
// Check for active in-memory batch
|
|
356
|
-
const hasActiveBatch = orchBatchState.phase !== "idle" &&
|
|
357
|
-
orchBatchState.phase !== "completed" &&
|
|
358
|
-
orchBatchState.phase !== "failed" &&
|
|
359
|
-
orchBatchState.phase !== "stopped";
|
|
360
|
-
|
|
361
|
-
// Also check for persisted state (abort can work on orphaned batches too)
|
|
362
|
-
let persistedState: PersistedBatchState | null = null;
|
|
363
351
|
try {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
352
|
+
const hard = args?.trim() === "--hard";
|
|
353
|
+
const mode: AbortMode = hard ? "hard" : "graceful";
|
|
354
|
+
const prefix = orchConfig.orchestrator.tmux_prefix;
|
|
355
|
+
const gracePeriodMs = orchConfig.orchestrator.abort_grace_period * 1000;
|
|
356
|
+
|
|
357
|
+
ctx.ui.notify(`🛑 Abort requested (${mode} mode, prefix: ${prefix})...`, "info");
|
|
358
|
+
|
|
359
|
+
// ── Step 1: Write abort signal file immediately ──────────
|
|
360
|
+
// This is the primary abort mechanism. The orchestrator's polling
|
|
361
|
+
// loop checks for this file on every cycle, so even if this command
|
|
362
|
+
// handler runs concurrently with /orch (or is queued behind it),
|
|
363
|
+
// the signal file will be detected.
|
|
364
|
+
const abortSignalFile = join(ctx.cwd, ".pi", "orch-abort-signal");
|
|
365
|
+
try {
|
|
366
|
+
mkdirSync(join(ctx.cwd, ".pi"), { recursive: true });
|
|
367
|
+
writeFileSync(abortSignalFile, `abort requested at ${new Date().toISOString()} (mode: ${mode})`, "utf-8");
|
|
368
|
+
ctx.ui.notify(" ✓ Abort signal file written (.pi/orch-abort-signal)", "info");
|
|
369
|
+
} catch (err) {
|
|
370
|
+
ctx.ui.notify(` ⚠ Failed to write abort signal file: ${err instanceof Error ? err.message : String(err)}`, "warning");
|
|
371
|
+
}
|
|
368
372
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
//
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
return execSync('tmux list-sessions -F "#{session_name}"', {
|
|
376
|
-
encoding: "utf-8",
|
|
377
|
-
timeout: 5000,
|
|
378
|
-
});
|
|
379
|
-
} catch {
|
|
380
|
-
return "";
|
|
381
|
-
}
|
|
382
|
-
})(),
|
|
383
|
-
prefix,
|
|
384
|
-
);
|
|
385
|
-
if (sessionNames.length === 0) {
|
|
386
|
-
ctx.ui.notify(ORCH_MESSAGES.abortNoBatch(), "warning");
|
|
387
|
-
return;
|
|
373
|
+
// ── Step 2: Set pause signal immediately ─────────────────
|
|
374
|
+
// Belt-and-suspenders: if the /orch polling loop can see this
|
|
375
|
+
// shared object, it will stop on the next iteration.
|
|
376
|
+
if (orchBatchState.pauseSignal) {
|
|
377
|
+
orchBatchState.pauseSignal.paused = true;
|
|
378
|
+
ctx.ui.notify(" ✓ Pause signal set on in-memory batch state", "info");
|
|
388
379
|
}
|
|
389
|
-
// If orphan sessions exist, proceed with abort (will kill them)
|
|
390
|
-
}
|
|
391
380
|
|
|
392
|
-
|
|
381
|
+
// ── Step 3: Check what we're aborting ────────────────────
|
|
382
|
+
const hasActiveBatch = orchBatchState.phase !== "idle" &&
|
|
383
|
+
orchBatchState.phase !== "completed" &&
|
|
384
|
+
orchBatchState.phase !== "failed" &&
|
|
385
|
+
orchBatchState.phase !== "stopped";
|
|
386
|
+
|
|
387
|
+
let persistedState: PersistedBatchState | null = null;
|
|
388
|
+
try {
|
|
389
|
+
persistedState = loadBatchState(ctx.cwd);
|
|
390
|
+
} catch {
|
|
391
|
+
// Ignore — we may still have in-memory state or orphan sessions
|
|
392
|
+
}
|
|
393
393
|
|
|
394
|
-
// Notify user of abort start
|
|
395
|
-
if (mode === "graceful") {
|
|
396
|
-
const sessionCount = orchBatchState.currentLanes.length || persistedState?.tasks.length || 0;
|
|
397
|
-
ctx.ui.notify(ORCH_MESSAGES.abortGracefulStarting(batchId, sessionCount), "info");
|
|
398
394
|
ctx.ui.notify(
|
|
399
|
-
|
|
395
|
+
` Batch state: in-memory=${hasActiveBatch ? orchBatchState.phase : "none"}, ` +
|
|
396
|
+
`persisted=${persistedState ? persistedState.batchId : "none"}`,
|
|
400
397
|
"info",
|
|
401
398
|
);
|
|
402
|
-
} else {
|
|
403
|
-
const sessionCount = orchBatchState.currentLanes.length || persistedState?.tasks.length || 0;
|
|
404
|
-
ctx.ui.notify(ORCH_MESSAGES.abortHardStarting(batchId, sessionCount), "info");
|
|
405
|
-
}
|
|
406
399
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
400
|
+
// ── Step 4: Scan for tmux sessions ──────────────────────
|
|
401
|
+
let allSessionNames: string[] = [];
|
|
402
|
+
try {
|
|
403
|
+
const tmuxOutput = execSync('tmux list-sessions -F "#{session_name}"', {
|
|
404
|
+
encoding: "utf-8",
|
|
405
|
+
timeout: 5000,
|
|
406
|
+
}).trim();
|
|
407
|
+
const all = tmuxOutput ? tmuxOutput.split("\n").map(s => s.trim()).filter(Boolean) : [];
|
|
408
|
+
allSessionNames = all.filter(name => name.startsWith(`${prefix}-`));
|
|
409
|
+
ctx.ui.notify(` Found ${allSessionNames.length} session(s) matching prefix "${prefix}-": ${allSessionNames.join(", ") || "(none)"}`, "info");
|
|
410
|
+
} catch {
|
|
411
|
+
ctx.ui.notify(" ⚠ Could not list tmux sessions (tmux not available?)", "warning");
|
|
412
|
+
}
|
|
416
413
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
414
|
+
// If no batch AND no sessions, nothing to abort
|
|
415
|
+
if (!hasActiveBatch && !persistedState && allSessionNames.length === 0) {
|
|
416
|
+
ctx.ui.notify(ORCH_MESSAGES.abortNoBatch(), "warning");
|
|
417
|
+
// Clean up signal file
|
|
418
|
+
try { unlinkSync(abortSignalFile); } catch {}
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const batchId = orchBatchState.batchId || persistedState?.batchId || "unknown";
|
|
421
423
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
424
|
+
// ── Step 5: Kill sessions directly (fast path) ──────────
|
|
425
|
+
// For hard mode or when sessions are found, kill them immediately
|
|
426
|
+
// rather than waiting through the full executeAbort flow.
|
|
427
|
+
if (allSessionNames.length > 0) {
|
|
428
|
+
ctx.ui.notify(` Killing ${allSessionNames.length} tmux session(s)...`, "info");
|
|
429
|
+
let killed = 0;
|
|
430
|
+
for (const name of allSessionNames) {
|
|
431
|
+
try {
|
|
432
|
+
// Kill child sessions first (worker, reviewer)
|
|
433
|
+
execSync(`tmux kill-session -t "${name}-worker" 2>/dev/null`, { timeout: 3000 }).toString();
|
|
434
|
+
} catch {}
|
|
435
|
+
try {
|
|
436
|
+
execSync(`tmux kill-session -t "${name}-reviewer" 2>/dev/null`, { timeout: 3000 }).toString();
|
|
437
|
+
} catch {}
|
|
438
|
+
try {
|
|
439
|
+
execSync(`tmux kill-session -t "${name}" 2>/dev/null`, { timeout: 3000 }).toString();
|
|
440
|
+
killed++;
|
|
441
|
+
ctx.ui.notify(` ✓ Killed: ${name}`, "info");
|
|
442
|
+
} catch {
|
|
443
|
+
// Session may have already exited
|
|
444
|
+
ctx.ui.notify(` · ${name} (already exited)`, "info");
|
|
445
|
+
killed++;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
ctx.ui.notify(` ✓ ${killed}/${allSessionNames.length} session(s) terminated`, "info");
|
|
449
|
+
} else {
|
|
450
|
+
ctx.ui.notify(" No tmux sessions to kill", "info");
|
|
431
451
|
}
|
|
452
|
+
|
|
453
|
+
// ── Step 6: Clean up batch state ────────────────────────
|
|
454
|
+
try {
|
|
455
|
+
orchBatchState.phase = "stopped";
|
|
456
|
+
orchBatchState.endedAt = Date.now();
|
|
457
|
+
updateOrchWidget();
|
|
458
|
+
ctx.ui.notify(" ✓ In-memory batch state set to 'stopped'", "info");
|
|
459
|
+
} catch (err) {
|
|
460
|
+
ctx.ui.notify(` ⚠ Failed to update in-memory state: ${err instanceof Error ? err.message : String(err)}`, "warning");
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
try {
|
|
464
|
+
deleteBatchState(ctx.cwd);
|
|
465
|
+
ctx.ui.notify(" ✓ Batch state file deleted (.pi/batch-state.json)", "info");
|
|
466
|
+
} catch (err) {
|
|
467
|
+
ctx.ui.notify(` ⚠ Failed to delete batch state file: ${err instanceof Error ? err.message : String(err)}`, "warning");
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// ── Step 7: Clean up abort signal file ───────────────────
|
|
471
|
+
try { unlinkSync(abortSignalFile); } catch {}
|
|
472
|
+
|
|
473
|
+
// ── Done ─────────────────────────────────────────────────
|
|
432
474
|
ctx.ui.notify(
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
);
|
|
436
|
-
} else {
|
|
437
|
-
ctx.ui.notify(
|
|
438
|
-
ORCH_MESSAGES.abortHardComplete(batchId, result.sessionsKilled, durationSec),
|
|
475
|
+
`✅ Abort complete for batch ${batchId}. Sessions killed, state cleaned up.\n` +
|
|
476
|
+
` Worktrees and branches are preserved for inspection.`,
|
|
439
477
|
"info",
|
|
440
478
|
);
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
// Report errors if any
|
|
444
|
-
if (result.errors.length > 0) {
|
|
445
|
-
const errorDetails = result.errors.map(e => ` • [${e.code}] ${e.message}`).join("\n");
|
|
479
|
+
} catch (err) {
|
|
480
|
+
// Top-level catch: ensure the user ALWAYS sees something
|
|
446
481
|
ctx.ui.notify(
|
|
447
|
-
|
|
448
|
-
"
|
|
482
|
+
`❌ Abort failed with error: ${err instanceof Error ? err.message : String(err)}\n` +
|
|
483
|
+
` Stack: ${err instanceof Error ? err.stack : "N/A"}\n\n` +
|
|
484
|
+
` Manual cleanup: tmux kill-server (kills ALL tmux sessions)\n` +
|
|
485
|
+
` Or: tmux kill-session -t <session-name> for each session`,
|
|
486
|
+
"error",
|
|
449
487
|
);
|
|
450
488
|
}
|
|
451
|
-
|
|
452
|
-
// Final message
|
|
453
|
-
ctx.ui.notify(
|
|
454
|
-
ORCH_MESSAGES.abortComplete(mode, result.sessionsKilled),
|
|
455
|
-
"info",
|
|
456
|
-
);
|
|
457
489
|
},
|
|
458
490
|
});
|
|
459
491
|
|