pi-herdsman 0.17.0 → 0.17.1

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/dist/index.js CHANGED
@@ -31,7 +31,7 @@ import { fileURLToPath as fileURLToPath2 } from "node:url";
31
31
  // package.json
32
32
  var package_default = {
33
33
  name: "pi-herdsman",
34
- version: "0.17.0",
34
+ version: "0.17.1",
35
35
  type: "module",
36
36
  description: "Asynchronous Pi subagents and agent fleet orchestration for parallel coding agents with nested delegation, background work, and supervision in herdr.",
37
37
  license: "Apache-2.0",
@@ -1805,6 +1805,7 @@ var HerdrStartFailure = class extends Error {
1805
1805
  };
1806
1806
  var SETTLE_TIMEOUT = 2e3;
1807
1807
  var START_DIAGNOSTIC_TIMEOUT = 2e3;
1808
+ var FRESH_PANE_BUSY_RETRY_TIMEOUT = 2e3;
1808
1809
  var HERDR_START_TIMEOUT_MIN = 3001;
1809
1810
  var HERDR_START_TIMEOUT_MAX = 3e5;
1810
1811
  var STARTUP_TIMEOUT_MIN = HERDR_START_TIMEOUT_MIN + START_DIAGNOSTIC_TIMEOUT;
@@ -2044,6 +2045,12 @@ function sameCwd(observed, expected) {
2044
2045
  if (typeof observed !== "string") return false;
2045
2046
  return canonicalCwd(observed) === canonicalCwd(expected);
2046
2047
  }
2048
+ function matchesAttemptTerminal(pane, attempt) {
2049
+ return pane?.pane_id === attempt.paneId && pane.workspace_id === attempt.workspaceId && pane.tab_id === attempt.tabId && pane.terminal_id === attempt.terminalId;
2050
+ }
2051
+ function matchesAttemptPane(pane, attempt) {
2052
+ return matchesAttemptTerminal(pane, attempt) && sameCwd(pane.cwd, attempt.cwd);
2053
+ }
2047
2054
  function capturedShellProcess(process2) {
2048
2055
  return process2.foreground_processes?.find(
2049
2056
  (candidate) => candidate.pid === process2.shell_pid
@@ -2383,6 +2390,7 @@ async function startHerdrAgent(pi, ctx, options) {
2383
2390
  const env = validateEnvironment(options.env ?? []);
2384
2391
  const release = await lockLifecycle(ctx, options.signal);
2385
2392
  let attempt;
2393
+ let retryAttempted = false;
2386
2394
  let stage = "topology";
2387
2395
  try {
2388
2396
  const cwd = canonicalCwd(options.cwd);
@@ -2397,7 +2405,7 @@ async function startHerdrAgent(pi, ctx, options) {
2397
2405
  error("start", "caller pane is required for split placement");
2398
2406
  let tab;
2399
2407
  let panes;
2400
- let paneId;
2408
+ let createdPane;
2401
2409
  let createdTab = false;
2402
2410
  const tabs = placement.kind === "tab" && !placement.tabId ? void 0 : (await runHerdr(
2403
2411
  pi,
@@ -2461,7 +2469,7 @@ async function startHerdrAgent(pi, ctx, options) {
2461
2469
  }
2462
2470
  );
2463
2471
  tab = made.tab;
2464
- paneId = made.root_pane.pane_id;
2472
+ createdPane = made.root_pane;
2465
2473
  createdTab = true;
2466
2474
  } else {
2467
2475
  const existingPanes = panes ?? (await runHerdr(
@@ -2511,31 +2519,44 @@ async function startHerdrAgent(pi, ctx, options) {
2511
2519
  timeout: startupCallTimeout(startupDeadline)
2512
2520
  }
2513
2521
  );
2514
- paneId = split.pane.pane_id;
2522
+ createdPane = split.pane;
2515
2523
  }
2516
2524
  }
2517
- if (!paneId) error("start", "Herdr did not return a pane");
2525
+ const paneId = createdPane?.pane_id;
2526
+ const terminalId = createdPane?.terminal_id;
2527
+ if (typeof paneId !== "string" || !paneId)
2528
+ error("start", "Herdr did not return a pane");
2529
+ if (typeof terminalId !== "string" || !terminalId)
2530
+ error("start", `pane ${paneId} did not include terminal identity`);
2518
2531
  attempt = {
2519
2532
  herdrAgent: alias(workspaceId, options.label, options.runId),
2520
2533
  workspaceId,
2521
2534
  tabId: tab.tab_id,
2522
2535
  paneId,
2536
+ terminalId,
2523
2537
  cwd,
2524
- createdTab
2538
+ createdTab,
2539
+ launchMayHaveStarted: false
2525
2540
  };
2526
2541
  let started;
2527
2542
  stage = "pane_readiness";
2528
- let shell;
2529
2543
  try {
2530
- shell = await proveShellReady(
2544
+ await waitForShellMarker(
2531
2545
  pi,
2532
2546
  ctx,
2533
2547
  paneId,
2534
- void 0,
2535
2548
  startupDeadline,
2536
2549
  "start",
2537
2550
  options.signal
2538
2551
  );
2552
+ attempt.shellProcess = await paneProcess(
2553
+ pi,
2554
+ ctx,
2555
+ paneId,
2556
+ options.signal,
2557
+ startupDeadline,
2558
+ true
2559
+ );
2539
2560
  } catch (failure) {
2540
2561
  if (failure instanceof OperationError)
2541
2562
  throw withStartupDiagnostic(
@@ -2551,7 +2572,6 @@ async function startHerdrAgent(pi, ctx, options) {
2551
2572
  throw failure;
2552
2573
  }
2553
2574
  stage = "ownership_capture";
2554
- attempt.shellProcess = shell;
2555
2575
  const currentPanes = (await runHerdr(pi, ctx, ["pane", "list", "--workspace", workspaceId], {
2556
2576
  signal: options.signal,
2557
2577
  timeout: startupCallTimeout(startupDeadline)
@@ -2559,8 +2579,8 @@ async function startHerdrAgent(pi, ctx, options) {
2559
2579
  const currentPane = currentPanes.find(
2560
2580
  (item) => item.pane_id === paneId
2561
2581
  );
2562
- if (!currentPane || currentPane.workspace_id !== workspaceId || currentPane.tab_id !== tab.tab_id)
2563
- error("start", `pane ${paneId} topology changed before launch`);
2582
+ if (!matchesAttemptPane(currentPane, attempt))
2583
+ error("start", `pane ${paneId} identity changed before launch`);
2564
2584
  if (createdTab) {
2565
2585
  const tabPanes = currentPanes.filter(
2566
2586
  (item) => item.tab_id === tab.tab_id
@@ -2568,67 +2588,81 @@ async function startHerdrAgent(pi, ctx, options) {
2568
2588
  if (tabPanes.length !== 1 || tabPanes[0]?.pane_id !== paneId)
2569
2589
  error("start", `tab ${tab.tab_id} topology changed before launch`);
2570
2590
  }
2571
- if (!sameCwd(currentPane.cwd, cwd))
2572
- error("start", `pane ${paneId} cwd does not match requested cwd`, {
2573
- expected_cwd: cwd,
2574
- observed_cwd: typeof currentPane.cwd === "string" ? currentPane.cwd : null
2575
- });
2576
2591
  stage = "agent_start";
2577
- const latest = await paneProcess(
2578
- pi,
2579
- ctx,
2580
- paneId,
2581
- options.signal,
2582
- startupDeadline,
2583
- true
2592
+ const retryDeadline = Math.min(
2593
+ Date.now() + FRESH_PANE_BUSY_RETRY_TIMEOUT,
2594
+ startupDeadline - START_DIAGNOSTIC_TIMEOUT
2584
2595
  );
2585
- if (!latest || latest.pane_id !== paneId || !sameShellProcessOwner(shell, latest))
2586
- error("start", `pane ${paneId} shell identity changed before launch`);
2587
- const remaining = startupCallTimeout(startupDeadline, childTimeout);
2588
- if (remaining < HERDR_START_TIMEOUT_MIN)
2589
- error("start", "startup deadline exhausted before agent start");
2590
- try {
2591
- started = await runHerdr(
2592
- pi,
2593
- ctx,
2594
- [
2595
- "agent",
2596
- "start",
2597
- attempt.herdrAgent,
2598
- "--kind",
2599
- "pi",
2600
- "--pane",
2601
- paneId,
2602
- "--timeout",
2603
- String(Math.min(remaining, childTimeout)),
2604
- "--",
2605
- ...options.extensionPath ? ["--extension", options.extensionPath] : [],
2606
- "--extension",
2607
- HERDR_AGENT_STATE_EXTENSION,
2608
- ...options.agentArgs ?? []
2609
- ],
2610
- {
2611
- signal: options.signal,
2612
- // Outlive Herdr's own readiness deadline by the diagnostic window so
2613
- // its structured failure is the one reported. With the identical
2614
- // value this command's kill always won the race, and "Herdr command
2615
- // was killed" replaced Herdr's reason.
2616
- timeout: Math.min(remaining, childTimeout) + START_DIAGNOSTIC_TIMEOUT
2617
- }
2618
- );
2619
- } catch (failure) {
2620
- if (isUnstructuredResultFailure(failure))
2621
- throw withStartupDiagnostic(
2622
- failure,
2623
- await captureStartupDiagnostic(
2624
- pi,
2625
- ctx,
2596
+ for (; ; ) {
2597
+ const remaining = startupCallTimeout(startupDeadline, childTimeout);
2598
+ if (remaining < HERDR_START_TIMEOUT_MIN)
2599
+ error("start", "startup deadline exhausted before agent start");
2600
+ try {
2601
+ started = await runHerdr(
2602
+ pi,
2603
+ ctx,
2604
+ [
2605
+ "agent",
2606
+ "start",
2607
+ attempt.herdrAgent,
2608
+ "--kind",
2609
+ "pi",
2610
+ "--pane",
2626
2611
  paneId,
2627
- startupDeadline,
2628
- options.signal
2629
- )
2612
+ "--timeout",
2613
+ String(Math.min(remaining, childTimeout)),
2614
+ "--",
2615
+ ...options.extensionPath ? ["--extension", options.extensionPath] : [],
2616
+ "--extension",
2617
+ HERDR_AGENT_STATE_EXTENSION,
2618
+ ...options.agentArgs ?? []
2619
+ ],
2620
+ {
2621
+ signal: options.signal,
2622
+ // Outlive Herdr's own readiness deadline by the diagnostic window so
2623
+ // its structured failure is the one reported. With the identical
2624
+ // value this command's kill always won the race, and "Herdr command
2625
+ // was killed" replaced Herdr's reason.
2626
+ timeout: Math.min(remaining, childTimeout) + START_DIAGNOSTIC_TIMEOUT
2627
+ }
2630
2628
  );
2631
- throw failure;
2629
+ attempt.launchMayHaveStarted = true;
2630
+ break;
2631
+ } catch (failure) {
2632
+ const busy = failure instanceof OperationError && failure.detail.details?.herdrCode === "agent_pane_busy";
2633
+ if (busy && Date.now() < retryDeadline) {
2634
+ retryAttempted = true;
2635
+ const pane = (await runHerdr(pi, ctx, ["pane", "get", paneId], {
2636
+ signal: options.signal,
2637
+ timeout: startupCallTimeout(startupDeadline)
2638
+ })).pane;
2639
+ if (!matchesAttemptPane(pane, attempt))
2640
+ error(
2641
+ "start",
2642
+ `pane ${paneId} identity changed during startup retry`
2643
+ );
2644
+ const remainingRetry = retryDeadline - Date.now();
2645
+ if (remainingRetry > 0)
2646
+ await sleep(Math.min(POLL_INTERVAL, remainingRetry), void 0, {
2647
+ signal: options.signal
2648
+ });
2649
+ if (Date.now() >= retryDeadline) throw failure;
2650
+ continue;
2651
+ }
2652
+ if (!busy) attempt.launchMayHaveStarted = true;
2653
+ if (isUnstructuredResultFailure(failure))
2654
+ throw withStartupDiagnostic(
2655
+ failure,
2656
+ await captureStartupDiagnostic(
2657
+ pi,
2658
+ ctx,
2659
+ paneId,
2660
+ startupDeadline,
2661
+ options.signal
2662
+ )
2663
+ );
2664
+ throw failure;
2665
+ }
2632
2666
  }
2633
2667
  stage = "agent_result";
2634
2668
  const agent = started.agent;
@@ -2640,7 +2674,8 @@ async function startHerdrAgent(pi, ctx, options) {
2640
2674
  attempt.agent = agent;
2641
2675
  return attempt;
2642
2676
  } catch (cause) {
2643
- if (attempt) throw new HerdrStartFailure(cause, stage, attempt, false);
2677
+ if (attempt)
2678
+ throw new HerdrStartFailure(cause, stage, attempt, retryAttempted);
2644
2679
  throw cause;
2645
2680
  } finally {
2646
2681
  release();
@@ -2718,7 +2753,7 @@ function normalizePaneProcess(value2, paneId, required) {
2718
2753
  ...foreground !== void 0 ? { foreground_processes: Object.freeze(foreground) } : {}
2719
2754
  });
2720
2755
  }
2721
- async function proveShellReady(pi, ctx, paneId, expected, deadline, operation, signal) {
2756
+ async function waitForShellMarker(pi, ctx, paneId, deadline, operation, signal) {
2722
2757
  const timeout = () => {
2723
2758
  if (operation === "start") return startupCallTimeout(deadline);
2724
2759
  const remaining = deadline - Date.now();
@@ -2751,11 +2786,17 @@ async function proveShellReady(pi, ctx, paneId, expected, deadline, operation, s
2751
2786
  noResult: true
2752
2787
  }
2753
2788
  );
2789
+ }
2790
+ async function proveShellReady(pi, ctx, paneId, expected, deadline, operation, signal) {
2791
+ await waitForShellMarker(pi, ctx, paneId, deadline, operation, signal);
2792
+ const timeout = operation === "start" ? startupCallTimeout(deadline) : deadline - Date.now();
2793
+ if (timeout <= 0)
2794
+ error(operation, `pane ${paneId} shell readiness deadline exhausted`);
2754
2795
  const result = await runHerdr(
2755
2796
  pi,
2756
2797
  ctx,
2757
2798
  ["pane", "process-info", "--pane", paneId],
2758
- { signal, timeout: timeout() }
2799
+ { signal, timeout }
2759
2800
  );
2760
2801
  const value2 = result?.process_info;
2761
2802
  const shell = normalizePaneProcess(value2, paneId, true);
@@ -3026,7 +3067,7 @@ async function rollbackHerdrStart(pi, ctx, started, signal) {
3026
3067
  const release = await lockLifecycle(ctx, signal);
3027
3068
  try {
3028
3069
  const expectedShell = started.shellProcess;
3029
- if (!expectedShell)
3070
+ if (started.launchMayHaveStarted && !expectedShell)
3030
3071
  error("rollback", `pane ${started.paneId} process ownership is unproven`);
3031
3072
  const listedAgents2 = await runHerdr(pi, ctx, ["agent", "list"], {
3032
3073
  signal
@@ -3042,7 +3083,9 @@ async function rollbackHerdrStart(pi, ctx, started, signal) {
3042
3083
  );
3043
3084
  if (replacement)
3044
3085
  error("rollback", `pane ${started.paneId} has a replacement agent`);
3045
- if (managed) {
3086
+ if (!started.launchMayHaveStarted && managed)
3087
+ error("rollback", `pane ${started.paneId} agent ownership changed`);
3088
+ if (started.launchMayHaveStarted && managed) {
3046
3089
  const observedSession = sessionIdentity(managed.agent_session);
3047
3090
  const sessionReference = started.sessionReference ?? (observedSession?.kind === "id" ? { id: observedSession.value } : observedSession?.kind === "path" ? { path: observedSession.value } : void 0);
3048
3091
  const initial = await proveExactRunningAgent(
@@ -3128,11 +3171,16 @@ async function rollbackHerdrStart(pi, ctx, started, signal) {
3128
3171
  error("rollback", `tab ${started.tabId} pane ownership is unproven`);
3129
3172
  }
3130
3173
  const pane = (await runHerdr(pi, ctx, ["pane", "get", started.paneId], { signal })).pane;
3131
- if (!pane || pane.pane_id !== started.paneId || pane.workspace_id !== started.workspaceId || pane.tab_id !== started.tabId)
3174
+ if (!matchesAttemptTerminal(pane, started))
3132
3175
  error("rollback", `pane ${started.paneId} ownership is unproven`);
3133
- const observed = await paneProcess(pi, ctx, started.paneId, signal);
3134
- if (!observed || !sameShellProcessOwner(expectedShell, observed))
3135
- error("rollback", `pane ${started.paneId} process ownership is unproven`);
3176
+ if (started.launchMayHaveStarted || !sameCwd(pane.cwd, started.cwd)) {
3177
+ const observed = await paneProcess(pi, ctx, started.paneId, signal);
3178
+ if (!expectedShell || !observed || !sameShellProcessOwner(expectedShell, observed))
3179
+ error(
3180
+ "rollback",
3181
+ `pane ${started.paneId} process ownership is unproven`
3182
+ );
3183
+ }
3136
3184
  if (!managed) {
3137
3185
  const listedAgents3 = await runHerdr(pi, ctx, ["agent", "list"], {
3138
3186
  signal