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
|
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { splitKnownThinkingSuffix } from "../../shared/model-info.ts";
|
|
4
4
|
import { TEMP_ROOT_DIR } from "../../shared/types.ts";
|
|
5
|
+
import { getAgentDir } from "../../shared/utils.ts";
|
|
5
6
|
|
|
6
7
|
export const EXCLUSIONS_PATH_ENV = "PI_MODEL_EXCLUSIONS_PATH";
|
|
7
8
|
|
|
@@ -30,6 +31,43 @@ let loadedTTLCeilingMs: number | undefined;
|
|
|
30
31
|
let persistTimer: ReturnType<typeof setTimeout> | null = null;
|
|
31
32
|
let persistSeq = 0;
|
|
32
33
|
|
|
34
|
+
const AUTH_FAILURE_PATTERNS = [
|
|
35
|
+
/auth(?:entication)?/i,
|
|
36
|
+
/unauthori[sz]ed/i,
|
|
37
|
+
/forbidden/i,
|
|
38
|
+
/api key/i,
|
|
39
|
+
/token expired/i,
|
|
40
|
+
/invalid key/i,
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
function isAuthModelExclusion(entry: ModelExclusion): boolean {
|
|
44
|
+
const reason = entry.reason;
|
|
45
|
+
return typeof reason === "string" && AUTH_FAILURE_PATTERNS.some((pattern) => pattern.test(reason));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getAuthStoreMtimeMs(): number | undefined {
|
|
49
|
+
const authStorePath = path.join(getAgentDir(), "auth.json");
|
|
50
|
+
try {
|
|
51
|
+
const stats = fs.statSync(authStorePath);
|
|
52
|
+
return stats.isFile() && Number.isFinite(stats.mtimeMs) ? stats.mtimeMs : undefined;
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
55
|
+
console.error(`[model-exclusions] Failed to stat Pi auth store at ${authStorePath}; preserving auth-related exclusions:`, error);
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function invalidateAuthExclusions(): void {
|
|
62
|
+
if (!exclusions.some(isAuthModelExclusion)) return;
|
|
63
|
+
const authStoreMtimeMs = getAuthStoreMtimeMs();
|
|
64
|
+
if (authStoreMtimeMs === undefined) return;
|
|
65
|
+
const retained = exclusions.filter((entry) => !isAuthModelExclusion(entry) || authStoreMtimeMs <= entry.recordedAt);
|
|
66
|
+
if (retained.length === exclusions.length) return;
|
|
67
|
+
exclusions = retained;
|
|
68
|
+
schedulePersist();
|
|
69
|
+
}
|
|
70
|
+
|
|
33
71
|
/**
|
|
34
72
|
* Override the default TTL applied to newly recorded model exclusions.
|
|
35
73
|
*
|
|
@@ -95,23 +133,23 @@ function ensureLoaded(): void {
|
|
|
95
133
|
const data = JSON.parse(raw);
|
|
96
134
|
if (data.version === 1) {
|
|
97
135
|
if (!Array.isArray(data.exclusions)) throw new Error("Model exclusion store version 1 must contain an exclusions array.");
|
|
98
|
-
for (let index = 0; index < data.exclusions.length; index++) {
|
|
99
|
-
const entry = data.exclusions[index];
|
|
100
|
-
if (!entry || typeof entry !== "object" || Array.isArray(entry)) throw new Error(`Model exclusion store entry ${index} must be an object.`);
|
|
101
|
-
if (entry.reason !== undefined && typeof entry.reason !== "string") throw new Error(`Model exclusion store entry ${index} has an invalid reason.`);
|
|
102
|
-
for (const field of ["recordedAt", "expiresAt"] as const) {
|
|
103
|
-
const timestamp = entry[field];
|
|
104
|
-
if (typeof timestamp !== "number" || !Number.isFinite(timestamp) || timestamp <= 0 || timestamp > MAX_DATE_TIMESTAMP_MS) {
|
|
105
|
-
throw new Error(`Model exclusion store entry ${index} has an invalid ${field}.`);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
136
|
const now = Date.now();
|
|
110
|
-
|
|
137
|
+
let droppedInvalid = false;
|
|
138
|
+
exclusions = data.exclusions.flatMap((entry: unknown, index: number) => {
|
|
139
|
+
const result = readPersistedExclusion(entry, index);
|
|
140
|
+
if (!result.ok) {
|
|
141
|
+
droppedInvalid = true;
|
|
142
|
+
console.warn(`[model-exclusions] Ignoring invalid exclusion: ${result.message}`);
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
return [result.exclusion];
|
|
146
|
+
}).filter((e: ModelExclusion) => e.expiresAt > now);
|
|
111
147
|
const shortened = loadedTTLCeilingMs !== undefined && shortenExclusionsToTTL(exclusions, loadedTTLCeilingMs, now);
|
|
112
148
|
exclusions = deduplicate(exclusions);
|
|
113
|
-
if (
|
|
149
|
+
if (droppedInvalid) flushPersist();
|
|
150
|
+
else if (shortened) schedulePersist();
|
|
114
151
|
}
|
|
152
|
+
invalidateAuthExclusions();
|
|
115
153
|
} catch (error) {
|
|
116
154
|
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
117
155
|
console.error(`[model-exclusions] Failed to load exclusions from ${getExclusionsFilePath()}:`, error);
|
|
@@ -119,6 +157,32 @@ function ensureLoaded(): void {
|
|
|
119
157
|
}
|
|
120
158
|
}
|
|
121
159
|
|
|
160
|
+
type PersistedExclusionRead =
|
|
161
|
+
| { ok: true; exclusion: ModelExclusion }
|
|
162
|
+
| { ok: false; message: string };
|
|
163
|
+
|
|
164
|
+
function invalidPersistedExclusion(index: number, message: string): PersistedExclusionRead {
|
|
165
|
+
return { ok: false, message: `Model exclusion store entry ${index} ${message}.` };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function readPersistedExclusion(entry: unknown, index: number): PersistedExclusionRead {
|
|
169
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return invalidPersistedExclusion(index, "must be an object");
|
|
170
|
+
const candidate = entry as { modelId?: unknown; provider?: unknown; reason?: unknown; recordedAt?: unknown; expiresAt?: unknown };
|
|
171
|
+
const { modelId, provider, reason } = candidate;
|
|
172
|
+
if (modelId !== undefined && (typeof modelId !== "string" || modelId.length === 0)) return invalidPersistedExclusion(index, "has an invalid modelId");
|
|
173
|
+
if (provider !== undefined && (typeof provider !== "string" || provider.length === 0)) return invalidPersistedExclusion(index, "has an invalid provider");
|
|
174
|
+
if (reason !== undefined && typeof reason !== "string") return invalidPersistedExclusion(index, "has an invalid reason");
|
|
175
|
+
const { recordedAt, expiresAt } = candidate;
|
|
176
|
+
if (typeof recordedAt !== "number" || !Number.isFinite(recordedAt) || recordedAt <= 0 || recordedAt > MAX_DATE_TIMESTAMP_MS) return invalidPersistedExclusion(index, "has an invalid recordedAt");
|
|
177
|
+
if (typeof expiresAt !== "number" || !Number.isFinite(expiresAt) || expiresAt <= 0 || expiresAt > MAX_DATE_TIMESTAMP_MS) return invalidPersistedExclusion(index, "has an invalid expiresAt");
|
|
178
|
+
const metadata = { ...(reason === undefined ? {} : { reason }), recordedAt, expiresAt };
|
|
179
|
+
if (modelId === undefined) {
|
|
180
|
+
if (provider === undefined) return invalidPersistedExclusion(index, "must include modelId or provider");
|
|
181
|
+
return { ok: true, exclusion: { provider, ...metadata } };
|
|
182
|
+
}
|
|
183
|
+
return { ok: true, exclusion: { modelId, ...(provider === undefined ? {} : { provider }), ...metadata } };
|
|
184
|
+
}
|
|
185
|
+
|
|
122
186
|
function dedupKey(entry: ModelExclusion): string {
|
|
123
187
|
return `${entry.provider ?? ""}|${entry.modelId ?? ""}`;
|
|
124
188
|
}
|
|
@@ -165,6 +229,7 @@ export function recordModelFailure(options: RecordModelFailureOptions): void {
|
|
|
165
229
|
*/
|
|
166
230
|
export function clearExpiredExclusions(): void {
|
|
167
231
|
ensureLoaded();
|
|
232
|
+
invalidateAuthExclusions();
|
|
168
233
|
prune(exclusions, Date.now());
|
|
169
234
|
schedulePersist();
|
|
170
235
|
}
|
|
@@ -190,7 +255,7 @@ export function clearExclusions(): void {
|
|
|
190
255
|
*/
|
|
191
256
|
function entryMatches(entry: ModelExclusion, candidateModelId: string, candidateProvider: string | undefined, now: number): boolean {
|
|
192
257
|
if (entry.expiresAt <= now) return false;
|
|
193
|
-
if (entry.modelId) {
|
|
258
|
+
if (entry.modelId !== undefined) {
|
|
194
259
|
if (entry.modelId !== candidateModelId) return false;
|
|
195
260
|
return !entry.provider || !candidateProvider || entry.provider === candidateProvider;
|
|
196
261
|
}
|
|
@@ -202,6 +267,7 @@ function entryMatches(entry: ModelExclusion, candidateModelId: string, candidate
|
|
|
202
267
|
*/
|
|
203
268
|
export function isExcluded(modelId: string, provider: string): boolean {
|
|
204
269
|
ensureLoaded();
|
|
270
|
+
invalidateAuthExclusions();
|
|
205
271
|
return exclusions.some((entry) => entryMatches(entry, modelId, provider, Date.now()));
|
|
206
272
|
}
|
|
207
273
|
|
|
@@ -213,6 +279,7 @@ export function isExcluded(modelId: string, provider: string): boolean {
|
|
|
213
279
|
*/
|
|
214
280
|
export function findModelExclusion(fullId: string, now = Date.now()): Readonly<ModelExclusion> | undefined {
|
|
215
281
|
ensureLoaded();
|
|
282
|
+
invalidateAuthExclusions();
|
|
216
283
|
const { provider, modelId } = parseModelKey(fullId);
|
|
217
284
|
return exclusions.find((entry) => entryMatches(entry, modelId, provider, now));
|
|
218
285
|
}
|
|
@@ -250,15 +317,17 @@ export function parseModelKey(fullId: string): { provider?: string; modelId: str
|
|
|
250
317
|
export function filterFallbackCandidates(candidates: string[], opts?: {
|
|
251
318
|
now?: number;
|
|
252
319
|
onExcluded?: (candidate: string, exclusion: Readonly<ModelExclusion>) => void;
|
|
320
|
+
ignoreExclusion?: (candidate: string, exclusion: Readonly<ModelExclusion>) => boolean;
|
|
253
321
|
}): string[] {
|
|
254
322
|
ensureLoaded();
|
|
323
|
+
invalidateAuthExclusions();
|
|
255
324
|
const timestamp = opts?.now ?? Date.now();
|
|
256
325
|
const seen = new Set<string>();
|
|
257
326
|
const filtered: string[] = [];
|
|
258
327
|
for (const raw of candidates) {
|
|
259
328
|
if (!raw || seen.has(raw)) continue;
|
|
260
329
|
const { provider: candidateProvider, modelId: candidateModelId } = parseModelKey(raw);
|
|
261
|
-
const exclusion = exclusions.find((entry) => entryMatches(entry, candidateModelId, candidateProvider, timestamp));
|
|
330
|
+
const exclusion = exclusions.find((entry) => entryMatches(entry, candidateModelId, candidateProvider, timestamp) && opts?.ignoreExclusion?.(raw, entry) !== true);
|
|
262
331
|
if (exclusion) {
|
|
263
332
|
opts?.onExcluded?.(raw, exclusion);
|
|
264
333
|
continue;
|
|
@@ -18,9 +18,17 @@ export function splitThinkingSuffix(model: string): { baseModel: string; thinkin
|
|
|
18
18
|
return splitKnownThinkingSuffix(model);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
/** Aliases apply only to the resolved launch candidate (without its thinking suffix) and the exact raw response ID. */
|
|
22
|
+
export function formatSubagentModelVerificationError(
|
|
23
|
+
expectedModel: string,
|
|
24
|
+
observedModel: string,
|
|
25
|
+
availableModels: AvailableModelInfo[] | undefined,
|
|
26
|
+
modelResponseAliases?: Record<string, string[]>,
|
|
27
|
+
): string | undefined {
|
|
22
28
|
if (!availableModels || availableModels.length === 0) return undefined;
|
|
23
29
|
const expectedBase = splitThinkingSuffix(expectedModel).baseModel;
|
|
30
|
+
if (modelResponseAliases && Object.hasOwn(modelResponseAliases, expectedBase)
|
|
31
|
+
&& modelResponseAliases[expectedBase]?.includes(observedModel)) return undefined;
|
|
24
32
|
const observedBase = splitThinkingSuffix(observedModel).baseModel;
|
|
25
33
|
if (expectedBase === observedBase) return undefined;
|
|
26
34
|
const expectedEntry = availableModels.find((entry) => entry.fullId === expectedBase);
|
|
@@ -284,6 +292,49 @@ function enforceModelScopes(
|
|
|
284
292
|
for (const violation of violations) (onWarn ?? defaultScopeWarn)(violation);
|
|
285
293
|
}
|
|
286
294
|
|
|
295
|
+
const MODEL_EXCLUSION_DIAGNOSTIC_MAX_LENGTH = 240;
|
|
296
|
+
const MODEL_EXCLUSION_DIAGNOSTIC_MAX_ENTRIES = 20;
|
|
297
|
+
|
|
298
|
+
function sanitizeModelExclusionDiagnostic(value: string | undefined, fallback: string): string {
|
|
299
|
+
const normalized = typeof value === "string"
|
|
300
|
+
? value.replace(/[\u0000-\u001f\u007f\u2028\u2029]+/g, " ").trim()
|
|
301
|
+
: "";
|
|
302
|
+
return redactSecretValues(normalized || fallback).slice(0, MODEL_EXCLUSION_DIAGNOSTIC_MAX_LENGTH);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function formatModelExclusionExpiry(expiresAt: number): string {
|
|
306
|
+
if (!Number.isFinite(expiresAt)) return "unknown";
|
|
307
|
+
const date = new Date(expiresAt);
|
|
308
|
+
return Number.isNaN(date.getTime()) ? "unknown" : date.toISOString();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function formatExcludedCandidateEvidence(candidate: string, exclusion: NonNullable<ReturnType<typeof findModelExclusion>>): string {
|
|
312
|
+
const { provider, modelId } = parseModelKey(candidate);
|
|
313
|
+
const displayCandidate = sanitizeModelExclusionDiagnostic(candidate, "unknown");
|
|
314
|
+
const displayModel = sanitizeModelExclusionDiagnostic(modelId, "unknown");
|
|
315
|
+
const displayProvider = sanitizeModelExclusionDiagnostic(provider ?? exclusion.provider, "unspecified");
|
|
316
|
+
const reason = sanitizeModelExclusionDiagnostic(exclusion.reason, "runtime-failure");
|
|
317
|
+
return `${displayCandidate} — model: ${displayModel}; provider: ${displayProvider}; reason: ${reason}; expires: ${formatModelExclusionExpiry(exclusion.expiresAt)}`;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const MODEL_UNAVAILABLE_EXCLUSION_PATTERNS = [
|
|
321
|
+
/model.*not found/i,
|
|
322
|
+
/unknown model/i,
|
|
323
|
+
/model.*unavailable/i,
|
|
324
|
+
/model.*disabled/i,
|
|
325
|
+
];
|
|
326
|
+
|
|
327
|
+
function isCurrentRegistryModel(candidate: string, availableModels: AvailableModelInfo[] | undefined): boolean {
|
|
328
|
+
if (!availableModels || availableModels.length === 0) return false;
|
|
329
|
+
const { baseModel } = splitThinkingSuffix(candidate);
|
|
330
|
+
return availableModels.some((entry) => entry.fullId === baseModel);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function ignoreStaleModelUnavailableExclusion(candidate: string, exclusion: NonNullable<ReturnType<typeof findModelExclusion>>, availableModels: AvailableModelInfo[] | undefined): boolean {
|
|
334
|
+
const reason = exclusion.reason ?? "";
|
|
335
|
+
return MODEL_UNAVAILABLE_EXCLUSION_PATTERNS.some((pattern) => pattern.test(reason)) && isCurrentRegistryModel(candidate, availableModels);
|
|
336
|
+
}
|
|
337
|
+
|
|
287
338
|
function throwForExplicitModelExclusion(model: string): void {
|
|
288
339
|
const exclusion = findModelExclusion(model);
|
|
289
340
|
if (!exclusion) return;
|
|
@@ -419,9 +470,15 @@ export function buildModelCandidates(
|
|
|
419
470
|
if (!primaryModel) throwForUnresolvedEnforcedInheritScope(options?.scope, true);
|
|
420
471
|
const origin = options?.origin ?? (options?.primaryModelFromParent ? "inherited" : "configured");
|
|
421
472
|
const scopes = configuredScopes(options?.scope);
|
|
473
|
+
type ExcludedCandidate = { candidate: string; exclusion: NonNullable<ReturnType<typeof findModelExclusion>> };
|
|
474
|
+
const excludedCandidates: ExcludedCandidate[] = [];
|
|
475
|
+
let excludedCandidateCount = 0;
|
|
422
476
|
const warnCachedExclusion = (candidate: string, exclusion: NonNullable<ReturnType<typeof findModelExclusion>>) => {
|
|
423
|
-
|
|
424
|
-
|
|
477
|
+
excludedCandidateCount++;
|
|
478
|
+
if (excludedCandidates.length < MODEL_EXCLUSION_DIAGNOSTIC_MAX_ENTRIES) excludedCandidates.push({ candidate, exclusion });
|
|
479
|
+
const displayCandidate = sanitizeModelExclusionDiagnostic(candidate, "unknown");
|
|
480
|
+
const reason = sanitizeModelExclusionDiagnostic(exclusion.reason, "runtime-failure");
|
|
481
|
+
console.warn(`[pi-subagents] Skipping model '${displayCandidate}' due to a cached exclusion (reason: ${reason}; expires: ${formatModelExclusionExpiry(exclusion.expiresAt)}).`);
|
|
425
482
|
};
|
|
426
483
|
if (origin === "explicit" && primaryModel) {
|
|
427
484
|
const normalized = resolveRequiredSubagentModelCandidate(primaryModel.trim(), availableModels, preferredProvider);
|
|
@@ -433,6 +490,7 @@ export function buildModelCandidates(
|
|
|
433
490
|
const candidates: string[] = [];
|
|
434
491
|
const rawCandidates = [primaryModel, ...(fallbackModels ?? [])];
|
|
435
492
|
let skippedPrimary: string | undefined;
|
|
493
|
+
let skippedFallback: string | undefined;
|
|
436
494
|
for (let index = 0; index < rawCandidates.length; index++) {
|
|
437
495
|
const raw = rawCandidates[index];
|
|
438
496
|
if (!raw) continue;
|
|
@@ -442,7 +500,10 @@ export function buildModelCandidates(
|
|
|
442
500
|
: resolveSubagentModelCandidate(model, availableModels, preferredProvider);
|
|
443
501
|
if (!normalized) {
|
|
444
502
|
if (index === 0) skippedPrimary = model;
|
|
445
|
-
else
|
|
503
|
+
else {
|
|
504
|
+
skippedFallback ??= model;
|
|
505
|
+
console.warn(`[pi-subagents] Skipping fallback model '${model}' because it is unavailable in this environment.`);
|
|
506
|
+
}
|
|
446
507
|
continue;
|
|
447
508
|
}
|
|
448
509
|
if (seen.has(normalized)) continue;
|
|
@@ -452,10 +513,21 @@ export function buildModelCandidates(
|
|
|
452
513
|
seen.add(normalized);
|
|
453
514
|
candidates.push(normalized);
|
|
454
515
|
}
|
|
455
|
-
const resolved = filterFallbackCandidates(candidates, {
|
|
516
|
+
const resolved = filterFallbackCandidates(candidates, {
|
|
517
|
+
onExcluded: warnCachedExclusion,
|
|
518
|
+
ignoreExclusion: (candidate, exclusion) => ignoreStaleModelUnavailableExclusion(candidate, exclusion, availableModels),
|
|
519
|
+
});
|
|
456
520
|
if (resolved.length === 0) {
|
|
457
521
|
if (skippedPrimary) resolveRequiredSubagentModelCandidate(skippedPrimary, availableModels, preferredProvider);
|
|
458
|
-
if (candidates.length
|
|
522
|
+
if (candidates.length === 0 && skippedFallback) resolveRequiredSubagentModelCandidate(skippedFallback, availableModels, preferredProvider);
|
|
523
|
+
if (candidates.length > 0) {
|
|
524
|
+
const shownExclusions = excludedCandidates;
|
|
525
|
+
const omittedExclusions = excludedCandidateCount - shownExclusions.length;
|
|
526
|
+
const evidence = shownExclusions.length > 0
|
|
527
|
+
? ` (excluded: ${shownExclusions.map(({ candidate, exclusion }) => formatExcludedCandidateEvidence(candidate, exclusion)).join("; ")}${omittedExclusions > 0 ? `; ... and ${omittedExclusions} more` : ""})`
|
|
528
|
+
: "";
|
|
529
|
+
throw new Error(`${ZERO_USABLE_MODEL_CANDIDATES_ERROR}${evidence}`);
|
|
530
|
+
}
|
|
459
531
|
return resolved;
|
|
460
532
|
}
|
|
461
533
|
if (skippedPrimary) {
|
|
@@ -16,17 +16,7 @@ import {
|
|
|
16
16
|
type SubagentRunMode,
|
|
17
17
|
type SubagentState,
|
|
18
18
|
} from "../../shared/types.ts";
|
|
19
|
-
import { isSafeNestedPathId,
|
|
20
|
-
import {
|
|
21
|
-
SUBAGENT_PARENT_CAPABILITY_TOKEN_ENV,
|
|
22
|
-
SUBAGENT_PARENT_CHILD_INDEX_ENV,
|
|
23
|
-
SUBAGENT_PARENT_CONTROL_INBOX_ENV,
|
|
24
|
-
SUBAGENT_PARENT_DEPTH_ENV,
|
|
25
|
-
SUBAGENT_PARENT_EVENT_SINK_ENV,
|
|
26
|
-
SUBAGENT_PARENT_PATH_ENV,
|
|
27
|
-
SUBAGENT_PARENT_ROOT_RUN_ID_ENV,
|
|
28
|
-
SUBAGENT_PARENT_RUN_ID_ENV,
|
|
29
|
-
} from "./pi-args.ts";
|
|
19
|
+
import { isSafeNestedPathId, sanitizeNestedPath, type NestedPathEntry } from "./nested-path.ts";
|
|
30
20
|
import { writeAtomicJson } from "../../shared/atomic-json.ts";
|
|
31
21
|
import { sanitizeProcessTerminal } from "../background/process-terminal.ts";
|
|
32
22
|
import { THINKING_LEVELS } from "../../shared/model-info.ts";
|
|
@@ -141,13 +131,9 @@ export function createNestedRoute(rootRunId: string): NestedRoute {
|
|
|
141
131
|
return { rootRunId, eventSink, controlInbox, capabilityToken };
|
|
142
132
|
}
|
|
143
133
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const
|
|
147
|
-
const controlInbox = env[SUBAGENT_PARENT_CONTROL_INBOX_ENV];
|
|
148
|
-
const capabilityToken = env[SUBAGENT_PARENT_CAPABILITY_TOKEN_ENV];
|
|
149
|
-
if (!rootRunId || !eventSink || !controlInbox || !capabilityToken) return undefined;
|
|
150
|
-
const route = { rootRunId, eventSink, controlInbox, capabilityToken };
|
|
134
|
+
/** Validate a route handed to an in-process child against its on-disk metadata. */
|
|
135
|
+
export function resolveNestedRoute(route: NestedRoute): NestedRoute {
|
|
136
|
+
const { rootRunId, capabilityToken } = route;
|
|
151
137
|
validateRouteShape(route);
|
|
152
138
|
const routeFile = path.join(commonRouteRoot(route), ROUTE_FILE);
|
|
153
139
|
const metadata = JSON.parse(fs.readFileSync(routeFile, "utf-8")) as { rootRunId?: unknown; capabilityToken?: unknown };
|
|
@@ -157,24 +143,38 @@ export function resolveNestedRouteFromEnv(env: NodeJS.ProcessEnv = process.env):
|
|
|
157
143
|
return route;
|
|
158
144
|
}
|
|
159
145
|
|
|
160
|
-
export function
|
|
146
|
+
export function resolveInheritedNestedRoute(route: NestedRoute): NestedRoute | undefined {
|
|
161
147
|
try {
|
|
162
|
-
return
|
|
148
|
+
return resolveNestedRoute(route);
|
|
163
149
|
} catch (error) {
|
|
164
150
|
console.error("Ignoring invalid nested subagent event route:", error);
|
|
165
151
|
return undefined;
|
|
166
152
|
}
|
|
167
153
|
}
|
|
168
154
|
|
|
169
|
-
export
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
155
|
+
export interface NestedParentAddress {
|
|
156
|
+
parentRunId: string;
|
|
157
|
+
parentStepIndex?: number;
|
|
158
|
+
depth: number;
|
|
159
|
+
path: NestedPathEntry[];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** The nested route an executor inherited from its own child runtime, validated against disk. */
|
|
163
|
+
export function inheritedNestedRouteOf(runtime: { nestedRoute?: NestedRoute } | undefined): NestedRoute | undefined {
|
|
164
|
+
return runtime?.nestedRoute ? resolveInheritedNestedRoute(runtime.nestedRoute) : undefined;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** The executor's own position in the nested run tree, from its child runtime. */
|
|
168
|
+
export function inheritedNestedParentAddressOf(runtime: { nestedParent?: { parentRunId: string; parentChildIndex?: number; depth: number; path: NestedPathEntry[] } } | undefined): NestedParentAddress | undefined {
|
|
169
|
+
const parent = runtime?.nestedParent;
|
|
170
|
+
if (!parent || !isSafeNestedId(parent.parentRunId)) return undefined;
|
|
171
|
+
const depth = Math.min(Math.max(1, clampNumber(parent.depth) ?? 1), MAX_DEPTH);
|
|
172
|
+
return {
|
|
173
|
+
parentRunId: parent.parentRunId,
|
|
174
|
+
...(parent.parentChildIndex !== undefined ? { parentStepIndex: parent.parentChildIndex } : {}),
|
|
175
|
+
depth,
|
|
176
|
+
path: parent.path.length ? parent.path : [{ runId: parent.parentRunId, ...(parent.parentChildIndex !== undefined ? { stepIndex: parent.parentChildIndex } : {}) }],
|
|
177
|
+
};
|
|
178
178
|
}
|
|
179
179
|
|
|
180
180
|
export function resolveNestedAsyncDir(rootRunId: string, run: NestedRunSummary): string | undefined {
|
|
@@ -286,7 +286,7 @@ function sanitizeTurnBudget(value: unknown): TurnBudgetState | undefined {
|
|
|
286
286
|
}
|
|
287
287
|
|
|
288
288
|
function sanitizeState(value: unknown, fallback: NestedRunState): NestedRunState {
|
|
289
|
-
return value === "queued" || value === "running" || value === "complete" || value === "failed" || value === "partial" || value === "paused" || value === "stopped"
|
|
289
|
+
return value === "queued" || value === "running" || value === "complete" || value === "failed" || value === "partial" || value === "paused" || value === "stopped" || value === "rejected"
|
|
290
290
|
? value
|
|
291
291
|
: fallback;
|
|
292
292
|
}
|
|
@@ -296,7 +296,7 @@ function sanitizeStep(input: unknown, depth: number): NestedStepSummary | undefi
|
|
|
296
296
|
const raw = input as Record<string, unknown>;
|
|
297
297
|
const agent = stringValue(raw.agent, 128);
|
|
298
298
|
if (!agent) return undefined;
|
|
299
|
-
const status = raw.status === "pending" || raw.status === "running" || raw.status === "complete" || raw.status === "completed" || raw.status === "failed" || raw.status === "paused" || raw.status === "stopped"
|
|
299
|
+
const status = raw.status === "pending" || raw.status === "running" || raw.status === "complete" || raw.status === "completed" || raw.status === "failed" || raw.status === "partial" || raw.status === "paused" || raw.status === "stopped" || raw.status === "rejected"
|
|
300
300
|
? raw.status
|
|
301
301
|
: "pending";
|
|
302
302
|
const model = stringValue(raw.model);
|
|
@@ -438,7 +438,7 @@ export function parseNestedEventRecords(content: string, route: NestedRoute): Ne
|
|
|
438
438
|
}
|
|
439
439
|
|
|
440
440
|
function terminal(state: NestedRunState): boolean {
|
|
441
|
-
return state === "complete" || state === "failed" || state === "partial" || state === "paused" || state === "stopped";
|
|
441
|
+
return state === "complete" || state === "failed" || state === "partial" || state === "paused" || state === "rejected" || state === "stopped";
|
|
442
442
|
}
|
|
443
443
|
|
|
444
444
|
function mergeBoundedChildren(existing: NestedRunSummary[] | undefined, incoming: NestedRunSummary[] | undefined): NestedRunSummary[] | undefined {
|
|
@@ -947,15 +947,6 @@ export function readNestedControlResults(route: NestedRoute): NestedControlResul
|
|
|
947
947
|
.flatMap((entry) => readControlResultsFromFile(route, path.join(route.eventSink, entry)));
|
|
948
948
|
}
|
|
949
949
|
|
|
950
|
-
export function nestedRouteEnv(route: NestedRoute): Record<string, string> {
|
|
951
|
-
return {
|
|
952
|
-
[SUBAGENT_PARENT_EVENT_SINK_ENV]: route.eventSink,
|
|
953
|
-
[SUBAGENT_PARENT_CONTROL_INBOX_ENV]: route.controlInbox,
|
|
954
|
-
[SUBAGENT_PARENT_ROOT_RUN_ID_ENV]: route.rootRunId,
|
|
955
|
-
[SUBAGENT_PARENT_CAPABILITY_TOKEN_ENV]: route.capabilityToken,
|
|
956
|
-
};
|
|
957
|
-
}
|
|
958
|
-
|
|
959
950
|
export function attachRootChildrenToSteps<T extends { children?: NestedRunSummary[]; index?: number }>(rootRunId: string, steps: T[] | undefined, children: NestedRunSummary[] | undefined): void {
|
|
960
951
|
if (!steps?.length) return;
|
|
961
952
|
for (const step of steps) {
|
|
@@ -1067,13 +1058,6 @@ export function nestedSummaryFromAsyncStatus(status: AsyncStatus, asyncDir: stri
|
|
|
1067
1058
|
};
|
|
1068
1059
|
}
|
|
1069
1060
|
|
|
1070
|
-
export function nestedArtifactEnv(rootRunId: string, parentRunId: string): Record<string, string> {
|
|
1071
|
-
return {
|
|
1072
|
-
PI_SUBAGENT_NESTED_ROOT_RUN_ID: rootRunId,
|
|
1073
|
-
PI_SUBAGENT_NESTED_PARENT_RUN_ID: parentRunId,
|
|
1074
|
-
};
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
1061
|
export function isTopLevelAsyncDir(asyncDir: string): boolean {
|
|
1078
1062
|
const resolved = path.resolve(asyncDir);
|
|
1079
1063
|
return containedPath(DIRS.async, resolved) && !containedPath(path.join(TEMP_ROOT_DIR, "nested-subagent-runs"), resolved);
|
|
@@ -36,17 +36,3 @@ export function sanitizeNestedPath(value: unknown): NestedPathEntry[] {
|
|
|
36
36
|
};
|
|
37
37
|
}).filter((part): part is NestedPathEntry => Boolean(part)).slice(0, MAX_NESTED_PATH_ENTRIES);
|
|
38
38
|
}
|
|
39
|
-
|
|
40
|
-
export function parseNestedPathEnv(value: string | undefined): NestedPathEntry[] {
|
|
41
|
-
if (!value) return [];
|
|
42
|
-
try {
|
|
43
|
-
return sanitizeNestedPath(JSON.parse(value) as unknown);
|
|
44
|
-
} catch {
|
|
45
|
-
return [];
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function encodeNestedPathEnv(value: NestedPathEntry[]): string {
|
|
50
|
-
const sanitized = sanitizeNestedPath(value);
|
|
51
|
-
return sanitized.length ? JSON.stringify(sanitized) : "";
|
|
52
|
-
}
|
|
@@ -56,7 +56,8 @@ export interface OrcaProgressTab {
|
|
|
56
56
|
append(text: string): void;
|
|
57
57
|
section(input: { agent: string; index: number; count: number }): void;
|
|
58
58
|
event(event: { type?: string; message?: Message; toolName?: string; args?: unknown }): void;
|
|
59
|
-
|
|
59
|
+
/** Resolves once the mirrored log and its done marker are on disk, so a host may exit afterwards. */
|
|
60
|
+
finish(status: "completed" | "failed" | "stopped", sessionFile?: string): Promise<void>;
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
function executableFile(candidate: string): boolean {
|
|
@@ -475,7 +476,7 @@ export function createOrcaProgressTab(input: {
|
|
|
475
476
|
}
|
|
476
477
|
},
|
|
477
478
|
finish(status, sessionFile) {
|
|
478
|
-
if (!available || finished) return;
|
|
479
|
+
if (!available || finished) return Promise.resolve();
|
|
479
480
|
finished = true;
|
|
480
481
|
const sessionId = status === "completed" ? resolvePiSessionId(sessionFile) : undefined;
|
|
481
482
|
let verifiedSessionFile: string | undefined;
|
|
@@ -488,11 +489,15 @@ export function createOrcaProgressTab(input: {
|
|
|
488
489
|
const truncation = truncated ? `\n[progress mirror truncated at ${MAX_MIRROR_BYTES} bytes]\n` : "";
|
|
489
490
|
let footer = `${truncation}\n${"─".repeat(48)}\n${terminalMessage}\n`;
|
|
490
491
|
if (scheduledBytes + Buffer.byteLength(footer) > MAX_MIRROR_BYTES) footer = `\n${status}\n`;
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
492
|
+
return new Promise<void>((resolve) => {
|
|
493
|
+
logStream.end(footer, () => {
|
|
494
|
+
if (available) {
|
|
495
|
+
try { fs.writeFileSync(donePath, `${status}\n`, { encoding: "utf-8", mode: 0o600 }); } catch { /* best effort */ }
|
|
496
|
+
cleanupPaths = [logPath, donePath];
|
|
497
|
+
scheduleDeferredCleanup();
|
|
498
|
+
}
|
|
499
|
+
resolve();
|
|
500
|
+
});
|
|
496
501
|
});
|
|
497
502
|
},
|
|
498
503
|
};
|
|
@@ -41,14 +41,13 @@ export interface RunnerSubagentStep {
|
|
|
41
41
|
/** The primary model is inherited from the parent session and should not be verified against the child-reported active registry model. */
|
|
42
42
|
skipPrimaryModelVerification?: boolean;
|
|
43
43
|
modelVerificationRegistry?: Array<{ provider: string; id: string; fullId: string; contextWindow?: number }>;
|
|
44
|
+
modelResponseAliases?: Record<string, string[]>;
|
|
44
45
|
tools?: string[];
|
|
45
46
|
excludeTools?: string[];
|
|
46
47
|
allowNestedSubagents?: boolean;
|
|
47
48
|
extensions?: string[];
|
|
48
49
|
subagentOnlyExtensions?: string[];
|
|
49
50
|
mcpDirectTools?: string[];
|
|
50
|
-
mcpConfig?: import("./mcp-direct-tool-allowlist.ts").McpConfig;
|
|
51
|
-
runtimeServerNames?: string[];
|
|
52
51
|
mutationTools?: string[];
|
|
53
52
|
completionGuard?: boolean;
|
|
54
53
|
systemPrompt?: string | null;
|
|
@@ -5,8 +5,6 @@ export type PermissionDecision = "allow" | "ask" | "deny";
|
|
|
5
5
|
export type PermissionRules = Record<string, PermissionDecision>;
|
|
6
6
|
export interface PermissionConfig { rules?: PermissionRules }
|
|
7
7
|
|
|
8
|
-
export const PERMISSION_POLICY_ENV = "PI_SUBAGENT_PERMISSION_POLICY";
|
|
9
|
-
export const PERMISSION_AUDIT_PATH_ENV = "PI_SUBAGENT_PERMISSION_AUDIT_PATH";
|
|
10
8
|
const INTERNAL_TOOLS = new Set(["contact_supervisor", "intercom", "bg_wait", "structured_output"]);
|
|
11
9
|
const DECISIONS = new Set<PermissionDecision>(["allow", "ask", "deny"]);
|
|
12
10
|
const MAX_POLICY_BYTES = 16 * 1024;
|
|
@@ -52,17 +50,6 @@ export function permissionDecision(rules: PermissionRules | undefined, toolName:
|
|
|
52
50
|
return rules?.[toolName] ?? "allow";
|
|
53
51
|
}
|
|
54
52
|
|
|
55
|
-
export function encodePermissionRules(rules: PermissionRules | undefined): string | undefined {
|
|
56
|
-
if (!rules || Object.keys(rules).length === 0) return undefined;
|
|
57
|
-
const encoded = JSON.stringify(rules);
|
|
58
|
-
if (Buffer.byteLength(encoded, "utf-8") > MAX_POLICY_BYTES) throw new Error("Resolved permission policy is too large.");
|
|
59
|
-
return encoded;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function decodePermissionRules(encoded: string | undefined): PermissionRules | undefined {
|
|
63
|
-
if (!encoded?.trim()) return undefined;
|
|
64
|
-
return validatePermissionRules(JSON.parse(encoded), PERMISSION_POLICY_ENV);
|
|
65
|
-
}
|
|
66
53
|
|
|
67
54
|
function redact(value: unknown, key = "", depth = 0): unknown {
|
|
68
55
|
if (SECRET_KEY.test(key)) return "[redacted]";
|
|
@@ -4,9 +4,12 @@ export function formatProcessSignalError(signal: string): string {
|
|
|
4
4
|
|
|
5
5
|
export function formatMidToolExitError(input: {
|
|
6
6
|
toolName: string;
|
|
7
|
-
exitCode
|
|
7
|
+
exitCode?: number | null;
|
|
8
8
|
processSignal?: string | null;
|
|
9
9
|
}): string {
|
|
10
|
+
if (input.exitCode === undefined && !input.processSignal) {
|
|
11
|
+
return `Subagent session ended during '${input.toolName}' tool execution before the tool completed. Earlier assistant output is not a terminal result.`;
|
|
12
|
+
}
|
|
10
13
|
const terminal = [`exit ${input.exitCode ?? "unknown"}`, ...(input.processSignal ? [`signal ${input.processSignal}`] : [])].join(", ");
|
|
11
14
|
return `Subagent process exited during '${input.toolName}' tool execution (${terminal}) before the tool completed. Earlier assistant output is not a terminal result.`;
|
|
12
15
|
}
|
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
type RunFanoutRejection,
|
|
10
10
|
} from "../../shared/types.ts";
|
|
11
11
|
|
|
12
|
-
export const RUN_FANOUT_BUDGET_ENV = "PI_SUBAGENT_RUN_FANOUT_BUDGET";
|
|
13
12
|
const RUN_FANOUT_ROOT = path.join(TEMP_ROOT_DIR, "run-fanout-budgets");
|
|
14
13
|
|
|
15
14
|
interface ManifestV1 {
|
|
@@ -124,18 +123,6 @@ export function readRunFanoutBudgetDescriptor(asyncDir: string | undefined): Run
|
|
|
124
123
|
}
|
|
125
124
|
}
|
|
126
125
|
|
|
127
|
-
export function encodeRunFanoutBudgetDescriptor(descriptor: RunFanoutBudgetDescriptor): string {
|
|
128
|
-
return Buffer.from(JSON.stringify(validateRunFanoutBudgetDescriptor(descriptor)), "utf-8").toString("base64url");
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
export function decodeRunFanoutBudgetDescriptor(encoded: string | undefined): RunFanoutBudgetDescriptor | undefined {
|
|
132
|
-
if (!encoded) return undefined;
|
|
133
|
-
try {
|
|
134
|
-
return validateRunFanoutBudgetDescriptor(JSON.parse(Buffer.from(encoded, "base64url").toString("utf-8")));
|
|
135
|
-
} catch (error) {
|
|
136
|
-
throw new Error(`Invalid inherited run fan-out budget: ${error instanceof Error ? error.message : String(error)}`);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
126
|
|
|
140
127
|
interface AdmissionLockOwner {
|
|
141
128
|
pid: number;
|
|
@@ -1,13 +1,8 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import * as path from "node:path";
|
|
3
1
|
import type { RuntimeAcknowledgedChildExtensionsV1 } from "../../shared/types.ts";
|
|
4
2
|
|
|
5
3
|
export const RUNTIME_EXTENSION_ACK_EVENT = "subagent:acknowledge-extension";
|
|
6
|
-
export const RUNTIME_EXTENSION_ACK_PATH_ENV = "PI_SUBAGENT_RUNTIME_ACKNOWLEDGED_EXTENSIONS";
|
|
7
4
|
export const MAX_RUNTIME_ACKNOWLEDGED_EXTENSION_IDS = 32;
|
|
8
5
|
export const MAX_RUNTIME_ACKNOWLEDGED_EXTENSION_ID_LENGTH = 128;
|
|
9
|
-
// Generous bound for a valid capture (32 ids x 128 chars plus envelope); larger files are child misbehavior.
|
|
10
|
-
export const MAX_RUNTIME_ACKNOWLEDGED_EXTENSION_FILE_BYTES = 64 * 1024;
|
|
11
6
|
|
|
12
7
|
export function isRuntimeAcknowledgedExtensionId(value: unknown): value is string {
|
|
13
8
|
return typeof value === "string"
|
|
@@ -47,25 +42,3 @@ export function sanitizeRuntimeAcknowledgedExtensions(value: unknown): RuntimeAc
|
|
|
47
42
|
: 0;
|
|
48
43
|
return { ...projected, omitted: projected.omitted + omitted };
|
|
49
44
|
}
|
|
50
|
-
|
|
51
|
-
export function readRuntimeAcknowledgedExtensions(filePath: string | undefined): RuntimeAcknowledgedChildExtensionsV1 | undefined {
|
|
52
|
-
if (!filePath) return undefined;
|
|
53
|
-
try {
|
|
54
|
-
if (fs.statSync(filePath).size > MAX_RUNTIME_ACKNOWLEDGED_EXTENSION_FILE_BYTES) return undefined;
|
|
55
|
-
const parsed = JSON.parse(fs.readFileSync(filePath, "utf-8")) as unknown;
|
|
56
|
-
return sanitizeRuntimeAcknowledgedExtensions(parsed);
|
|
57
|
-
} catch {
|
|
58
|
-
return undefined;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function writeRuntimeAcknowledgedExtensions(filePath: string, ids: Iterable<unknown>): void {
|
|
63
|
-
const projection = projectRuntimeAcknowledgedExtensions(ids);
|
|
64
|
-
try {
|
|
65
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
66
|
-
if (projection) fs.writeFileSync(filePath, JSON.stringify(projection), { mode: 0o600 });
|
|
67
|
-
else fs.rmSync(filePath, { force: true });
|
|
68
|
-
} catch {
|
|
69
|
-
// Runtime acknowledgement is best-effort observability only.
|
|
70
|
-
}
|
|
71
|
-
}
|