pi-subagents 0.64.0 → 0.65.0

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.
Files changed (89) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +2 -2
  3. package/agents/reviewer.md +1 -1
  4. package/agents/scout.md +1 -1
  5. package/docs/agents.md +18 -16
  6. package/docs/configuration.md +7 -15
  7. package/docs/extension-api.md +15 -6
  8. package/docs/missions.md +2 -0
  9. package/docs/observability.md +10 -12
  10. package/docs/tool-reference.md +7 -6
  11. package/docs/watchdog.md +1 -1
  12. package/docs/workflows.md +5 -3
  13. package/package.json +3 -4
  14. package/skills/pi-subagents/references/constraints-and-recipes.md +1 -1
  15. package/skills/pi-subagents/references/execution-controls.md +2 -2
  16. package/src/agents/agent-management.ts +47 -15
  17. package/src/api/capability-ceiling.ts +0 -1
  18. package/src/api/{pi-args.ts → child-tool-plan.ts} +1 -1
  19. package/src/api/preflight.ts +2 -3
  20. package/src/extension/doctor.ts +2 -10
  21. package/src/extension/fanout-child.ts +9 -11
  22. package/src/extension/index.ts +27 -5
  23. package/src/extension/public-execution.ts +14 -0
  24. package/src/extension/rpc.ts +3 -2
  25. package/src/extension/schemas.ts +2 -1
  26. package/src/extension/tool-description.ts +9 -8
  27. package/src/intercom/native-supervisor-channel.ts +138 -60
  28. package/src/intercom/supervisor-ui.ts +243 -0
  29. package/src/runs/background/async-execution.ts +43 -24
  30. package/src/runs/background/async-job-tracker.ts +11 -0
  31. package/src/runs/background/async-resume.ts +11 -2
  32. package/src/runs/background/control-channel.ts +2 -204
  33. package/src/runs/background/process-terminal.ts +1 -1
  34. package/src/runs/background/run-child-session.ts +613 -0
  35. package/src/runs/background/run-status.ts +0 -1
  36. package/src/runs/background/runner-aliases.ts +125 -0
  37. package/src/runs/background/runner-child-sessions.ts +31 -0
  38. package/src/runs/background/scheduled-runs.ts +18 -4
  39. package/src/runs/background/subagent-runner.ts +184 -901
  40. package/src/runs/foreground/async-steering-action.ts +1 -17
  41. package/src/runs/foreground/execution.ts +189 -377
  42. package/src/runs/foreground/foreground-control.ts +4 -0
  43. package/src/runs/foreground/subagent-executor.ts +100 -57
  44. package/src/runs/foreground/workflow-foreground-steering.ts +24 -98
  45. package/src/runs/shared/abort-recovery.ts +3 -3
  46. package/src/runs/shared/capability-ceiling.ts +1 -2
  47. package/src/runs/shared/child-hooks.ts +25 -0
  48. package/src/runs/shared/child-identity.ts +13 -2
  49. package/src/runs/shared/child-launch.ts +314 -0
  50. package/src/runs/shared/child-lifecycle.ts +25 -0
  51. package/src/runs/shared/child-runtime-config.ts +126 -0
  52. package/src/runs/shared/child-session.ts +342 -0
  53. package/src/runs/shared/child-tool-plan.ts +530 -0
  54. package/src/runs/shared/claude-code-adapter.ts +5 -1
  55. package/src/runs/shared/completion-guard.ts +1 -1
  56. package/src/runs/shared/external-cli-preflight.ts +16 -0
  57. package/src/runs/shared/mcp-direct-tool-allowlist.ts +5 -4
  58. package/src/runs/shared/model-exclusions.ts +82 -14
  59. package/src/runs/shared/model-fallback.ts +47 -4
  60. package/src/runs/shared/nested-events.ts +29 -45
  61. package/src/runs/shared/nested-path.ts +0 -14
  62. package/src/runs/shared/orca-progress-tabs.ts +12 -7
  63. package/src/runs/shared/parallel-utils.ts +0 -2
  64. package/src/runs/shared/permissions.ts +0 -13
  65. package/src/runs/shared/process-signal.ts +4 -1
  66. package/src/runs/shared/run-fanout-budget.ts +0 -13
  67. package/src/runs/shared/runtime-acknowledged-extensions.ts +0 -27
  68. package/src/runs/shared/structured-output.ts +17 -4
  69. package/src/runs/shared/subagent-control.ts +2 -0
  70. package/src/runs/shared/subagent-prompt-runtime.ts +87 -384
  71. package/src/runs/shared/tool-availability.ts +18 -62
  72. package/src/runs/shared/tool-budget.ts +0 -14
  73. package/src/runs/shared/worktree-cleanup-plan.ts +25 -6
  74. package/src/runs/shared/worktree.ts +117 -30
  75. package/src/shared/child-session-name.ts +1 -1
  76. package/src/shared/jsonl-writer.ts +11 -0
  77. package/src/shared/thinking-ceiling.ts +0 -6
  78. package/src/shared/types.ts +55 -28
  79. package/src/shared/utils.ts +3 -4
  80. package/src/slash/slash-commands.ts +0 -6
  81. package/src/tui/fleet.ts +0 -1
  82. package/src/tui/render.ts +233 -31
  83. package/src/watchdog/child-status.ts +0 -1
  84. package/src/watchdog/register-child.ts +12 -12
  85. package/src/workflows/scripted-workflow.ts +48 -1
  86. package/src/runs/shared/child-protocol.ts +0 -415
  87. package/src/runs/shared/pi-args.ts +0 -1059
  88. package/src/runs/shared/subagent-startup-retry.ts +0 -116
  89. package/src/shared/post-exit-stdio-guard.ts +0 -85
