pi-herdr-agents 0.1.0 → 0.1.2

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/README.md CHANGED
@@ -78,12 +78,6 @@ pi
78
78
 
79
79
  herdr is the only supported terminal environment. The extension requires `HERDR_ENV=1` and the `herdr` CLI to be available.
80
80
 
81
- If your shell startup is slow and subagent commands sometimes get dropped before the prompt is ready, set `PI_SUBAGENT_SHELL_READY_DELAY_MS` to a higher value (defaults to `500`):
82
-
83
- ```bash
84
- export PI_SUBAGENT_SHELL_READY_DELAY_MS=2500
85
- ```
86
-
87
81
  ### Troubleshooting completion delivery
88
82
 
89
83
  If a child finishes but the parent returns an empty or unrelated response, first verify that the result reached the parent session:
@@ -429,7 +423,7 @@ herdr_workflow({ action: "cancel", runId: "run-1" });
429
423
 
430
424
  - Cancel claims a process-global terminal gate. Completion, failure, interruption, and cancellation cannot each produce a terminal outcome.
431
425
  - Queued `agent()` calls resolve as cancelled; no later reviewer or synthesizer starts.
432
- - Active panes are queried through Herdr process-info before close so foreground process identities can be waited on.
426
+ - New panes are queried through Herdr process-info until their interactive shell is ready before launch. Active panes are queried again before close so foreground process identities can be waited on.
433
427
  - After synchronous pane close, cancel waits for pane absence and captured process exit before disposing the reader checkout.
434
428
  - If process identity cannot be captured for an active pane, the pane remains present after close, or any captured process still lives after the bounded wait, the checkout is retained and the run ends `failed` with `cancel_termination_failed`. Successful cancellation is not reported in that case.
435
429
  - A successful cancel writes one `cancelled` terminal journal event and one result-free delivery. Repeated cancel is idempotent and returns the authoritative terminal outcome (including a prior fail-closed result).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-herdr-agents",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Asynchronous Pi subagents and approved review workflows in Herdr, with optional isolated Git worktrees",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -49,9 +49,9 @@
49
49
  ]
50
50
  },
51
51
  "devDependencies": {
52
- "@earendil-works/pi-ai": "^0.83.0",
53
- "@earendil-works/pi-coding-agent": "^0.83.0",
54
- "@earendil-works/pi-tui": "^0.83.0",
52
+ "@earendil-works/pi-ai": "^0.84.0",
53
+ "@earendil-works/pi-coding-agent": "^0.84.0",
54
+ "@earendil-works/pi-tui": "^0.84.0",
55
55
  "@sinclair/typebox": "^0.34.52",
56
56
  "oxlint": "^1.73.0"
57
57
  }
@@ -129,6 +129,10 @@ function getHerdrParentPaneId(): string {
129
129
  return paneId;
130
130
  }
131
131
 
