pi-subagents 0.63.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.
- package/CHANGELOG.md +57 -1
- package/README.md +2 -2
- package/agents/reviewer.md +1 -1
- package/agents/scout.md +1 -1
- package/docs/agents.md +18 -16
- package/docs/configuration.md +7 -15
- package/docs/extension-api.md +15 -6
- package/docs/missions.md +2 -0
- package/docs/observability.md +10 -12
- package/docs/tool-reference.md +7 -6
- package/docs/watchdog.md +92 -114
- package/docs/workflows.md +5 -3
- package/package.json +3 -4
- package/skills/pi-subagents/references/constraints-and-recipes.md +1 -1
- package/skills/pi-subagents/references/execution-controls.md +6 -5
- package/src/agents/agent-management.ts +47 -15
- package/src/api/capability-ceiling.ts +0 -1
- package/src/api/{pi-args.ts → child-tool-plan.ts} +1 -1
- package/src/api/preflight.ts +2 -3
- package/src/extension/doctor.ts +2 -10
- package/src/extension/fanout-child.ts +9 -11
- package/src/extension/index.ts +27 -5
- package/src/extension/public-execution.ts +14 -0
- package/src/extension/rpc.ts +3 -2
- package/src/extension/schemas.ts +2 -1
- package/src/extension/tool-description.ts +9 -8
- package/src/intercom/native-supervisor-channel.ts +138 -60
- package/src/intercom/supervisor-ui.ts +243 -0
- package/src/runs/background/async-execution.ts +46 -24
- package/src/runs/background/async-job-tracker.ts +11 -0
- package/src/runs/background/async-resume.ts +11 -2
- package/src/runs/background/control-channel.ts +2 -204
- package/src/runs/background/notify.ts +41 -2
- package/src/runs/background/process-terminal.ts +1 -1
- package/src/runs/background/run-child-session.ts +613 -0
- package/src/runs/background/run-status.ts +0 -1
- package/src/runs/background/runner-aliases.ts +125 -0
- package/src/runs/background/runner-child-sessions.ts +31 -0
- package/src/runs/background/scheduled-runs.ts +18 -4
- package/src/runs/background/subagent-runner.ts +203 -898
- package/src/runs/foreground/async-steering-action.ts +1 -17
- package/src/runs/foreground/execution.ts +196 -378
- package/src/runs/foreground/foreground-control.ts +4 -0
- package/src/runs/foreground/subagent-executor.ts +121 -58
- package/src/runs/foreground/workflow-foreground-steering.ts +24 -98
- package/src/runs/shared/abort-recovery.ts +3 -3
- package/src/runs/shared/acceptance.ts +10 -0
- package/src/runs/shared/async-status-projection.ts +11 -43
- package/src/runs/shared/capability-ceiling.ts +1 -2
- package/src/runs/shared/child-hooks.ts +25 -0
- package/src/runs/shared/child-identity.ts +13 -2
- package/src/runs/shared/child-launch.ts +314 -0
- package/src/runs/shared/child-lifecycle.ts +25 -0
- package/src/runs/shared/child-runtime-config.ts +126 -0
- package/src/runs/shared/child-session.ts +342 -0
- package/src/runs/shared/child-tool-plan.ts +530 -0
- package/src/runs/shared/claude-code-adapter.ts +5 -1
- package/src/runs/shared/completion-guard.ts +1 -1
- package/src/runs/shared/external-cli-preflight.ts +16 -0
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +5 -4
- package/src/runs/shared/model-exclusions.ts +82 -14
- package/src/runs/shared/model-fallback.ts +47 -4
- package/src/runs/shared/nested-events.ts +29 -45
- package/src/runs/shared/nested-path.ts +0 -14
- package/src/runs/shared/orca-progress-tabs.ts +12 -7
- package/src/runs/shared/parallel-utils.ts +0 -2
- package/src/runs/shared/permissions.ts +0 -13
- package/src/runs/shared/process-signal.ts +4 -1
- package/src/runs/shared/run-fanout-budget.ts +0 -13
- package/src/runs/shared/runtime-acknowledged-extensions.ts +0 -27
- package/src/runs/shared/structured-output.ts +17 -4
- package/src/runs/shared/subagent-control.ts +6 -2
- package/src/runs/shared/subagent-prompt-runtime.ts +87 -384
- package/src/runs/shared/tool-availability.ts +18 -62
- package/src/runs/shared/tool-budget.ts +0 -14
- package/src/runs/shared/worktree-cleanup-plan.ts +25 -6
- package/src/runs/shared/worktree.ts +117 -30
- package/src/shared/child-session-name.ts +1 -1
- package/src/shared/jsonl-writer.ts +11 -0
- package/src/shared/thinking-ceiling.ts +0 -6
- package/src/shared/types.ts +64 -30
- package/src/shared/utils.ts +3 -4
- package/src/slash/slash-commands.ts +0 -6
- package/src/tui/fleet.ts +0 -1
- package/src/tui/render.ts +238 -31
- package/src/watchdog/child-status.ts +54 -34
- package/src/watchdog/diff-tool.ts +77 -0
- package/src/watchdog/emission-guard.ts +5 -3
- package/src/watchdog/guidance.ts +20 -0
- package/src/watchdog/register-child.ts +28 -25
- package/src/watchdog/register-main.ts +10 -9
- package/src/watchdog/render.ts +4 -5
- package/src/watchdog/review.ts +15 -4
- package/src/watchdog/rules.ts +70 -0
- package/src/watchdog/runtime.ts +75 -92
- package/src/watchdog/scope.ts +0 -11
- package/src/watchdog/settings.ts +48 -104
- package/src/watchdog/types.ts +18 -32
- package/src/watchdog/warning-format.ts +0 -1
- package/src/workflows/chat-progress.ts +3 -2
- package/src/workflows/scripted-workflow.ts +48 -1
- package/src/workflows/workflow-checklist.ts +10 -12
- package/src/workflows/workflow-preflight.ts +28 -1
- package/src/runs/shared/child-protocol.ts +0 -415
- package/src/runs/shared/pi-args.ts +0 -1062
- package/src/runs/shared/subagent-startup-retry.ts +0 -116
- package/src/shared/post-exit-stdio-guard.ts +0 -85
|
@@ -146,8 +146,35 @@ function declaredKeys(preflight: WorkflowPreflightV1): Set<string> {
|
|
|
146
146
|
* Treat declared lane keys as plan roots: `lane` is the lane itself and
|
|
147
147
|
* `lane.stage` is a stage by convention, even without generated provenance.
|
|
148
148
|
*/
|
|
149
|
+
export function workflowKeyMatchesPreflightLane(key: string, laneKey: string, generatedLaneKey?: string): boolean {
|
|
150
|
+
return generatedLaneKey === laneKey || key === laneKey || key.startsWith(`${laneKey}.`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Select the most specific advisory lane without allowing declaration order to override an exact runtime key. */
|
|
154
|
+
export function workflowPreflightLaneForRuntimeKey(
|
|
155
|
+
preflight: WorkflowPreflightV1 | undefined,
|
|
156
|
+
key: string,
|
|
157
|
+
preferredKeys: readonly (string | undefined)[] = [],
|
|
158
|
+
): WorkflowPreflightLaneV1 | undefined {
|
|
159
|
+
const lanes = preflight?.lanes;
|
|
160
|
+
if (!lanes) return undefined;
|
|
161
|
+
const exact = lanes.find((lane) => lane.key === key);
|
|
162
|
+
if (exact) return exact;
|
|
163
|
+
let closestRoot: WorkflowPreflightLaneV1 | undefined;
|
|
164
|
+
for (const lane of lanes) {
|
|
165
|
+
if (key.startsWith(`${lane.key}.`) && (!closestRoot || lane.key.length > closestRoot.key.length)) closestRoot = lane;
|
|
166
|
+
}
|
|
167
|
+
if (closestRoot) return closestRoot;
|
|
168
|
+
for (const preferredKey of preferredKeys) {
|
|
169
|
+
if (!preferredKey) continue;
|
|
170
|
+
const preferred = lanes.find((lane) => lane.key === preferredKey);
|
|
171
|
+
if (preferred) return preferred;
|
|
172
|
+
}
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
|
|
149
176
|
function declaredLaneCoversWorkflowKey(entry: WorkflowTraceLike, laneKey: string): boolean {
|
|
150
|
-
return entry.key
|
|
177
|
+
return workflowKeyMatchesPreflightLane(entry.key, laneKey, entry.generatedLaneKey);
|
|
151
178
|
}
|
|
152
179
|
|
|
153
180
|
function keyCoveredByDeclaredLane(entry: WorkflowTraceLike, declared: ReadonlySet<string>): boolean {
|
|
@@ -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
|
-
}
|