@@ -9,7 +9,14 @@ import type { AcceptanceRecoveryMetadata, HostStepNodeV1, SingleResult } from ".
9
9
  import { normalizeWorkflowHostCommandParams, type WorkflowHostCommandParams, type WorkflowHostCommandResult } from "./host-command.ts";
10
10
 
11
11
  const KEY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
12
+ function validGitRef(ref: unknown): ref is string {
13
+ if (typeof ref !== "string" || !ref || ref === "@" || Buffer.byteLength(ref, "utf-8") > 1024 || ref.startsWith("/") || ref.endsWith("/") || ref.includes("//") || ref.includes("..") || ref.includes("@{")) return false;
14
+ if (/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/iu.test(ref)) return false;
15
+ if (/[[\]\\~^:?*\u0000-\u0020\u007f]/u.test(ref) || ref.endsWith(".") || ref.endsWith(".lock")) return false;
16
+ return ref.split("/").every((component) => component.length > 0 && component !== "." && component !== ".." && !component.startsWith(".") && !component.endsWith(".") && !component.endsWith(".lock"));
17
+ }
12
18
  const requireFromPackage = createRequire(import.meta.url);
19
+ const WORKFLOW_ASSEMBLY_FLUSH_TIMEOUT_MS = 5_000;
13
20
 
