@henryqw/pi-subagent 22.2.0 → 22.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.
@@ -1,5 +1,6 @@
1
1
  import { type ExecuteRequest, type HostAllocationIntent, type HostAllocationPlan, type TaskAttempt, type TaskRequest, type WorkspaceIdentity } from "./schema.ts";
2
2
  import { type AllocationReconciliation, type HostAllocationKind, type HostAllocationResult, type HostCleanupKind, type HostRuntime, type InFlightTaskCandidateInspector, type OperationContext, type TextTaskContext, type TransientLaunchHandle, type VerifiedLaunch, type WorkerResult } from "./runner.ts";
3
+ import { type EphemeralSubagentExecutionBudget } from "./ephemeral.ts";
3
4
  export interface HostProcessOptions {
4
5
  cwd: string;
5
6
  signal: AbortSignal;
@@ -21,6 +22,7 @@ export interface HerdrHostRuntimeOptions {
21
22
  env?: NodeJS.ProcessEnv;
22
23
  leaseDirectory?: string;
23
24
  lsofCommand?: string;
25
+ executionBudget?: () => Omit<EphemeralSubagentExecutionBudget, "startedAt">;
24
26
  }
25
27
  export declare function workspaceLabel(attempt: Pick<TaskAttempt, "correlationToken">): string;
26
28
  export declare class HerdrHostRuntime implements HostRuntime {
@@ -34,6 +36,7 @@ export declare class HerdrHostRuntime implements HostRuntime {
34
36
  private readonly env;
35
37
  private readonly leaseDirectory;
36
38
  private readonly lsofCommand;
39
+ private readonly executionBudget?;
37
40
  constructor(options: HerdrHostRuntimeOptions);
38
41
  /** Fail-closed Herdr caller/capability gate. This method creates no state or host resource. */
39
42
  preflightHost(input: {
@@ -8,6 +8,7 @@ import { createHerdrClient, hasHerdrErrorCode, herdrCommandFailure, startPiAgent
8
8
  import { isCleanCommitted, } from "./schema.js";
9
9
  import { buildChangesetTaskPrompt, withTransientLaunch, } from "./runner.js";
10
10
  import { runProcess as defaultRunProcess } from "./process.js";
11
+ import { EXECUTION_BUDGET_ENV } from "./ephemeral.js";
11
12
  const MIN_HERDR_VERSION = [0, 9, 0];
12
13
  const MIN_HERDR_PROTOCOL = 22;
13
14
  const HERDR_OPERATION_CAP_MS = 30_000;
@@ -151,9 +152,6 @@ function worktreeIntent(attempt) {
151
152
  export function workspaceLabel(attempt) {
152
153
  return attempt.correlationToken.slice(-6);
153
154
  }
154
- function legacyWorkspaceLabel(requestId, task, attempt) {
155
- return `${requestId}/${task.id}#${attempt.number}`;
156
- }
157
155
  function expectedWorkerLabel(task) {
158
156
  return `${task.role}/${task.modelClass}`;
159
157
  }
@@ -173,10 +171,9 @@ function requireIntentIdentity(intent, attempt) {
173
171
  throw new Error(`${intent.kind} allocation intent does not match the task attempt.`);
174
172
  }
175
173
  }
176
- function assertWorkspaceIntent(intent, requestId, task, attempt) {
174
+ function assertWorkspaceIntent(intent, attempt) {
177
175
  const worktree = worktreeIntent(attempt);
178
- const label = exactString(intent.label, "workspace label");
179
- if ((label !== workspaceLabel(attempt) && label !== legacyWorkspaceLabel(requestId, task, attempt))
176
+ if (exactString(intent.label, "workspace label") !== workspaceLabel(attempt)
180
177
  || exactAbsolutePath(intent.worktreeCwd, "workspace worktree cwd") !== worktree.cwd
181
178
  || exactAbsolutePath(intent.mainRoot, "workspace Main root") !== worktree.repoRoot) {
182
179
  throw new Error("Workspace allocation plan drifted from the exact owned worktree.");
@@ -252,6 +249,7 @@ export class HerdrHostRuntime {
252
249
  env;
253
250
  leaseDirectory;
254
251
  lsofCommand;
252
+ executionBudget;
255
253
  constructor(options) {
256
254
  this.inspectInFlightCandidate = options.inspectInFlightTaskCandidate;
257
255
  this.execute = options.runProcess ?? defaultRunProcess;
@@ -263,6 +261,7 @@ export class HerdrHostRuntime {
263
261
  this.env = options.env ?? process.env;
264
262
  this.leaseDirectory = resolve(options.leaseDirectory ?? join(extensionConfigDir("pi-subagent"), "leases"));
265
263
  this.lsofCommand = options.lsofCommand ?? "lsof";
264
+ this.executionBudget = options.executionBudget;
266
265
  }
267
266
  /** Fail-closed Herdr caller/capability gate. This method creates no state or host resource. */
268
267
  async preflightHost(input, context) {
@@ -365,7 +364,7 @@ export class HerdrHostRuntime {
365
364
  if (input.intent.kind === "workspace") {
366
365
  if (input.acquireLaunch)
367
366
  throw new Error("Role launch acquisition is valid only at the exact agent allocation boundary.");
368
- return await this.allocateWorkspace(input.intent, input.requestId, input.task, input.attempt, context);
367
+ return await this.allocateWorkspace(input.intent, input.attempt, context);
369
368
  }
370
369
  if (input.intent.kind === "worker_tab") {
371
370
  if (input.acquireLaunch)
@@ -378,7 +377,7 @@ export class HerdrHostRuntime {
378
377
  requireIntentIdentity(input.intent, input.attempt);
379
378
  const allocation = input.intent;
380
379
  if (allocation.kind === "workspace") {
381
- assertWorkspaceIntent(allocation, input.requestId, input.task, input.attempt);
380
+ assertWorkspaceIntent(allocation, input.attempt);
382
381
  await this.assertRepositoryIdentity(allocation, context);
383
382
  const response = await this.herdr.json(["worktree", "list", "--cwd", allocation.worktreeCwd], this.processOptions(allocation.herdrRepoRoot, context, HERDR_OPERATION_CAP_MS));
384
383
  const result = resultRecord(response, "Herdr worktree list response");
@@ -653,7 +652,7 @@ export class HerdrHostRuntime {
653
652
  assertWorkerTabIntent(allocation, input.task, input.attempt);
654
653
  const leasePath = exactString(allocation.leasePath, "saved worker lease path");
655
654
  this.assertLeasePath(leasePath, input.attempt.correlationToken);
656
- assertWorkspaceIntent(workspace, input.requestId, input.task, input.attempt);
655
+ assertWorkspaceIntent(workspace, input.attempt);
657
656
  await this.assertRepositoryIdentity(workspace, context);
658
657
  const tabId = exactString(allocation.tabId, "saved worker tab ID");
659
658
  const workspaceId = exactString(workspace.workspaceId, "saved workspace ID");
@@ -685,7 +684,7 @@ export class HerdrHostRuntime {
685
684
  return { outcome: "blocked", failure: "Workspace cleanup must follow worker-tab reconciliation." };
686
685
  const allocation = ownedIntent(input.attempt, "workspace");
687
686
  requireIntentIdentity(allocation, input.attempt);
688
- assertWorkspaceIntent(allocation, input.requestId, input.task, input.attempt);
687
+ assertWorkspaceIntent(allocation, input.attempt);
689
688
  await this.assertRepositoryIdentity(allocation, context);
690
689
  const workspaceId = exactString(allocation.workspaceId, "saved workspace ID");
691
690
  if (await this.workspaceAbsent(workspaceId, allocation, context))
@@ -705,8 +704,8 @@ export class HerdrHostRuntime {
705
704
  return { outcome: "blocked", failure: safeText(error) };
706
705
  }
707
706
  }
708
- async allocateWorkspace(allocation, requestId, task, attempt, context) {
709
- assertWorkspaceIntent(allocation, requestId, task, attempt);
707
+ async allocateWorkspace(allocation, attempt, context) {
708
+ assertWorkspaceIntent(allocation, attempt);
710
709
  await this.assertRepositoryIdentity(allocation, context);
711
710
  const args = ["worktree", "open", "--cwd", allocation.herdrRepoRoot, "--path", allocation.worktreeCwd, "--label", allocation.label, "--no-focus"];
712
711
  const response = await this.herdr.exec(args, this.processOptions(allocation.herdrRepoRoot, context, HERDR_OPERATION_CAP_MS));
@@ -741,9 +740,12 @@ export class HerdrHostRuntime {
741
740
  assertWorkerTabIntent(allocation, task, attempt);
742
741
  await this.createPrivateLease(allocation.leasePath, allocation.token);
743
742
  await this.waitForStableWorkspacePanes(allocation.workspaceId, allocation.worktreeCwd, context);
743
+ const budget = this.executionBudget?.();
744
744
  const args = [
745
745
  "tab", "create", "--workspace", allocation.workspaceId, "--cwd", allocation.worktreeCwd,
746
- "--label", allocation.label, "--env", `${PROCESS_LEASE_ENV}=${allocation.leasePath}`, "--no-focus",
746
+ "--label", allocation.label, "--env", `${PROCESS_LEASE_ENV}=${allocation.leasePath}`,
747
+ ...(budget ? ["--env", `${EXECUTION_BUDGET_ENV}=${JSON.stringify({ ...budget, startedAt: this.now() })}`] : []),
748
+ "--no-focus",
747
749
  ];
748
750
  const response = await this.herdr.exec(args, this.processOptions(allocation.worktreeCwd, context, HERDR_OPERATION_CAP_MS));
749
751
  if (response.code !== 0 || response.killed) {
@@ -783,8 +785,9 @@ export class HerdrHostRuntime {
783
785
  return await withTransientLaunch(handle, async (launch) => {
784
786
  if (launch.role !== expectedRole)
785
787
  throw new Error("Agent start acquisition returned the wrong Role.");
786
- if (Object.keys(launch.env).length)
788
+ if (Object.keys(launch.env).some((name) => name !== EXECUTION_BUDGET_ENV || !this.executionBudget)) {
787
789
  throw new Error("Herdr agent launch must not receive caller Role environment variables.");
790
+ }
788
791
  const response = await startPiAgent(this.herdr, {
789
792
  name: allocation.agentName,
790
793
  pane: allocation.paneId,
@@ -827,6 +830,15 @@ export class HerdrHostRuntime {
827
830
  if (lifecycle.status === "unknown") {
828
831
  return { outcome: "unknown", diagnostic: "Delivered prompt lifecycle is unknown." };
829
832
  }
833
+ if (lifecycle.status === "working") {
834
+ try {
835
+ await this.delay(STALLED_PROMPT_POLL_MS, context.signal);
836
+ }
837
+ catch (error) {
838
+ return { outcome: this.contextInterrupted(context) ? "interrupted" : "unknown", diagnostic: `Delivered prompt polling failed: ${safeText(error)}` };
839
+ }
840
+ continue;
841
+ }
830
842
  let inspection;
831
843
  try {
832
844
  inspection = await this.inspectInFlightCandidate({ root: allocation.worktreeCwd, task: input.task, attempt: input.attempt }, this.childContext(context, GIT_INSPECTION_CAP_MS));
@@ -45,12 +45,15 @@ async function sharedHistory(base, tip, cwd, signal) {
45
45
  /** Check helper-derived names even after a safely removed checkout is no longer present. */
46
46
  async function ownershipMetadata(root, info, signal) {
47
47
  const canonical = await realpath(root);
48
- const first = (await requireGit(["worktree", "list", "--porcelain", "-z"], root, signal)).split("\0")[0];
49
- const primary = first.startsWith("worktree ") ? await realpath(first.slice("worktree ".length)) : "";
48
+ const listed = await requireGit(["worktree", "list", "--porcelain", "-z"], root, signal);
49
+ const primary = listed.split("\0", 1)[0];
50
+ if (!primary?.startsWith("worktree "))
51
+ throw new Error("Primary Git worktree is unavailable.");
52
+ const repoRoot = await realpath(primary.slice("worktree ".length));
50
53
  if (await realpath(await requireGit(["rev-parse", "--show-toplevel"], root, signal)) !== canonical
51
- || info.repoRoot !== primary || info.cwd !== info.path
54
+ || info.repoRoot !== repoRoot || info.cwd !== info.path
52
55
  || !/^pi-subagent\/subagent-[0-9a-f]{24}$/.test(info.branch)
53
- || info.path !== join(primary, ".worktrees", basename(info.branch))) {
56
+ || info.path !== join(repoRoot, ".worktrees", basename(info.branch))) {
54
57
  throw new Error("Git worktree is not an exact owned checkout of Main.");
55
58
  }
56
59
  }
package/dist/runner.js CHANGED
@@ -940,6 +940,7 @@ export class IsolatedRunner {
940
940
  if (generation && generation.status !== "staging" && generation.status !== "conflict")
941
941
  throw new Error("Stage action has a stale generation.");
942
942
  const previousStage = generation?.stages.at(-1);
943
+ const pendingAllocation = action.action === "resolve" && previousStage?.status === "pending";
943
944
  const expected = previousStage?.status === "staged"
944
945
  ? previousStage.tip : previousStage?.onto ?? generation?.integrationBase;
945
946
  if (action.action === "stage" && generation?.stages.some((stage) => stage.status !== "staged")) {
@@ -974,6 +975,18 @@ export class IsolatedRunner {
974
975
  const actualMain = await scope.call((context) => this.gitRuntime.inspectMain({ root }, context));
975
976
  if (!sameIdentity(actualMain, state.main))
976
977
  throw new Error("Main changed or became dirty; staging is blocked.");
978
+ if (pendingAllocation) {
979
+ if (!generation?.worktree || generation.status !== "staging" || generation.stages.length !== 1
980
+ || !sameIdentity(action.expectedTip, generation.integrationBase)) {
981
+ throw new Error("Pending integration allocation is not an exact initial stage.");
982
+ }
983
+ const checkout = generation.worktree;
984
+ const base = await scope.call((context) => this.integrationGit.inspectCombined(root, checkout, state.main, [], context.signal));
985
+ if (!sameIdentity(base, generation.integrationBase))
986
+ throw new Error("Pending integration checkout is not the clean recorded base.");
987
+ previousStage.status = "staging";
988
+ await this.saveProductive(handle); // Explicit resolve proves allocation; merge intent precedes the first merge.
989
+ }
977
990
  if (!generation) {
978
991
  if (action.action !== "stage")
979
992
  throw new Error("No integration generation exists to resolve.");
@@ -1018,15 +1031,9 @@ export class IsolatedRunner {
1018
1031
  }
1019
1032
  await this.saveProductive(handle); // Exact candidate and previous tip before merge.
1020
1033
  }
1021
- let outcome = await scope.call((context) => action.action === "stage"
1034
+ const outcome = await scope.call((context) => action.action === "stage" || pendingAllocation
1022
1035
  ? this.integrationGit.stage(root, worktree, state.main, receipts, worker, candidate.tip, context.signal)
1023
1036
  : this.integrationGit.reconcileStage(root, worktree, state.main, receipts, worker, candidate.tip, context.signal));
1024
- if (action.action === "resolve" && stage.status === "pending"
1025
- && outcome.outcome === "ready" && outcome.value === "not_started") {
1026
- stage.status = "staging";
1027
- await this.saveProductive(handle); // Read-only proof before Main's explicit merge request.
1028
- outcome = await scope.call((context) => this.integrationGit.stage(root, worktree, state.main, receipts, worker, candidate.tip, context.signal));
1029
- }
1030
1037
  if (outcome.outcome === "ready" && outcome.value !== "not_started") {
1031
1038
  stage.status = "staged";
1032
1039
  stage.tip = outcome.value.tip;
@@ -46,7 +46,7 @@ export declare function inspectIndexFlags(cwd: string, run?: GitRunner, signal?:
46
46
  hidden: boolean;
47
47
  failure?: string;
48
48
  }>;
49
- /** Proves a worktree has no tracked, untracked, ignored, index-hidden, or nested submodule work. */
49
+ /** Proves a worktree has no tracked, untracked, index-hidden, or nested submodule work; cleanup also checks ignored files. */
50
50
  export declare function inspectWorktreeDirty(cwd: string, run?: GitRunner, includeIgnored?: boolean): Promise<WorktreeDirtyInspection>;
51
51
  /**
52
52
  * Inspects and possibly prunes a child worktree after it finishes. Commit count
package/dist/worktree.js CHANGED
@@ -187,7 +187,7 @@ async function inspectDirty(run, cwd, includeIgnored = true) {
187
187
  return { dirty: false, failure: "assume-unchanged or skip-worktree index entries remain" };
188
188
  return { dirty: false };
189
189
  }
190
- /** Proves a worktree has no tracked, untracked, ignored, index-hidden, or nested submodule work. */
190
+ /** Proves a worktree has no tracked, untracked, index-hidden, or nested submodule work; cleanup also checks ignored files. */
191
191
  export async function inspectWorktreeDirty(cwd, run = runGit, includeIgnored = true) {
192
192
  const root = await inspectDirty(run, cwd, includeIgnored);
193
193
  if (root.dirty || root.failure)
@@ -232,7 +232,7 @@ export async function finalizeChildWorktree(info, run = runGit) {
232
232
  const countFailure = counted.code !== 0
233
233
  ? `rev-list exit ${counted.code}: ${counted.stderr.trim().slice(0, 200)}`
234
234
  : Number.isNaN(commits) ? "rev-list produced non-numeric output" : undefined;
235
- const inspection = checkoutExists ? await inspectDirty(run, info.path) : undefined;
235
+ const inspection = checkoutExists ? await inspectDirty(run, info.path, true) : undefined;
236
236
  const measurements = {
237
237
  ...(countFailure === undefined ? { commits } : {}),
238
238
  ...(inspection?.failure === undefined && inspection !== undefined ? { dirty: inspection.dirty } : {}),
@@ -65,18 +65,19 @@ export const createIsolatedComponents: CreateIsolatedComponents = ({
65
65
  : await directRunProcess(command, args, options);
66
66
  const resolveRoot = createCanonicalGitRootResolver({ runProcess });
67
67
  const git = new CheckedGitRuntime({ runProcess, executeReview: createExactJudgmentExecutor(executor) });
68
- const host = new HerdrHostRuntime({ inspectInFlightTaskCandidate: git.inspectInFlightTaskCandidate.bind(git), runProcess });
68
+ const executionBudget = () => ({
69
+ maxTurns: policy.maxTurns,
70
+ maxMs: null,
71
+ ...(policy.maxTokens === undefined ? {} : { maxTokens: policy.maxTokens }),
72
+ });
73
+ const host = new HerdrHostRuntime({ inspectInFlightTaskCandidate: git.inspectInFlightTaskCandidate.bind(git), runProcess, executionBudget });
69
74
  const coordinator = new RoleLaunchRuntime({
70
75
  pi,
71
76
  context,
72
77
  resolveRoot,
73
78
  preflightHost: async ({ root }, operation) => await host.preflightHost({ root }, operation),
74
79
  inspectMain: async (input, operation) => await git.inspectMain(input, operation),
75
- executionBudget: () => ({
76
- maxTurns: policy.maxTurns,
77
- maxMs: null,
78
- ...(policy.maxTokens === undefined ? {} : { maxTokens: policy.maxTokens }),
79
- }),
80
+ executionBudget,
80
81
  });
81
82
  return {
82
83
  resolveRoot,
@@ -3,7 +3,7 @@ import type { WorkflowEntry, WorkflowMode } from "./workflow.ts";
3
3
 
4
4
  const EVIDENCE_PREVIEW_CODE_POINTS = 256;
5
5
 
6
- export type WorkflowTransportStatus = "pending" | "running" | "succeeded" | "failed" | "rejected" | "skipped";
6
+ export type WorkflowTransportStatus = "pending" | "running" | "succeeded" | "rejected" | "skipped";
7
7
 
8
8
  type TransportEntryBase = {
9
9
  id: WorkflowEntry["id"];
@@ -17,7 +17,7 @@ type TransportEntryBase = {
17
17
  export type WorkflowTransportEntry =
18
18
  | TransportEntryBase & { status: "pending" | "skipped"; assistantOutput?: never; failure?: never }
19
19
  | TransportEntryBase & { status: "running" | "succeeded"; assistantOutput: string; failure?: never }
20
- | TransportEntryBase & { status: "failed" | "rejected"; assistantOutput?: never; failure: string };
20
+ | TransportEntryBase & { status: "rejected"; assistantOutput?: never; failure: string };
21
21
 
22
22
  export type WorkflowTransportEntryDetails = {
23
23
  id: string;
@@ -37,7 +37,7 @@ export type WorkflowTransportDetails = {
37
37
 
38
38
  export type BackgroundWorkflowTransportDetails = WorkflowTransportDetails & {
39
39
  taskId: string;
40
- outcome: "completed" | "failed" | "aborted";
40
+ outcome: "completed" | "failed";
41
41
  recovery?: true;
42
42
  };
43
43
 
@@ -63,7 +63,7 @@ function workflowTitle(mode: WorkflowMode): string {
63
63
  function statusCounts(entries: readonly WorkflowTransportEntry[]): string[] {
64
64
  const count = (statuses: WorkflowTransportStatus[]) => entries.filter(({ status }) => statuses.includes(status)).length;
65
65
  return ([
66
- [["failed", "rejected"], "failed"],
66
+ [["rejected"], "failed"],
67
67
  [["succeeded"], "completed"],
68
68
  [["skipped"], "skipped"],
69
69
  ] as const).flatMap(([statuses, word]) => {
@@ -84,7 +84,6 @@ const ENTRY_STATUS_PRESENTATION = {
84
84
  pending: { glyph: "○", fallback: "queued" },
85
85
  running: { glyph: "◌", fallback: "working" },
86
86
  succeeded: { glyph: "✓", fallback: "completed" },
87
- failed: { glyph: "✗", fallback: "failed" },
88
87
  rejected: { glyph: "✗", fallback: "failed" },
89
88
  skipped: { glyph: "–", fallback: "skipped" },
90
89
  } as const satisfies Record<WorkflowTransportEntryDetails["status"], WorkflowEntryStatusPresentation>;
@@ -94,7 +93,7 @@ export function presentWorkflowEntryStatus(status: WorkflowTransportEntryDetails
94
93
  }
95
94
 
96
95
  function sourceFor(entry: WorkflowTransportEntry): string | undefined {
97
- if (entry.status === "failed" || entry.status === "rejected") return entry.failure;
96
+ if (entry.status === "rejected") return entry.failure;
98
97
  if (entry.status === "running" || entry.status === "succeeded") return entry.assistantOutput;
99
98
  }
100
99
 
@@ -103,7 +102,7 @@ function evidenceFor(entry: WorkflowTransportEntry, position: number, total: num
103
102
  if (source === undefined || !source) return;
104
103
  const [preview, remainder] = splitEvidence(source);
105
104
  return {
106
- heading: `- [${position}/${total}] ${entry.name} · ${entry.role} · ${entry.status === "failed" || entry.status === "rejected" ? "failure" : "result"}:`,
105
+ heading: `- [${position}/${total}] ${entry.name} · ${entry.role} · ${entry.status === "rejected" ? "failure" : "result"}:`,
107
106
  preview,
108
107
  remainder,
109
108
  };
@@ -114,7 +113,7 @@ export function formatWorkflowResult(mode: WorkflowMode, entries: readonly Workf
114
113
  if (ordered.some(({ status }) => status === "pending" || status === "running")) {
115
114
  throw new TypeError("Final workflow transport requires terminal entry states.");
116
115
  }
117
- const failed = ordered.some(({ status }) => status === "failed" || status === "rejected");
116
+ const failed = ordered.some(({ status }) => status === "rejected");
118
117
  const positioned = ordered.map((entry, index) => ({ entry, position: index + 1 }));
119
118
  const evidence = positioned.flatMap(({ entry, position }) => {
120
119
  const value = evidenceFor(entry, position, ordered.length);
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
3
3
  import { type Component, Text, type TUI, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
4
4
  import { availableTaskModels, loadTaskModelsConfig, modelReference, registerModelTask, resolveAvailableModel, type ResolvedTaskRoute, taskThinkingLevels } from "@henryqw/pi-task-models";
5
- import { capEphemeralSubagentOutput as capOutput, createEphemeralSubagentExecutor, DELEGATE_TASK, formatDuration, loadRoles, prepareRoleLaunch, ROLE_TOOL_POLICY_FLAG, type EphemeralSubagentTimeout, type Role } from "@henryqw/pi-subagent";
5
+ import { capEphemeralSubagentOutput as capOutput, createEphemeralSubagentExecutor, DELEGATE_TASK, formatDuration, loadRoles, prepareRoleLaunch, ROLE_TOOL_POLICY_FLAG, type Role } from "@henryqw/pi-subagent";
6
6
  import { DEFAULT_EXECUTION_POLICY, DEFAULT_TIMEOUT_CONFIG, readSubagentConfig, resolveExecutionPolicy, type EffectiveExecutionPolicy } from "./config.ts";
7
7
  import { createCheckoutAdmission, roleCanWrite } from "./admission.ts";
8
8
  import { registerIsolatedExtension } from "./isolated.ts";
@@ -18,8 +18,7 @@ const MAX_WIDGET_ITEMS = 8;
18
18
  const MAX_WIDGET_LINES = 6;
19
19
  const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
20
20
 
21
- type TimeoutPolicy = EphemeralSubagentTimeout;
22
- type WidgetStatus = "working" | "success" | "failure" | "aborted";
21
+ type WidgetStatus = "working" | "success" | "failure";
23
22
  type WidgetItem = {
24
23
  role: string;
25
24
  model: string;
@@ -42,7 +41,6 @@ function statusGlyph(status: WidgetStatus, spinnerIndex: number, theme: Theme):
42
41
  case "working": return theme.fg("accent", SPINNER_FRAMES[spinnerIndex % SPINNER_FRAMES.length]!);
43
42
  case "success": return theme.fg("success", "✓");
44
43
  case "failure": return theme.fg("error", "✗");
45
- case "aborted": return theme.fg("warning", "■");
46
44
  }
47
45
  }
48
46
 
@@ -51,7 +49,6 @@ function statusLabel(status: WidgetStatus): string {
51
49
  case "working": return "working";
52
50
  case "success": return "complete";
53
51
  case "failure": return "failed";
54
- case "aborted": return "stopped";
55
52
  }
56
53
  }
57
54
 
@@ -87,11 +84,11 @@ function renderWidgetRows(
87
84
  });
88
85
  const hidden = ordered.slice(visible.length);
89
86
  if (hidden.length) {
90
- const counts: Record<WidgetStatus, number> = { working: 0, success: 0, failure: 0, aborted: 0 };
87
+ const counts: Record<WidgetStatus, number> = { working: 0, success: 0, failure: 0 };
91
88
  for (const { status } of hidden) counts[status] += 1;
92
89
  lines.push(truncateToWidth(theme.fg("muted", [
93
90
  `… ${hidden.length} more`,
94
- ...(["working", "success", "failure", "aborted"] as const).flatMap((status) =>
91
+ ...(["working", "success", "failure"] as const).flatMap((status) =>
95
92
  counts[status] ? [`${counts[status]} ${statusLabel(status)}`] : []),
96
93
  ].join(" · ")), width));
97
94
  }
@@ -128,10 +125,7 @@ const roleSummary = (): string => {
128
125
  }
129
126
  };
130
127
 
131
- export default function subagentExtension(
132
- pi: ExtensionAPI,
133
- overrideTimeoutPolicy?: TimeoutPolicy,
134
- ): void {
128
+ export default function subagentExtension(pi: ExtensionAPI): void {
135
129
  if (process.argv.includes(`--${ROLE_TOOL_POLICY_FLAG}`)) return;
136
130
  registerModelTask(pi, DELEGATE_TASK);
137
131
  pi.registerMessageRenderer(DIRECT_RESULT_TYPE, (message, { expanded, outputPad }, theme) => {
@@ -142,11 +136,9 @@ export default function subagentExtension(
142
136
  if (!details?.entries) return new Text(content, outputPad, 0);
143
137
  const count = details.entries.length;
144
138
  const subject = count === 1 ? "Direct subagent" : `${count} direct subagents`;
145
- const state = details.recovery ? "stopped; recovery needed"
146
- : details.outcome === "completed" ? "completed"
147
- : details.outcome === "failed" ? "failed" : "stopped";
148
- const glyph = details.recovery || details.outcome === "aborted" ? "■" : details.outcome === "completed" ? "✓" : "✗";
149
- const color = details.recovery || details.outcome === "aborted" ? "warning" : details.outcome === "completed" ? "success" : "error";
139
+ const state = details.recovery ? "stopped; recovery needed" : details.outcome;
140
+ const glyph = details.recovery ? "■" : details.outcome === "completed" ? "✓" : "✗";
141
+ const color = details.recovery ? "warning" : details.outcome === "completed" ? "success" : "error";
150
142
  const rows = details.entries.map(({ name, role, status, summary }) => {
151
143
  const { glyph, fallback } = presentWorkflowEntryStatus(status);
152
144
  return `${glyph} ${name} · ${role} — ${details.recovery ? fallback : summary || fallback}`;
@@ -175,20 +167,11 @@ export default function subagentExtension(
175
167
  maxCorrections: DEFAULT_EXECUTION_POLICY.maxCorrections,
176
168
  };
177
169
  }
178
- const timeoutPolicy: TimeoutPolicy = overrideTimeoutPolicy ?? {
179
- idleMs: initialPolicy.childIdleMs,
180
- maxMs: initialPolicy.childMaxMs,
181
- };
182
- const effectiveInitialPolicy: EffectiveExecutionPolicy = {
183
- ...initialPolicy,
184
- childIdleMs: timeoutPolicy.idleMs,
185
- childMaxMs: timeoutPolicy.maxMs ?? initialPolicy.childMaxMs,
186
- };
187
170
  const executor = createEphemeralSubagentExecutor({
188
- maxConcurrency: effectiveInitialPolicy.maxSubagents,
189
- maxTurns: effectiveInitialPolicy.maxTurns,
190
- maxTokens: effectiveInitialPolicy.maxTokens,
191
- timeout: timeoutPolicy,
171
+ maxConcurrency: initialPolicy.maxSubagents,
172
+ maxTurns: initialPolicy.maxTurns,
173
+ maxTokens: initialPolicy.maxTokens,
174
+ timeout: { idleMs: initialPolicy.childIdleMs, maxMs: initialPolicy.childMaxMs },
192
175
  });
193
176
  const currentPolicy = (): EffectiveExecutionPolicy => resolveExecutionPolicy(readSubagentConfig());
194
177
  const canWrite = (input: unknown): boolean => {
@@ -205,7 +188,7 @@ export default function subagentExtension(
205
188
  admission.register(pi, canWrite);
206
189
  const isolatedSurface = registerIsolatedExtension(pi, {
207
190
  executor,
208
- policy: effectiveInitialPolicy,
191
+ policy: initialPolicy,
209
192
  currentPolicy,
210
193
  });
211
194
  let directSequence = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henryqw/pi-subagent",
3
- "version": "22.2.0",
3
+ "version": "22.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",