pi-subagents 0.64.0 → 0.65.1
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 +65 -0
- package/README.md +2 -2
- package/agents/reviewer.md +1 -1
- package/agents/scout.md +1 -1
- package/docs/agents.md +19 -17
- package/docs/configuration.md +23 -15
- package/docs/extension-api.md +18 -6
- package/docs/missions.md +2 -0
- package/docs/observability.md +10 -12
- package/docs/tool-reference.md +15 -8
- package/docs/watchdog.md +1 -1
- package/docs/workflows.md +13 -3
- package/package.json +5 -4
- package/runner-server-preload.mjs +13 -0
- package/skills/pi-subagents/SKILL.md +2 -1
- package/skills/pi-subagents/references/constraints-and-recipes.md +1 -1
- package/skills/pi-subagents/references/execution-controls.md +7 -3
- package/skills/pi-subagents/references/multi-lane-orchestration.md +2 -0
- 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 +7 -4
- package/src/extension/config.ts +4 -2
- package/src/extension/doctor.ts +2 -10
- package/src/extension/fanout-child.ts +9 -11
- package/src/extension/index.ts +58 -7
- package/src/extension/public-execution.ts +14 -0
- package/src/extension/rpc.ts +3 -2
- package/src/extension/schemas.ts +3 -2
- package/src/extension/tool-description.ts +13 -8
- package/src/integrations/pi-web-session-liveness.ts +73 -0
- package/src/intercom/native-supervisor-channel.ts +159 -95
- package/src/intercom/supervisor-ui.ts +244 -0
- package/src/missions/workflow-state.ts +37 -16
- package/src/runs/background/async-execution.ts +51 -25
- package/src/runs/background/async-job-tracker.ts +11 -0
- package/src/runs/background/async-resume.ts +14 -3
- package/src/runs/background/async-retention.ts +9 -0
- package/src/runs/background/control-channel.ts +2 -204
- package/src/runs/background/notify.ts +2 -0
- package/src/runs/background/process-terminal.ts +1 -1
- package/src/runs/background/retained-nested-route-tracker.ts +96 -0
- package/src/runs/background/run-child-session.ts +614 -0
- package/src/runs/background/run-status.ts +0 -1
- package/src/runs/background/runner-aliases.ts +152 -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 -901
- package/src/runs/foreground/async-steering-action.ts +1 -17
- package/src/runs/foreground/execution.ts +204 -378
- package/src/runs/foreground/foreground-control.ts +4 -0
- package/src/runs/foreground/foreground-history.ts +3 -1
- package/src/runs/foreground/prompt-audit.ts +9 -5
- package/src/runs/foreground/subagent-executor.ts +161 -91
- 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 +14 -1
- 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 +337 -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/llm-intent-arbiter.ts +20 -11
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +5 -4
- package/src/runs/shared/model-exclusions.ts +84 -15
- package/src/runs/shared/model-fallback.ts +78 -6
- package/src/runs/shared/nested-events.ts +32 -48
- 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 +1 -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 +2 -0
- 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 +31 -9
- package/src/runs/shared/worktree.ts +192 -40
- package/src/shared/child-session-name.ts +1 -1
- package/src/shared/jsonl-writer.ts +11 -0
- package/src/shared/model-response-aliases.ts +13 -0
- package/src/shared/thinking-ceiling.ts +0 -6
- package/src/shared/types.ts +63 -28
- package/src/shared/utils.ts +6 -4
- package/src/shared/watch-strategy.ts +2 -0
- package/src/slash/slash-commands.ts +0 -6
- package/src/tui/fleet-status.ts +1 -1
- package/src/tui/fleet.ts +0 -1
- package/src/tui/render.ts +254 -41
- package/src/watchdog/child-status.ts +0 -1
- package/src/watchdog/register-child.ts +12 -12
- package/src/workflows/scripted-workflow.ts +80 -7
- package/src/workflows/workflow-checklist.ts +4 -3
- package/src/runs/shared/child-protocol.ts +0 -415
- package/src/runs/shared/pi-args.ts +0 -1059
- package/src/runs/shared/subagent-startup-retry.ts +0 -116
- package/src/shared/post-exit-stdio-guard.ts +0 -85
|
@@ -5,11 +5,19 @@ import { Worker } from "node:worker_threads";
|
|
|
5
5
|
import { DEFAULT_GLOBAL_CONCURRENCY_LIMIT, Semaphore } from "../runs/shared/parallel-utils.ts";
|
|
6
6
|
import { HOST_STEP_MAX_COUNT } from "../runs/shared/host-step-status.ts";
|
|
7
7
|
import { classifyTaskMutationIntent } from "../runs/shared/task-intent.ts";
|
|
8
|
+
import { describeGateAcceptanceConflict } from "../runs/shared/acceptance.ts";
|
|
8
9
|
import type { AcceptanceRecoveryMetadata, HostStepNodeV1, SingleResult } from "../shared/types.ts";
|
|
9
10
|
import { normalizeWorkflowHostCommandParams, type WorkflowHostCommandParams, type WorkflowHostCommandResult } from "./host-command.ts";
|
|
10
11
|
|
|
11
12
|
const KEY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
13
|
+
function validGitRef(ref: unknown): ref is string {
|
|
14
|
+
if (typeof ref !== "string" || !ref || ref === "@" || Buffer.byteLength(ref, "utf-8") > 1024 || ref.startsWith("/") || ref.endsWith("/") || ref.includes("//") || ref.includes("..") || ref.includes("@{")) return false;
|
|
15
|
+
if (/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/iu.test(ref)) return false;
|
|
16
|
+
if (/[[\]\\~^:?*\u0000-\u0020\u007f]/u.test(ref) || ref.endsWith(".") || ref.endsWith(".lock")) return false;
|
|
17
|
+
return ref.split("/").every((component) => component.length > 0 && component !== "." && component !== ".." && !component.startsWith(".") && !component.endsWith(".") && !component.endsWith(".lock"));
|
|
18
|
+
}
|
|
12
19
|
const requireFromPackage = createRequire(import.meta.url);
|
|
20
|
+
const WORKFLOW_ASSEMBLY_FLUSH_TIMEOUT_MS = 5_000;
|
|
13
21
|
|
|
14
22
|
export interface WorkflowScriptValidationError {
|
|
15
23
|
message: string;
|
|
@@ -49,6 +57,12 @@ let suppressNativePromiseConsumption = 0;
|
|
|
49
57
|
const activeNativePromises = [];
|
|
50
58
|
const pending = new Map();
|
|
51
59
|
const runKeyPattern = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
60
|
+
function validGitRef(ref) {
|
|
61
|
+
if (typeof ref !== "string" || !ref || ref === "@" || new TextEncoder().encode(ref).length > 1024 || ref.startsWith("/") || ref.endsWith("/") || ref.includes("//") || ref.includes("..") || ref.includes("@{")) return false;
|
|
62
|
+
if (/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/iu.test(ref)) return false;
|
|
63
|
+
if (/[[\]\\~^:?*\u0000-\u0020\u007f]/u.test(ref) || ref.endsWith(".") || ref.endsWith(".lock")) return false;
|
|
64
|
+
return ref.split("/").every((component) => component.length > 0 && component !== "." && component !== ".." && !component.startsWith(".") && !component.endsWith(".") && !component.endsWith(".lock"));
|
|
65
|
+
}
|
|
52
66
|
const trackedPromiseTrackers = new WeakMap();
|
|
53
67
|
const trackedPromiseTargets = new WeakMap();
|
|
54
68
|
let nativePromiseTrackers = new WeakMap();
|
|
@@ -61,6 +75,12 @@ function stableRunJson(value) {
|
|
|
61
75
|
return JSON.stringify(value) ?? "undefined";
|
|
62
76
|
}
|
|
63
77
|
|
|
78
|
+
function canonicalRunParams(params) {
|
|
79
|
+
if (params.gate === undefined || params.acceptance !== false) return params;
|
|
80
|
+
const { acceptance: _acceptance, ...withoutAcceptance } = params;
|
|
81
|
+
return withoutAcceptance;
|
|
82
|
+
}
|
|
83
|
+
|
|
64
84
|
function isDirectWorkflowScriptPromiseHandlerCall() {
|
|
65
85
|
const stack = new Error().stack;
|
|
66
86
|
if (typeof stack !== "string") return false;
|
|
@@ -432,7 +452,7 @@ function validateLaneSpecs(laneSpecs) {
|
|
|
432
452
|
validateLaneStageBounds(validationParams, stageLabel);
|
|
433
453
|
validateRunCall(generatedKey, validationParams, stageLabel, validationFingerprints);
|
|
434
454
|
const existingFingerprint = runFingerprints.get(generatedKey);
|
|
435
|
-
if (existingFingerprint !== undefined && (resume === "previous" || existingFingerprint !== stableRunJson(params))) {
|
|
455
|
+
if (existingFingerprint !== undefined && (resume === "previous" || existingFingerprint !== stableRunJson(canonicalRunParams(params)))) {
|
|
436
456
|
throw new Error("runs.lanes generated child key '" + generatedKey + "' is already used with incompatible launch params.");
|
|
437
457
|
}
|
|
438
458
|
stages.push({ key: stageKey, generatedKey, resume, params });
|
|
@@ -601,6 +621,19 @@ function validateLaneMetadata(value, label, workflowKey) {
|
|
|
601
621
|
}
|
|
602
622
|
}
|
|
603
623
|
|
|
624
|
+
function describeGateAcceptanceConflict(gate, acceptance) {
|
|
625
|
+
const render = (value) => {
|
|
626
|
+
let encoded;
|
|
627
|
+
try {
|
|
628
|
+
encoded = JSON.stringify(value) ?? String(value);
|
|
629
|
+
} catch {
|
|
630
|
+
encoded = String(value);
|
|
631
|
+
}
|
|
632
|
+
return encoded.length > 120 ? encoded.slice(0, 120) + "..." : encoded;
|
|
633
|
+
};
|
|
634
|
+
return " Both fields were present: gate=" + render(gate) + " acceptance=" + render(acceptance) + ".";
|
|
635
|
+
}
|
|
636
|
+
|
|
604
637
|
function validateRunCall(key, params, label, fingerprints) {
|
|
605
638
|
if (typeof key !== "string" || !runKeyPattern.test(key)) throw new Error(label + " has an invalid key.");
|
|
606
639
|
if (hostKeys.has(key)) throw new Error("Workflow key '" + key + "' is already used by runs.host.");
|
|
@@ -611,9 +644,10 @@ function validateRunCall(key, params, label, fingerprints) {
|
|
|
611
644
|
}
|
|
612
645
|
if (Object.prototype.hasOwnProperty.call(params, "clarify")) throw new Error(label + " does not support clarify UI.");
|
|
613
646
|
if (params.worktree !== undefined && typeof params.worktree !== "boolean") throw new Error(label + " worktree must be true or false.");
|
|
647
|
+
if (params.baseRef !== undefined && (typeof params.baseRef !== "string" || !validGitRef(params.baseRef))) throw new Error(label + " baseRef must be a valid Git ref.");
|
|
614
648
|
validateLaneMetadata(params.lane, label + " lane", key);
|
|
615
649
|
if (params.gate !== undefined && (typeof params.gate !== "string" || !params.gate.trim())) throw new Error(label + " gate must be a non-empty command string.");
|
|
616
|
-
if (params.gate !== undefined && params.acceptance !== undefined) throw new Error(label + " gate cannot be combined with acceptance; use one gate command or acceptance.verify.");
|
|
650
|
+
if (params.gate !== undefined && params.acceptance !== undefined && params.acceptance !== false) throw new Error(label + " gate cannot be combined with acceptance; use one gate command or acceptance.verify." + describeGateAcceptanceConflict(params.gate, params.acceptance));
|
|
617
651
|
if (params.gate !== undefined && params.resume !== undefined) throw new Error(label + " gate is not supported with retained resume.");
|
|
618
652
|
if (params.extensionBindings !== undefined && params.resume !== undefined) throw new Error(label + " extensionBindings is not supported with retained resume; resume uses the original retained child binding.");
|
|
619
653
|
if (params.resume !== undefined && typeof params.resume !== "string") {
|
|
@@ -630,7 +664,7 @@ function validateRunCall(key, params, label, fingerprints) {
|
|
|
630
664
|
if (params.resume !== undefined && (typeof params.task !== "string" || !params.task.trim())) throw new Error(label + " resume requires a non-empty task follow-up.");
|
|
631
665
|
validateExtensionBindings(params.extensionBindings, label);
|
|
632
666
|
assertJsonValue(params, label + " params");
|
|
633
|
-
const fingerprint = stableRunJson(params);
|
|
667
|
+
const fingerprint = stableRunJson(canonicalRunParams(params));
|
|
634
668
|
const existing = fingerprints.get(key);
|
|
635
669
|
if (existing !== undefined && existing !== fingerprint) throw new Error("Duplicate workflow key '" + key + "' used with incompatible launch params.");
|
|
636
670
|
fingerprints.set(key, fingerprint);
|
|
@@ -1070,6 +1104,8 @@ export interface RunWorkflowScriptOptions {
|
|
|
1070
1104
|
oneUsePermit?: { claim: (key: string) => string | undefined };
|
|
1071
1105
|
timeoutMs?: number;
|
|
1072
1106
|
signal?: AbortSignal;
|
|
1107
|
+
/** Let an async workflow flush pure result assembly after reload once every child is terminal. */
|
|
1108
|
+
continueAfterAbortWhenChildrenSettled?: (abortError: Error) => boolean;
|
|
1073
1109
|
/** Maximum children executing concurrently within this workflow. Defaults to 20. */
|
|
1074
1110
|
globalConcurrencyLimit?: number;
|
|
1075
1111
|
admit?: (calls: Array<{ key: string; params: Record<string, unknown> }>) => void | Promise<void>;
|
|
@@ -1354,6 +1390,12 @@ function stableJson(value: unknown): string {
|
|
|
1354
1390
|
return JSON.stringify(value) ?? "undefined";
|
|
1355
1391
|
}
|
|
1356
1392
|
|
|
1393
|
+
function canonicalRunParams(params: Record<string, unknown>): Record<string, unknown> {
|
|
1394
|
+
if (params.gate === undefined || params.acceptance !== false) return params;
|
|
1395
|
+
const { acceptance: _acceptance, ...withoutAcceptance } = params;
|
|
1396
|
+
return withoutAcceptance;
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1357
1399
|
function validateKey(value: unknown, owner = "runs.run"): string {
|
|
1358
1400
|
if (typeof value !== "string" || !KEY_PATTERN.test(value)) {
|
|
1359
1401
|
throw new Error(`${owner} key must be 1-128 characters using letters, numbers, '.', '_' or '-', and start with a letter or number.`);
|
|
@@ -1614,7 +1656,7 @@ function resolveWorkflowParserEntry(): string {
|
|
|
1614
1656
|
}
|
|
1615
1657
|
}
|
|
1616
1658
|
|
|
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;
|
|
1659
|
+
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
1660
|
|
|
1619
1661
|
function isZeroUsage(usage: unknown): boolean {
|
|
1620
1662
|
if (!isRecord(usage)) return false;
|
|
@@ -1682,6 +1724,9 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1682
1724
|
let acceptanceRecoveryBarrier: { key: string } | undefined;
|
|
1683
1725
|
let settled = false;
|
|
1684
1726
|
let finishing = false;
|
|
1727
|
+
let assemblyAbortRequested = false;
|
|
1728
|
+
let assemblyFlushTimer: ReturnType<typeof setTimeout> | undefined;
|
|
1729
|
+
let abortError: Error | undefined;
|
|
1685
1730
|
|
|
1686
1731
|
const partial = (): Omit<WorkflowScriptResult, "value"> => ({ emits, console: consoleEntries, trace, children: childOrder.flatMap((key) => {
|
|
1687
1732
|
const child = children.get(key);
|
|
@@ -1750,6 +1795,10 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1750
1795
|
const finish = (outcome: { value: unknown } | { error: Error & { workflowErrorKind?: unknown } }) => {
|
|
1751
1796
|
if (settled || finishing) return;
|
|
1752
1797
|
finishing = true;
|
|
1798
|
+
if (assemblyFlushTimer !== undefined) {
|
|
1799
|
+
clearTimeout(assemblyFlushTimer);
|
|
1800
|
+
assemblyFlushTimer = undefined;
|
|
1801
|
+
}
|
|
1753
1802
|
childController.abort("error" in outcome ? outcome.error : new Error("Workflow script completed."));
|
|
1754
1803
|
void Promise.allSettled([...steers.values(), ...hostCalls.values()].map(({ promise }) => promise)).then(() => {
|
|
1755
1804
|
if (settled) return;
|
|
@@ -1781,6 +1830,26 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1781
1830
|
: typeof signalReason === "string"
|
|
1782
1831
|
? new Error(signalReason)
|
|
1783
1832
|
: new Error("Workflow script aborted.");
|
|
1833
|
+
if (finishing) return;
|
|
1834
|
+
abortError = error;
|
|
1835
|
+
const allChildrenSettled = launches.size > 0
|
|
1836
|
+
&& [...launches.keys()].every((key) => children.has(key));
|
|
1837
|
+
let mayFlushAssembly = false;
|
|
1838
|
+
try {
|
|
1839
|
+
mayFlushAssembly = options.continueAfterAbortWhenChildrenSettled?.(error) === true;
|
|
1840
|
+
} catch (callbackError) {
|
|
1841
|
+
const callbackMessage = callbackError instanceof Error ? callbackError.message : String(callbackError);
|
|
1842
|
+
return finish({ error: new Error(`Workflow assembly flush eligibility failed: ${callbackMessage}`) });
|
|
1843
|
+
}
|
|
1844
|
+
if (mayFlushAssembly && allChildrenSettled) {
|
|
1845
|
+
// A reloaded async workflow may already be past its last child launch.
|
|
1846
|
+
// Keep the worker alive for pure result assembly, but abort the child
|
|
1847
|
+
// signal so any later launch or side effect cannot use stale context.
|
|
1848
|
+
assemblyAbortRequested = true;
|
|
1849
|
+
childController.abort(error);
|
|
1850
|
+
assemblyFlushTimer = setTimeout(() => finish({ error }), WORKFLOW_ASSEMBLY_FLUSH_TIMEOUT_MS);
|
|
1851
|
+
return;
|
|
1852
|
+
}
|
|
1784
1853
|
for (const key of launches.keys()) {
|
|
1785
1854
|
if (children.has(key)) continue;
|
|
1786
1855
|
stoppedLaunches.add(key);
|
|
@@ -1873,6 +1942,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1873
1942
|
return;
|
|
1874
1943
|
}
|
|
1875
1944
|
if (message.type !== "call" || typeof message.callId !== "number" || typeof message.method !== "string" || !isRecord(message.args)) return;
|
|
1945
|
+
if (assemblyAbortRequested) return finish({ error: abortError ?? new Error("Workflow context was replaced or reloaded.") });
|
|
1876
1946
|
|
|
1877
1947
|
const respond = (promise: Promise<unknown>, responsePath?: string, onBoundaryError?: (error: unknown) => void) => {
|
|
1878
1948
|
void promise.then(
|
|
@@ -2038,7 +2108,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
2038
2108
|
}
|
|
2039
2109
|
return result;
|
|
2040
2110
|
});
|
|
2041
|
-
const fingerprint = stableJson(params);
|
|
2111
|
+
const fingerprint = stableJson(canonicalRunParams(params));
|
|
2042
2112
|
const existing = launches.get(key);
|
|
2043
2113
|
if (existing) {
|
|
2044
2114
|
if (existing.fingerprint !== fingerprint) return respond(Promise.reject(new Error(`Duplicate workflow key '${key}' used with incompatible launch params.`)));
|
|
@@ -2059,11 +2129,14 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
2059
2129
|
if (params.worktree !== undefined && typeof params.worktree !== "boolean") {
|
|
2060
2130
|
return respond(Promise.reject(new Error(`runs.run('${key}') worktree must be true or false.`)));
|
|
2061
2131
|
}
|
|
2132
|
+
if (params.baseRef !== undefined && (typeof params.baseRef !== "string" || !validGitRef(params.baseRef))) {
|
|
2133
|
+
return respond(Promise.reject(new Error(`runs.run('${key}') baseRef must be a valid Git ref.`)));
|
|
2134
|
+
}
|
|
2062
2135
|
if (params.gate !== undefined && (typeof params.gate !== "string" || !params.gate.trim())) {
|
|
2063
2136
|
return respond(Promise.reject(new Error(`runs.run('${key}') gate must be a non-empty command string.`)));
|
|
2064
2137
|
}
|
|
2065
|
-
if (params.gate !== undefined && params.acceptance !== undefined) {
|
|
2066
|
-
return respond(Promise.reject(new Error(`runs.run('${key}') gate cannot be combined with acceptance; use one gate command or acceptance.verify.`)));
|
|
2138
|
+
if (params.gate !== undefined && params.acceptance !== undefined && params.acceptance !== false) {
|
|
2139
|
+
return respond(Promise.reject(new Error(`runs.run('${key}') gate cannot be combined with acceptance; use one gate command or acceptance.verify.` + describeGateAcceptanceConflict(params.gate, params.acceptance))));
|
|
2067
2140
|
}
|
|
2068
2141
|
if (params.gate !== undefined && params.resume !== undefined) {
|
|
2069
2142
|
return respond(Promise.reject(new Error(`runs.run('${key}') gate is not supported with retained resume.`)));
|
|
@@ -402,11 +402,12 @@ export function formatWorkflowChecklistPhase(phase: WorkflowChecklistPhase): str
|
|
|
402
402
|
return counts.length ? `${phase.label} ${counts.join(" · ")}` : phase.label;
|
|
403
403
|
}
|
|
404
404
|
|
|
405
|
-
export function formatWorkflowChecklistBottleneck(item: WorkflowChecklistItem | undefined, options: { includeOutput?: boolean } = {}): string | undefined {
|
|
405
|
+
export function formatWorkflowChecklistBottleneck(item: WorkflowChecklistItem | undefined, options: { includeOutput?: boolean; includeError?: boolean } = {}): string | undefined {
|
|
406
406
|
if (!item) return undefined;
|
|
407
407
|
const identity = [item.label, item.agent && item.agent !== item.label ? item.agent : undefined].filter((value): value is string => Boolean(value)).join(" · ") || item.key;
|
|
408
408
|
const includeOutput = options.includeOutput ?? true;
|
|
409
|
-
const
|
|
409
|
+
const includeError = options.includeError ?? true;
|
|
410
|
+
const details = [item.context ? `(${item.context})` : undefined, item.currentTool ? `${item.currentTool}${item.durationMs !== undefined ? ` ${formatDurationText(item.durationMs)}` : ""}` : undefined, !item.currentTool && item.currentPath ? item.currentPath : undefined, !item.currentTool && item.durationMs !== undefined ? formatDurationText(item.durationMs) : undefined, item.toolCount !== undefined ? `${item.toolCount} tools` : undefined, includeOutput && item.outputName ? `out:${item.outputName}` : undefined, includeError && item.error ? `error:${item.error.replace(/\bOutput:/g, "output:")}` : undefined].filter((value): value is string => Boolean(value));
|
|
410
411
|
return [identity, ...details].join(" · ");
|
|
411
412
|
}
|
|
412
413
|
|
|
@@ -433,7 +434,7 @@ export function formatWorkflowChecklistText(projection: WorkflowChecklistProject
|
|
|
433
434
|
lines.push(`${indent} ${marker} ${formatWorkflowChecklistItem(item)}`);
|
|
434
435
|
}
|
|
435
436
|
}
|
|
436
|
-
const bottleneck = formatWorkflowChecklistBottleneck(projection.bottleneck);
|
|
437
|
+
const bottleneck = formatWorkflowChecklistBottleneck(projection.bottleneck, { includeError: options.includeItems === false });
|
|
437
438
|
if (bottleneck) lines.push(`${indent} bottleneck · ${bottleneck}`);
|
|
438
439
|
return lines;
|
|
439
440
|
}
|
|
@@ -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
|
-
}
|