14
21
  export interface WorkflowScriptValidationError {
15
22
  message: string;
@@ -49,6 +56,12 @@ let suppressNativePromiseConsumption = 0;
49
56
  const activeNativePromises = [];
50
57
  const pending = new Map();
51
58
  const runKeyPattern = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
59
+ function validGitRef(ref) {
60
+ if (typeof ref !== "string" || !ref || ref === "@" || new TextEncoder().encode(ref).length > 1024 || ref.startsWith("/") || ref.endsWith("/") || ref.includes("//") || ref.includes("..") || ref.includes("@{")) return false;
61
+ if (/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/iu.test(ref)) return false;
62
+ if (/[[\]\\~^:?*\u0000-\u0020\u007f]/u.test(ref) || ref.endsWith(".") || ref.endsWith(".lock")) return false;
63
+ return ref.split("/").every((component) => component.length > 0 && component !== "." && component !== ".." && !component.startsWith(".") && !component.endsWith(".") && !component.endsWith(".lock"));
64
+ }
52
65
  const trackedPromiseTrackers = new WeakMap();
53
66
  const trackedPromiseTargets = new WeakMap();
54
67
  let nativePromiseTrackers = new WeakMap();
@@ -611,6 +624,7 @@ function validateRunCall(key, params, label, fingerprints) {
611
624
  }
612
625
  if (Object.prototype.hasOwnProperty.call(params, "clarify")) throw new Error(label + " does not support clarify UI.");
613
626
  if (params.worktree !== undefined && typeof params.worktree !== "boolean") throw new Error(label + " worktree must be true or false.");
627
+ if (params.baseRef !== undefined && (typeof params.baseRef !== "string" || !validGitRef(params.baseRef))) throw new Error(label + " baseRef must be a valid Git ref.");
614
628
  validateLaneMetadata(params.lane, label + " lane", key);
615
629
  if (params.gate !== undefined && (typeof params.gate !== "string" || !params.gate.trim())) throw new Error(label + " gate must be a non-empty command string.");
616
630
  if (params.gate !== undefined && params.acceptance !== undefined) throw new Error(label + " gate cannot be combined with acceptance; use one gate command or acceptance.verify.");
@@ -1070,6 +1084,8 @@ export interface RunWorkflowScriptOptions {
1070
1084
  oneUsePermit?: { claim: (key: string) => string | undefined };
1071
1085
  timeoutMs?: number;
1072
1086
  signal?: AbortSignal;
1087
+ /** Let an async workflow flush pure result assembly after reload once every child is terminal. */
1088
+ continueAfterAbortWhenChildrenSettled?: (abortError: Error) => boolean;
1073
1089
  /** Maximum children executing concurrently within this workflow. Defaults to 20. */
1074
1090
  globalConcurrencyLimit?: number;
1075
1091
  admit?: (calls: Array<{ key: string; params: Record<string, unknown> }>) => void | Promise<void>;
@@ -1614,7 +1630,7 @@ function resolveWorkflowParserEntry(): string {
1614
1630
  }
1615
1631
  }
1616
1632
 
1617
- const AUTO_RESUME_PARAM_KEYS = ["acceptance", "agentContract", "index", "intercomBridge", "label", "lane", "maxRuntimeMs", "output", "outputMode", "outputSchema", "phase", "skill", "skills", "task", "timeoutMs", "toolBudget", "worktree"] as const;
1633
+ const AUTO_RESUME_PARAM_KEYS = ["acceptance", "agentContract", "baseRef", "index", "intercomBridge", "label", "lane", "maxRuntimeMs", "output", "outputMode", "outputSchema", "phase", "skill", "skills", "task", "timeoutMs", "toolBudget", "worktree"] as const;
1618
1634
 
1619
1635
  function isZeroUsage(usage: unknown): boolean {
1620
1636
  if (!isRecord(usage)) return false;
@@ -1682,6 +1698,9 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
1682
1698
  let acceptanceRecoveryBarrier: { key: string } | undefined;
1683
1699
  let settled = false;
1684
1700
  let finishing = false;
1701
+ let assemblyAbortRequested = false;
1702
+ let assemblyFlushTimer: ReturnType<typeof setTimeout> | undefined;
1703
+ let abortError: Error | undefined;
1685
1704
 
1686
1705
  const partial = (): Omit<WorkflowScriptResult, "value"> => ({ emits, console: consoleEntries, trace, children: childOrder.flatMap((key) => {
1687
1706
  const child = children.get(key);
@@ -1750,6 +1769,10 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
1750
1769
  const finish = (outcome: { value: unknown } | { error: Error & { workflowErrorKind?: unknown } }) => {
1751
1770
  if (settled || finishing) return;
1752
1771
  finishing = true;
1772
+ if (assemblyFlushTimer !== undefined) {
1773
+ clearTimeout(assemblyFlushTimer);
1774
+ assemblyFlushTimer = undefined;
1775
+ }
1753
1776
  childController.abort("error" in outcome ? outcome.error : new Error("Workflow script completed."));
1754
1777
  void Promise.allSettled([...steers.values(), ...hostCalls.values()].map(({ promise }) => promise)).then(() => {
1755
1778
  if (settled) return;
@@ -1781,6 +1804,26 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
1781
1804
  : typeof signalReason === "string"
1782
1805
  ? new Error(signalReason)
1783
1806
  : new Error("Workflow script aborted.");
1807
+ if (finishing) return;
1808
+ abortError = error;
1809
+ const allChildrenSettled = launches.size > 0
1810
+ && [...launches.keys()].every((key) => children.has(key));
1811
+ let mayFlushAssembly = false;
1812
+ try {
1813
+ mayFlushAssembly = options.continueAfterAbortWhenChildrenSettled?.(error) === true;
1814
+ } catch (callbackError) {
1815
+ const callbackMessage = callbackError instanceof Error ? callbackError.message : String(callbackError);
1816
+ return finish({ error: new Error(`Workflow assembly flush eligibility failed: ${callbackMessage}`) });
1817
+ }
1818
+ if (mayFlushAssembly && allChildrenSettled) {
1819
+ // A reloaded async workflow may already be past its last child launch.
1820
+ // Keep the worker alive for pure result assembly, but abort the child
1821
+ // signal so any later launch or side effect cannot use stale context.
1822
+ assemblyAbortRequested = true;
1823
+ childController.abort(error);
1824
+ assemblyFlushTimer = setTimeout(() => finish({ error }), WORKFLOW_ASSEMBLY_FLUSH_TIMEOUT_MS);
1825
+ return;
1826
+ }
1784
1827
  for (const key of launches.keys()) {
1785
1828
  if (children.has(key)) continue;
1786
1829
  stoppedLaunches.add(key);
@@ -1873,6 +1916,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
1873
1916
  return;
1874
1917
  }
1875
1918
  if (message.type !== "call" || typeof message.callId !== "number" || typeof message.method !== "string" || !isRecord(message.args)) return;
1919
+ if (assemblyAbortRequested) return finish({ error: abortError ?? new Error("Workflow context was replaced or reloaded.") });
1876
1920
 
1877
1921
  const respond = (promise: Promise<unknown>, responsePath?: string, onBoundaryError?: (error: unknown) => void) => {
1878
1922
  void promise.then(
@@ -2059,6 +2103,9 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
2059
2103
  if (params.worktree !== undefined && typeof params.worktree !== "boolean") {
2060
2104
  return respond(Promise.reject(new Error(`runs.run('${key}') worktree must be true or false.`)));
2061
2105
  }
2106
+ if (params.baseRef !== undefined && (typeof params.baseRef !== "string" || !validGitRef(params.baseRef))) {
2107
+ return respond(Promise.reject(new Error(`runs.run('${key}') baseRef must be a valid Git ref.`)));
2108
+ }
2062
2109
  if (params.gate !== undefined && (typeof params.gate !== "string" || !params.gate.trim())) {
2063
2110
  return respond(Promise.reject(new Error(`runs.run('${key}') gate must be a non-empty command string.`)));
2064
2111
  }
@@ -1,415 +0,0 @@
1
- import { Buffer } from "node:buffer";
2
- import type { ProtocolOutputLimit } from "../../shared/types.ts";
3
-
4
- export type { ProtocolOutputLimit } from "../../shared/types.ts";
5
-
6
- export const MAX_CHILD_PENDING_LINE_BYTES = 16 * 1024 * 1024;
7
- export const MAX_CHILD_STDERR_BYTES = 128 * 1024;
8
- const MAX_PROTOCOL_DIAGNOSTIC_BYTES = 4096;
9
- const MAX_PROJECTED_JSON_DEPTH = 256;
10
-
11
- export interface OversizedLineProjection {
12
- push(chunk: Buffer): boolean;
13
- finish(): string | undefined;
14
- }
15
-
16
- export interface OversizedLineProjector {
17
- accepts(prefix: string): boolean;
18
- create(): OversizedLineProjection;
19
- }
20
-
21
- type JsonContainer =
22
- | { type: "array"; state: "value-or-end" | "value" | "comma-or-end" }
23
- | { type: "object"; state: "key-or-end" | "key" | "colon" | "value" | "comma-or-end"; key?: string };
24
-
25
- type JsonToken =
26
- | { type: "string"; role: "key" | "value"; value: string; capture: boolean; escape: boolean; unicodeDigits: number; unicodeValue: string }
27
- | { type: "literal"; expected: string; index: number; value: boolean | null }
28
- | { type: "number"; phase: "minus" | "zero" | "int" | "dot" | "frac" | "exp" | "exp-sign" | "exp-digits" };
29
-
30
- function createPiAggregateProjection(): OversizedLineProjection {
31
- const decoder = new TextDecoder("utf-8", { fatal: true });
32
- const stack: JsonContainer[] = [];
33
- let rootState: "value" | "end" = "value";
34
- let token: JsonToken | undefined;
35
- let valid = true;
36
- let eventType: string | undefined;
37
- let willRetry: boolean | undefined;
38
-
39
- const parent = (): JsonContainer | undefined => stack.at(-1);
40
- const isTopLevelField = (key: string | undefined): boolean => stack.length === 1 && parent()?.type === "object" && (key === "type" || key === "willRetry");
41
-
42
- const completeValue = (value?: string | boolean | null): void => {
43
- const container = parent();
44
- if (!container) {
45
- rootState = "end";
46
- return;
47
- }
48
- if (container.type === "object") {
49
- if (stack.length === 1 && container.key === "type" && typeof value === "string") eventType = value;
50
- if (stack.length === 1 && container.key === "willRetry" && typeof value === "boolean") willRetry = value;
51
- container.key = undefined;
52
- container.state = "comma-or-end";
53
- } else container.state = "comma-or-end";
54
- };
55
-
56
- const startValue = (char: string): boolean => {
57
- const container = parent();
58
- const key = container?.type === "object" ? container.key : undefined;
59
- if (isTopLevelField(key)) {
60
- if (key === "type") eventType = undefined;
61
- else willRetry = undefined;
62
- }
63
- if (char === "{" || char === "[") {
64
- if (stack.length >= MAX_PROJECTED_JSON_DEPTH) return false;
65
- stack.push(char === "{" ? { type: "object", state: "key-or-end" } : { type: "array", state: "value-or-end" });
66
- return true;
67
- }
68
- if (char === '"') {
69
- token = { type: "string", role: "value", value: "", capture: key === "type" && stack.length === 1, escape: false, unicodeDigits: 0, unicodeValue: "" };
70
- return true;
71
- }
72
- if (char === "t") token = { type: "literal", expected: "true", index: 1, value: true };
73
- else if (char === "f") token = { type: "literal", expected: "false", index: 1, value: false };
74
- else if (char === "n") token = { type: "literal", expected: "null", index: 1, value: null };
75
- else if (char === "-") token = { type: "number", phase: "minus" };
76
- else if (char === "0") token = { type: "number", phase: "zero" };
77
- else if (char >= "1" && char <= "9") token = { type: "number", phase: "int" };
78
- else return false;
79
- return true;
80
- };
81
-
82
- const closeContainer = (): true => {
83
- stack.pop();
84
- completeValue();
85
- return true;
86
- };
87
-
88
- const processChar = (char: string): boolean => {
89
- if (token?.type === "string") {
90
- if (token.unicodeDigits > 0) {
91
- if (!/[0-9a-fA-F]/.test(char)) return false;
92
- token.unicodeValue += char;
93
- token.unicodeDigits--;
94
- if (token.unicodeDigits === 0 && token.capture) {
95
- if (token.value.length >= 64) return false;
96
- token.value += String.fromCharCode(Number.parseInt(token.unicodeValue, 16));
97
- }
98
- return true;
99
- }
100
- if (token.escape) {
101
- token.escape = false;
102
- if (char === "u") { token.unicodeDigits = 4; token.unicodeValue = ""; return true; }
103
- if (!'"\\/bfnrt'.includes(char)) return false;
104
- if (token.capture) {
105
- if (token.value.length >= 64) return false;
106
- token.value += ({ b: "\b", f: "\f", n: "\n", r: "\r", t: "\t" } as Record<string, string>)[char] ?? char;
107
- }
108
- return true;
109
- }
110
- if (char === "\\") { token.escape = true; return true; }
111
- if (char === '"') {
112
- const finished = token;
113
- token = undefined;
114
- if (finished.role === "key") {
115
- const container = parent();
116
- if (!container || container.type !== "object") return false;
117
- container.key = finished.value;
118
- container.state = "colon";
119
- } else completeValue(finished.capture ? finished.value : undefined);
120
- return true;
121
- }
122
- if (char.charCodeAt(0) < 0x20) return false;
123
- if (token.capture) {
124
- if (token.value.length >= 64) return false;
125
- token.value += char;
126
- }
127
- return true;
128
- }
129
- if (token?.type === "literal") {
130
- if (char !== token.expected[token.index]) return false;
131
- token.index++;
132
- if (token.index === token.expected.length) {
133
- const value = token.value;
134
- token = undefined;
135
- completeValue(value);
136
- }
137
- return true;
138
- }
139
- if (token?.type === "number") {
140
- const phase = token.phase;
141
- if (phase === "minus") {
142
- if (char === "0") token.phase = "zero";
143
- else if (char >= "1" && char <= "9") token.phase = "int";
144
- else return false;
145
- return true;
146
- }
147
- if (phase === "zero" || phase === "int") {
148
- if (char >= "0" && char <= "9") { if (phase === "zero") return false; return true; }
149
- if (char === ".") { token.phase = "dot"; return true; }
150
- if (char === "e" || char === "E") { token.phase = "exp"; return true; }
151
- } else if (phase === "dot") {
152
- if (char >= "0" && char <= "9") { token.phase = "frac"; return true; }
153
- return false;
154
- } else if (phase === "frac") {
155
- if (char >= "0" && char <= "9") return true;
156
- if (char === "e" || char === "E") { token.phase = "exp"; return true; }
157
- } else if (phase === "exp") {
158
- if (char === "+" || char === "-") { token.phase = "exp-sign"; return true; }
159
- if (char >= "0" && char <= "9") { token.phase = "exp-digits"; return true; }
160
- return false;
161
- } else if (phase === "exp-sign") {
162
- if (char >= "0" && char <= "9") { token.phase = "exp-digits"; return true; }
163
- return false;
164
- } else if (phase === "exp-digits" && char >= "0" && char <= "9") return true;
165
- if (!["zero", "int", "frac", "exp-digits"].includes(phase)) return false;
166
- token = undefined;
167
- completeValue();
168
- return processChar(char);
169
- }
170
- if (char === " " || char === "\t" || char === "\r" || char === "\n") return true;
171
- const container = parent();
172
- if (!container) return rootState === "value" ? startValue(char) : false;
173
- if (container.type === "object") {
174
- if (container.state === "key-or-end" || container.state === "key") {
175
- if (char === "}" && container.state === "key-or-end") return closeContainer();
176
- if (char !== '"') return false;
177
- token = { type: "string", role: "key", value: "", capture: stack.length === 1, escape: false, unicodeDigits: 0, unicodeValue: "" };
178
- return true;
179
- }
180
- if (container.state === "colon") {
181
- if (char !== ":") return false;
182
- container.state = "value";
183
- return true;
184
- }
185
- if (container.state === "value") return startValue(char);
186
- if (char === ",") { container.state = "key"; return true; }
187
- if (char === "}") return closeContainer();
188
- return false;
189
- }
190
- if (container.state === "value-or-end" || container.state === "value") {
191
- if (char === "]" && container.state === "value-or-end") return closeContainer();
192
- return startValue(char);
193
- }
194
- if (char === ",") { container.state = "value"; return true; }
195
- if (char === "]") return closeContainer();
196
- return false;
197
- };
198
-
199
- const processText = (text: string): boolean => {
200
- for (const char of text) if (!processChar(char)) return false;
201
- return true;
202
- };
203
-
204
- return {
205
- push(chunk) {
206
- if (!valid) return false;
207
- try { valid = processText(decoder.decode(chunk, { stream: true })); }
208
- catch { valid = false; }
209
- return valid;
210
- },
211
- finish() {
212
- try { valid = valid && processText(decoder.decode()); }
213
- catch { valid = false; }
214
- if (token?.type === "number" && ["zero", "int", "frac", "exp-digits"].includes(token.phase)) {
215
- token = undefined;
216
- completeValue();
217
- }
218
- if (!valid || token || stack.length !== 0 || rootState !== "end") return undefined;
219
- if (eventType === "turn_end") return '{"type":"turn_end"}';
220
- if (eventType === "agent_end" && typeof willRetry === "boolean") return JSON.stringify({ type: "agent_end", willRetry });
221
- if (eventType === "compaction_end" && typeof willRetry === "boolean") return JSON.stringify({ type: "compaction_end", willRetry });
222
- return undefined;
223
- },
224
- };
225
- }
226
-
227
- /**
228
- * Pi JSON mode emits granular message/tool events followed by aggregate
229
- * `turn_end`, `agent_end`, and `compaction_end` events that can duplicate
230
- * large payloads. Parallel image reads can make one aggregate record exceed
231
- * the child line limit even though every granular event was valid. Replace
232
- * only syntactically valid, redundant records with the lifecycle fields the
233
- * runners consume.
234
- */
235
- export const PI_AGGREGATE_EVENT_PROJECTOR: OversizedLineProjector = {
236
- accepts(prefix) {
237
- return prefix.startsWith('{"type":"turn_end"') || prefix.startsWith('{"type":"agent_end"') || prefix.startsWith('{"type":"compaction_end"');
238
- },
239
- create: createPiAggregateProjection,
240
- };
241
-
242
- export function formatProtocolOutputLimit(limit: ProtocolOutputLimit): string {
243
- return `${limit.code}: child ${limit.stream} line exceeded ${limit.limitBytes} bytes (observed at least ${limit.observedBytes} bytes without a newline).`;
244
- }
245
-
246
- export function createBoundedLineReader(options: {
247
- stream?: "stdout" | "stderr";
248
- maxPendingLineBytes?: number;
249
- oversizedLineProjector?: OversizedLineProjector;
250
- onLine: (line: string) => void;
251
- onLimit: (limit: ProtocolOutputLimit) => void;
252
- }): {
253
- push(chunk: Buffer | string): void;
254
- end(): void;
255
- exceeded(): boolean;
256
- } {
257
- const maxPendingLineBytes = options.maxPendingLineBytes ?? MAX_CHILD_PENDING_LINE_BYTES;
258
- if (!Number.isInteger(maxPendingLineBytes) || maxPendingLineBytes < 1) {
259
- throw new Error("maxPendingLineBytes must be a positive integer.");
260
- }
261
- let pending: Buffer[] = [];
262
- let pendingBytes = 0;
263
- let projectedPrefix: Buffer<ArrayBufferLike> = Buffer.alloc(0);
264
- let projectedTail: Buffer<ArrayBufferLike> = Buffer.alloc(0);
265
- let projectedBytes = 0;
266
- let projection: OversizedLineProjection | undefined;
267
- let projectingOversizedLine = false;
268
- let limitExceeded = false;
269
-
270
- const diagnosticTail = (prior: Buffer, segment: Buffer): Buffer => {
271
- const tailFromSegment = segment.subarray(Math.max(0, segment.length - MAX_PROTOCOL_DIAGNOSTIC_BYTES));
272
- return tailFromSegment.length === MAX_PROTOCOL_DIAGNOSTIC_BYTES
273
- ? tailFromSegment
274
- : Buffer.concat([prior.subarray(Math.max(0, prior.length - (MAX_PROTOCOL_DIAGNOSTIC_BYTES - tailFromSegment.length))), tailFromSegment]);
275
- };
276
-
277
- const failLimit = (observedBytes: number, prefix: Buffer, tail: Buffer): false => {
278
- limitExceeded = true;
279
- pending = [];
280
- pendingBytes = 0;
281
- projectingOversizedLine = false;
282
- projection = undefined;
283
- projectedPrefix = Buffer.alloc(0);
284
- projectedTail = Buffer.alloc(0);
285
- projectedBytes = 0;
286
- options.onLimit({
287
- code: "protocol_output_limit",
288
- stream: options.stream ?? "stdout",
289
- limitBytes: maxPendingLineBytes,
290
- observedBytes,
291
- diagnosticPrefix: prefix.toString("utf8"),
292
- diagnosticTail: tail.toString("utf8"),
293
- });
294
- return false;
295
- };
296
-
297
- const finishLine = (): void => {
298
- if (projectingOversizedLine) {
299
- const projected = projection?.finish();
300
- if (projected === undefined) {
301
- failLimit(projectedBytes, projectedPrefix, projectedTail);
302
- } else {
303
- options.onLine(projected);
304
- }
305
- } else if (pendingBytes > 0) {
306
- options.onLine(Buffer.concat(pending, pendingBytes).toString("utf8"));
307
- }
308
- pending = [];
309
- pendingBytes = 0;
310
- projectingOversizedLine = false;
311
- projection = undefined;
312
- projectedPrefix = Buffer.alloc(0);
313
- projectedTail = Buffer.alloc(0);
314
- projectedBytes = 0;
315
- };
316
-
317
- const append = (segment: Buffer): boolean => {
318
- if (segment.length === 0) return true;
319
- if (projectingOversizedLine) {
320
- projectedBytes += segment.length;
321
- projectedTail = diagnosticTail(projectedTail, segment);
322
- return projection?.push(segment) === true || failLimit(projectedBytes, projectedPrefix, projectedTail);
323
- }
324
- const observedBytes = pendingBytes + segment.length;
325
- if (observedBytes > maxPendingLineBytes) {
326
- const prior = pendingBytes > 0 ? Buffer.concat(pending, pendingBytes) : Buffer.alloc(0);
327
- const prefixFromPrior = prior.subarray(0, MAX_PROTOCOL_DIAGNOSTIC_BYTES);
328
- const prefix = prefixFromPrior.length === MAX_PROTOCOL_DIAGNOSTIC_BYTES
329
- ? prefixFromPrior
330
- : Buffer.concat([prefixFromPrior, segment.subarray(0, MAX_PROTOCOL_DIAGNOSTIC_BYTES - prefixFromPrior.length)]);
331
- const tail = diagnosticTail(prior, segment);
332
- if (options.oversizedLineProjector?.accepts(prefix.toString("utf8"))) {
333
- const candidate = options.oversizedLineProjector.create();
334
- if (!candidate.push(prior) || !candidate.push(segment)) return failLimit(observedBytes, prefix, tail);
335
- pending = [];
336
- pendingBytes = 0;
337
- projectingOversizedLine = true;
338
- projection = candidate;
339
- projectedPrefix = prefix;
340
- projectedTail = tail;
341
- projectedBytes = observedBytes;
342
- return true;
343
- }
344
- return failLimit(observedBytes, prefix, tail);
345
- }
346
- pending.push(segment);
347
- pendingBytes = observedBytes;
348
- return true;
349
- };
350
-
351
- return {
352
- push(chunk) {
353
- if (limitExceeded) return;
354
- const bytes = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
355
- let start = 0;
356
- for (let index = 0; index < bytes.length; index++) {
357
- if (bytes[index] !== 0x0a) continue;
358
- if (!append(bytes.subarray(start, index))) return;
359
- finishLine();
360
- if (limitExceeded) return;
361
- start = index + 1;
362
- }
363
- append(bytes.subarray(start));
364
- },
365
- end() {
366
- if (!limitExceeded) finishLine();
367
- },
368
- exceeded: () => limitExceeded,
369
- };
370
- }
371
-
372
- function trimToUtf8Boundary(buffer: Buffer, maxBytes: number): Buffer {
373
- if (buffer.length <= maxBytes) return buffer;
374
- let start = buffer.length - maxBytes;
375
- while (start < buffer.length && (buffer[start]! & 0xc0) === 0x80) start++;
376
- return buffer.subarray(start);
377
- }
378
-
379
- export function createBoundedByteTail(maxBytes = MAX_CHILD_STDERR_BYTES): {
380
- push(chunk: Buffer | string): void;
381
- text(): string;
382
- byteLength(): number;
383
- } {
384
- if (!Number.isInteger(maxBytes) || maxBytes < 1) throw new Error("maxBytes must be a positive integer.");
385
- let tail: Buffer<ArrayBufferLike> = Buffer.alloc(0);
386
- return {
387
- push(chunk) {
388
- const bytes = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
389
- tail = trimToUtf8Boundary(Buffer.concat([tail, bytes]), maxBytes);
390
- },
391
- text: () => tail.toString("utf8"),
392
- byteLength: () => tail.length,
393
- };
394
- }
395
-
396
- export type ChildLifecycleAction = "start-drain" | "cancel-drain" | "none";
397
-
398
- export interface ChildLifecycleState {
399
- compactionRetryActive: boolean;
400
- }
401
-
402
- export function projectChildLifecycle(event: { type?: string; willRetry?: unknown }, terminalAssistantStop = false, state?: ChildLifecycleState): ChildLifecycleAction {
403
- if (event.type === "compaction_end") {
404
- if (state) state.compactionRetryActive = event.willRetry === true;
405
- return event.willRetry === true ? "cancel-drain" : "none";
406
- }
407
- if (event.type === "agent_start" || event.type === "auto_retry_start") {
408
- if (state) state.compactionRetryActive = false;
409
- }
410
- if (event.type === "agent_end" && event.willRetry === true) return "cancel-drain";
411
- if (event.type === "agent_end" && state) state.compactionRetryActive = false;
412
- if (event.type === "agent_settled") return state?.compactionRetryActive ? "none" : "start-drain";
413
- if (terminalAssistantStop) return "start-drain";
414
- return "none";
415
- }