132
+ function buildCurrentPaneArgs(): string[] {
133
+ return ["pane", "current", "--current"];
134
+ }
135
+
132
136
  function getHerdrCurrentPaneInfo(): {
133
137
  pane_id: string;
134
138
  tab_id: string;
@@ -141,7 +145,7 @@ function getHerdrCurrentPaneInfo(): {
141
145
  // Fall back to `herdr pane current` if any identity env var is missing —
142
146
  // older herdr versions may not set all three.
143
147
  if (!paneId || !tabId || !workspaceId) {
144
- const output = herdrExec(["pane", "current"]);
148
+ const output = herdrExec(buildCurrentPaneArgs());
145
149
  const parsed = parseHerdrJson(output);
146
150
  const pane = (parsed as { result?: { pane?: unknown } } | null)?.result
147
151
  ?.pane as
@@ -449,6 +453,46 @@ export function getHerdrPaneProcessInfo(surface: string): HerdrPaneProcessInfo {
449
453
  );
450
454
  }
451
455
 
456
+ async function getHerdrPaneProcessInfoAsync(
457
+ surface: string,
458
+ ): Promise<HerdrPaneProcessInfo> {
459
+ return parsePaneProcessInfo(
460
+ await herdrExecAsync(["pane", "process-info", "--pane", surface]),
461
+ surface,
462
+ );
463
+ }
464
+
465
+ function isHerdrShellReady(info: HerdrPaneProcessInfo): boolean {
466
+ return (
467
+ info.shellPid != null &&
468
+ info.foregroundProcessGroupId === info.shellPid
469
+ );
470
+ }
471
+
472
+ export async function waitForHerdrShellReady(
473
+ surface: string,
474
+ options: { timeoutMs?: number; intervalMs?: number; signal?: AbortSignal } = {},
475
+ ): Promise<void> {
476
+ const timeoutMs = options.timeoutMs ?? 10_000;
477
+ const intervalMs = options.intervalMs ?? 50;
478
+ const deadline = Date.now() + timeoutMs;
479
+ let lastError = "no interactive shell foreground process";
480
+
481
+ while (Date.now() <= deadline) {
482
+ if (options.signal?.aborted) throw new Error("Shell readiness wait cancelled.");
483
+ try {
484
+ if (isHerdrShellReady(await getHerdrPaneProcessInfoAsync(surface))) return;
485
+ } catch (error) {
486
+ lastError = error instanceof Error ? error.message : String(error);
487
+ }
488
+ if (Date.now() >= deadline) break;
489
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
490
+ }
491
+ throw new Error(
492
+ `Timed out waiting for interactive shell in Herdr pane ${surface}: ${lastError}`,
493
+ );
494
+ }
495
+
452
496
  export function isProcessAlive(pid: number): boolean {
453
497
  try {
454
498
  process.kill(pid, 0);
@@ -529,6 +573,7 @@ export function renameHerdrWorkspace(title: string): void {
529
573
  }
530
574
 
531
575
  export const __herdrTest__ = {
576
+ buildCurrentPaneArgs,
532
577
  buildTabCreateArgs,
533
578
  buildWorktreeCreateArgs,
534
579
  parseHerdrJson,
@@ -538,4 +583,5 @@ export const __herdrTest__ = {
538
583
  parsePaneGetOutput,
539
584
  parsePaneGetError,
540
585
  parsePaneProcessInfo,
586
+ isHerdrShellReady,
541
587
  };
@@ -39,6 +39,7 @@ import {
39
39
  readPaneAsync,
40
40
  inspectPane,
41
41
  getPaneProcessInfo,
42
+ waitForShellReady,
42
43
  waitForPaneAbsence,
43
44
  waitForProcessesExit,
44
45
  } from "./terminal.ts";
@@ -803,19 +804,6 @@ function formatElapsed(seconds: number): string {
803
804
  return `${m}m ${s}s`;
804
805
  }
805
806
 
806
- /**
807
- * Wait long enough for a freshly created pane to finish shell startup.
808
- *
809
- * Some environments do extra shell-init work before the prompt is ready
810
- * (for example direnv/devenv), so the delay is configurable for users who hit
811
- * dropped commands. Keep the historical default at 500ms.
812
- */
813
- function getShellReadyDelayMs(): number {
814
- const raw = process.env.PI_SUBAGENT_SHELL_READY_DELAY_MS?.trim();
815
- const parsed = raw ? Number.parseInt(raw, 10) : Number.NaN;
816
- return Number.isFinite(parsed) && parsed >= 0 ? parsed : 500;
817
- }
818
-
819
807
  function muxUnavailableResult() {
820
808
  return {
821
809
  content: [
@@ -1984,7 +1972,6 @@ function buildWorkflowChildCommand(params: {
1984
1972
 
1985
1973
  export const __test__ = {
1986
1974
  borderLine,
1987
- getShellReadyDelayMs,
1988
1975
  renderSubagentWidgetLines,
1989
1976
  loadAgentDefaults,
1990
1977
  discoverAgentDefinitions,
@@ -2218,13 +2205,8 @@ async function launchSubagent(
2218
2205
  });
2219
2206
  }
2220
2207
 
2221
- // Use pre-created surface (parallel mode) or create a new one.
2222
- // For new surfaces, pause briefly so the shell is ready before sending the command.
2223
- if (!options?.surface) {
2224
- await new Promise<void>((resolve) =>
2225
- setTimeout(resolve, getShellReadyDelayMs()),
2226
- );
2227
- }
2208
+ // `pane run` is safe only after the shell owns the foreground process group.
2209
+ await waitForShellReady(surface);
2228
2210
 
2229
2211
  const launchBehavior = resolveLaunchBehavior(params, agentDefs);
2230
2212
 
@@ -3009,9 +2991,7 @@ export default function subagentsExtension(pi: ExtensionAPI) {
3009
2991
  mkdirSync(dirname(sessionFile), { recursive: true });
3010
2992
  surface = createSubagentPane(`${candidate.runId}: ${roleName}`);
3011
2993
  owner.children.set(id, { controller: childController, surface });
3012
- await new Promise<void>((done) =>
3013
- setTimeout(done, getShellReadyDelayMs()),
3014
- );
2994
+ await waitForShellReady(surface, { signal: childController.signal });
3015
2995
  if (childController.signal.aborted)
3016
2996
  return workflowFailure("cancelled", "Workflow cancelled.");
3017
2997
  const command = buildWorkflowChildCommand({
@@ -4182,9 +4162,7 @@ export default function subagentsExtension(pi: ExtensionAPI) {
4182
4162
  const entryCountBefore = getNewEntries(params.sessionPath, 0).length;
4183
4163
 
4184
4164
  const surface = createSubagentPane(name);
4185
- await new Promise<void>((resolve) =>
4186
- setTimeout(resolve, getShellReadyDelayMs()),
4187
- );
4165
+ await waitForShellReady(surface);
4188
4166
 
4189
4167
  // Build pi resume command
4190
4168
  const parts = ["pi", "--session", shellQuote(params.sessionPath)];
@@ -4418,9 +4396,7 @@ export default function subagentsExtension(pi: ExtensionAPI) {
4418
4396
 
4419
4397
  sessionFile = createBtwSessionSnapshot(parentSessionFile, leafId);
4420
4398
  surface = createSubagentPane("BTW");
4421
- await new Promise<void>((resolve) =>
4422
- setTimeout(resolve, getShellReadyDelayMs()),
4423
- );
4399
+ await waitForShellReady(surface);
4424
4400
 
4425
4401
  const artifactDir = getArtifactDir(
4426
4402
  ctx.sessionManager.getSessionDir(),
@@ -7,6 +7,7 @@ import {
7
7
  createHerdrSurfaceSplit,
8
8
  createHerdrWorktree,
9
9
  getHerdrPaneProcessInfo,
10
+ waitForHerdrShellReady,
10
11
  isHerdrAvailable,
11
12
  isProcessAlive,
12
13
  readHerdrScreen,
@@ -152,6 +153,14 @@ export function getPaneProcessInfo(paneId: PaneId): HerdrPaneProcessInfo {
152
153
  return getHerdrPaneProcessInfo(paneId);
153
154
  }
154
155
 
156
+ export async function waitForShellReady(
157
+ paneId: PaneId,
158
+ options?: { timeoutMs?: number; intervalMs?: number; signal?: AbortSignal },
159
+ ): Promise<void> {
160
+ assertTerminalAvailable();
161
+ return waitForHerdrShellReady(paneId, options);
162
+ }
163
+
155
164
  export async function waitForPaneAbsence(
156
165
  paneId: PaneId,
157
166
  options?: { timeoutMs?: number; intervalMs?: number },