@hcflabs/pi-subagent 28.2.1 → 28.2.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.
@@ -86,6 +86,9 @@ export function exactDirectTerminalTurn(jsonl, prompt) {
86
86
  return reason === "stop" || reason === "error" || reason === "aborted";
87
87
  }
88
88
  const turnSuffix = (turn) => `\n\nTurn identity: ${turn}`;
89
+ /** How long to wait for Pi to retry a turn that ended with a provider error, and how many times. */
90
+ const PROVIDER_RETRY_GRACE_MS = 65_000;
91
+ const PROVIDER_RETRY_WAITS = 5;
89
92
  /** Refuse continuation unless the saved session's exact recorded turn is terminal and no later user turn exists. */
90
93
  export async function assertSettledDirectSession(sessionFile, turn) {
91
94
  const info = await stat(sessionFile).catch((error) => {
@@ -481,36 +484,65 @@ export function createDirectHerdr(pi, cwd, idleMs) {
481
484
  deadline = Date.now() + idleMs;
482
485
  }
483
486
  };
484
- while (current === "working" || current === "idle") {
485
- // A turn may finish before observation begins (or between waits).
486
- if (current === "idle") {
487
- const answer = await maybeAnswer();
488
- if (answer !== undefined)
489
- return answer;
490
- }
491
- await observeProgress();
492
- const remaining = deadline - Date.now();
493
- if (remaining <= 0)
494
- throw new Error(`Herdr agent in tab ${tabId} made no progress for ${idleMs}ms; inspect ${sessionFile} before retrying.`);
495
- const args = ["agent", "wait", name,
496
- ...(current === "idle" ? ["--until", "working"] : ["--until", "idle"]),
497
- "--until", "done", "--until", "blocked", "--until", "unknown",
498
- "--timeout", String(Math.max(1, Math.min(current === "idle" ? 1000 : OPERATION_MS - 1000, remaining)))];
487
+ let retryWaits = 0;
488
+ // A provider error, such as HTTP 429, ends a turn with stopReason "error", and Pi can then retry it.
489
+ // Wait a bounded time for the retry before the error counts as the final answer.
490
+ const retryPending = async () => {
491
+ if (retryWaits >= PROVIDER_RETRY_WAITS)
492
+ return false;
493
+ const info = await stat(sessionFile).catch(() => undefined);
494
+ if (!info || info.size > SESSION_LIMIT)
495
+ return false;
496
+ if (exactFinalTurn(await readFile(sessionFile, "utf8"), prompt)?.stopReason !== "error")
497
+ return false;
498
+ retryWaits += 1;
499
+ const args = ["agent", "wait", name, "--until", "working", "--timeout", String(PROVIDER_RETRY_GRACE_MS)];
499
500
  const waited = await herdr.exec(args, options(signal));
500
501
  if (waited.code !== 0 || waited.killed) {
501
- if (!waited.killed && hasHerdrErrorCode(waited, "timeout")) {
502
- continue;
503
- }
502
+ if (!waited.killed && hasHerdrErrorCode(waited, "timeout"))
503
+ return false;
504
504
  throw new Error(herdrCommandFailure(args, waited));
505
505
  }
506
- const next = inspect(JSON.parse(waited.stdout), "agent_info", name, paneId, tabId);
507
- if (next !== current)
508
- deadline = Date.now() + idleMs;
509
- current = next;
506
+ current = inspect(JSON.parse(waited.stdout), "agent_info", name, paneId, tabId);
507
+ deadline = Date.now() + idleMs;
508
+ return current === "working";
509
+ };
510
+ for (;;) {
511
+ while (current === "working" || current === "idle") {
512
+ // A turn may finish before observation begins (or between waits).
513
+ if (current === "idle") {
514
+ const answer = await maybeAnswer();
515
+ if (answer !== undefined)
516
+ return answer;
517
+ }
518
+ await observeProgress();
519
+ const remaining = deadline - Date.now();
520
+ if (remaining <= 0)
521
+ throw new Error(`Herdr agent in tab ${tabId} made no progress for ${idleMs}ms; inspect ${sessionFile} before retrying.`);
522
+ const args = ["agent", "wait", name,
523
+ ...(current === "idle" ? ["--until", "working"] : ["--until", "idle"]),
524
+ "--until", "done", "--until", "blocked", "--until", "unknown",
525
+ "--timeout", String(Math.max(1, Math.min(current === "idle" ? 1000 : OPERATION_MS - 1000, remaining)))];
526
+ const waited = await herdr.exec(args, options(signal));
527
+ if (waited.code !== 0 || waited.killed) {
528
+ if (!waited.killed && hasHerdrErrorCode(waited, "timeout")) {
529
+ continue;
530
+ }
531
+ throw new Error(herdrCommandFailure(args, waited));
532
+ }
533
+ const next = inspect(JSON.parse(waited.stdout), "agent_info", name, paneId, tabId);
534
+ if (next !== current)
535
+ deadline = Date.now() + idleMs;
536
+ current = next;
537
+ }
538
+ if (current !== "done")
539
+ throw new Error(`Herdr agent in tab ${tabId} became ${current}; inspect it before retrying.`);
540
+ const answer = await maybeAnswer();
541
+ if (answer !== undefined)
542
+ return answer;
543
+ if (!await retryPending())
544
+ return readAnswer();
510
545
  }
511
- if (current !== "done")
512
- throw new Error(`Herdr agent in tab ${tabId} became ${current}; inspect it before retrying.`);
513
- return readAnswer();
514
546
  },
515
547
  async focus() {
516
548
  // A reused pane ID must not receive focus: the agent identity must match the launch first.
@@ -143,6 +143,8 @@ If the extension cannot prove termination of the stopped worker, the workflow ke
143
143
 
144
144
  The extension retrieves each final answer from its exact Pi session file, not its possibly truncated screen. Idle, blocked, unknown, or malformed answers fail with recoverable tab and session identities. No whole-run deadline applies.
145
145
 
146
+ A provider error, such as HTTP 429, can end a worker turn before Pi retries it automatically. When the final turn has `stopReason: "error"`, the extension waits up to 65 seconds for the worker to work again, at most 5 times for one task. The task fails only when Pi does not retry in that time.
147
+
146
148
  The tasks in a request share a 40 KiB inline answer budget. Each worker prompt states its share. A longer exact answer is still a success. The result shows a UTF-8-safe prefix and a truncation marker. The marker gives the path of a private `answer-<turn>.md` file beside the session file, which holds the full answer. For a write task, the budget applies to the `answer` field of the completion object. A chain step receives the full answer of the previous step in `{previous}`; the budget applies only to the result for Main. The result details keep `answerFile` for each full-answer file. Recovery text includes these paths outside the output limit, even when checkout notes remove the inline link.
147
149
 
148
150
  Herdr accepts prompt text as one command argument. To stay below the operating system's argument limit, a task packet above 64 KiB uses a private, exclusively created `task-<turn>.md` file beside the session file. This packet includes the full chain context. The worker prompt requires the Role's `read` tool to read all parts with line offsets before work starts. Pi's native `read` can return a line of exactly 50 KiB (51,200 bytes), but not a longer line. If any packet line is longer, or `read` is absent, the worker uses the Role's `bash` tool. If a line is too long and `bash` is absent, the task fails before tab creation with the read limit and a request for `bash` or shorter lines. A Role with neither tool also fails before tab creation; the extension adds no tool. The task file stays available for recovery.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hcflabs/pi-subagent",
3
- "version": "28.2.1",
3
+ "version": "28.2.2",
4
4
  "description": "Delegate bounded direct work or durable checked isolated task graphs to Pi roles.",
5
5
  "keywords": [
6
6
  "pi-package",