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
|
@@ -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
|
}
|
|
@@ -252,6 +319,7 @@ export function filterFallbackCandidates(candidates: string[], opts?: {
|
|
|
252
319
|
onExcluded?: (candidate: string, exclusion: Readonly<ModelExclusion>) => void;
|
|
253
320
|
}): string[] {
|
|
254
321
|
ensureLoaded();
|
|
322
|
+
invalidateAuthExclusions();
|
|
255
323
|
const timestamp = opts?.now ?? Date.now();
|
|
256
324
|
const seen = new Set<string>();
|
|
257
325
|
const filtered: string[] = [];
|
|
@@ -284,6 +284,31 @@ function enforceModelScopes(
|
|
|
284
284
|
for (const violation of violations) (onWarn ?? defaultScopeWarn)(violation);
|
|
285
285
|
}
|
|
286
286
|
|
|
287
|
+
const MODEL_EXCLUSION_DIAGNOSTIC_MAX_LENGTH = 240;
|
|
288
|
+
const MODEL_EXCLUSION_DIAGNOSTIC_MAX_ENTRIES = 20;
|
|
289
|
+
|
|
290
|
+
function sanitizeModelExclusionDiagnostic(value: string | undefined, fallback: string): string {
|
|
291
|
+
const normalized = typeof value === "string"
|
|
292
|
+
? value.replace(/[\u0000-\u001f\u007f\u2028\u2029]+/g, " ").trim()
|
|
293
|
+
: "";
|
|
294
|
+
return redactSecretValues(normalized || fallback).slice(0, MODEL_EXCLUSION_DIAGNOSTIC_MAX_LENGTH);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function formatModelExclusionExpiry(expiresAt: number): string {
|
|
298
|
+
if (!Number.isFinite(expiresAt)) return "unknown";
|
|
299
|
+
const date = new Date(expiresAt);
|
|
300
|
+
return Number.isNaN(date.getTime()) ? "unknown" : date.toISOString();
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function formatExcludedCandidateEvidence(candidate: string, exclusion: NonNullable<ReturnType<typeof findModelExclusion>>): string {
|
|
304
|
+
const { provider, modelId } = parseModelKey(candidate);
|
|
305
|
+
const displayCandidate = sanitizeModelExclusionDiagnostic(candidate, "unknown");
|
|
306
|
+
const displayModel = sanitizeModelExclusionDiagnostic(modelId, "unknown");
|
|
307
|
+
const displayProvider = sanitizeModelExclusionDiagnostic(provider ?? exclusion.provider, "unspecified");
|
|
308
|
+
const reason = sanitizeModelExclusionDiagnostic(exclusion.reason, "runtime-failure");
|
|
309
|
+
return `${displayCandidate} — model: ${displayModel}; provider: ${displayProvider}; reason: ${reason}; expires: ${formatModelExclusionExpiry(exclusion.expiresAt)}`;
|
|
310
|
+
}
|
|
311
|
+
|
|
287
312
|
function throwForExplicitModelExclusion(model: string): void {
|
|
288
313
|
const exclusion = findModelExclusion(model);
|
|
289
314
|
if (!exclusion) return;
|
|
@@ -419,9 +444,15 @@ export function buildModelCandidates(
|
|
|
419
444
|
if (!primaryModel) throwForUnresolvedEnforcedInheritScope(options?.scope, true);
|
|
420
445
|
const origin = options?.origin ?? (options?.primaryModelFromParent ? "inherited" : "configured");
|
|
421
446
|
const scopes = configuredScopes(options?.scope);
|
|
447
|
+
type ExcludedCandidate = { candidate: string; exclusion: NonNullable<ReturnType<typeof findModelExclusion>> };
|
|
448
|
+
const excludedCandidates: ExcludedCandidate[] = [];
|
|
449
|
+
let excludedCandidateCount = 0;
|
|
422
450
|
const warnCachedExclusion = (candidate: string, exclusion: NonNullable<ReturnType<typeof findModelExclusion>>) => {
|
|
423
|
-
|
|
424
|
-
|
|
451
|
+
excludedCandidateCount++;
|
|
452
|
+
if (excludedCandidates.length < MODEL_EXCLUSION_DIAGNOSTIC_MAX_ENTRIES) excludedCandidates.push({ candidate, exclusion });
|
|
453
|
+
const displayCandidate = sanitizeModelExclusionDiagnostic(candidate, "unknown");
|
|
454
|
+
const reason = sanitizeModelExclusionDiagnostic(exclusion.reason, "runtime-failure");
|
|
455
|
+
console.warn(`[pi-subagents] Skipping model '${displayCandidate}' due to a cached exclusion (reason: ${reason}; expires: ${formatModelExclusionExpiry(exclusion.expiresAt)}).`);
|
|
425
456
|
};
|
|
426
457
|
if (origin === "explicit" && primaryModel) {
|
|
427
458
|
const normalized = resolveRequiredSubagentModelCandidate(primaryModel.trim(), availableModels, preferredProvider);
|
|
@@ -433,6 +464,7 @@ export function buildModelCandidates(
|
|
|
433
464
|
const candidates: string[] = [];
|
|
434
465
|
const rawCandidates = [primaryModel, ...(fallbackModels ?? [])];
|
|
435
466
|
let skippedPrimary: string | undefined;
|
|
467
|
+
let skippedFallback: string | undefined;
|
|
436
468
|
for (let index = 0; index < rawCandidates.length; index++) {
|
|
437
469
|
const raw = rawCandidates[index];
|
|
438
470
|
if (!raw) continue;
|
|
@@ -442,7 +474,10 @@ export function buildModelCandidates(
|
|
|
442
474
|
: resolveSubagentModelCandidate(model, availableModels, preferredProvider);
|
|
443
475
|
if (!normalized) {
|
|
444
476
|
if (index === 0) skippedPrimary = model;
|
|
445
|
-
else
|
|
477
|
+
else {
|
|
478
|
+
skippedFallback ??= model;
|
|
479
|
+
console.warn(`[pi-subagents] Skipping fallback model '${model}' because it is unavailable in this environment.`);
|
|
480
|
+
}
|
|
446
481
|
continue;
|
|
447
482
|
}
|
|
448
483
|
if (seen.has(normalized)) continue;
|
|
@@ -455,7 +490,15 @@ export function buildModelCandidates(
|
|
|
455
490
|
const resolved = filterFallbackCandidates(candidates, { onExcluded: warnCachedExclusion });
|
|
456
491
|
if (resolved.length === 0) {
|
|
457
492
|
if (skippedPrimary) resolveRequiredSubagentModelCandidate(skippedPrimary, availableModels, preferredProvider);
|
|
458
|
-
if (candidates.length
|
|
493
|
+
if (candidates.length === 0 && skippedFallback) resolveRequiredSubagentModelCandidate(skippedFallback, availableModels, preferredProvider);
|
|
494
|
+
if (candidates.length > 0) {
|
|
495
|
+
const shownExclusions = excludedCandidates;
|
|
496
|
+
const omittedExclusions = excludedCandidateCount - shownExclusions.length;
|
|
497
|
+
const evidence = shownExclusions.length > 0
|
|
498
|
+
? ` (excluded: ${shownExclusions.map(({ candidate, exclusion }) => formatExcludedCandidateEvidence(candidate, exclusion)).join("; ")}${omittedExclusions > 0 ? `; ... and ${omittedExclusions} more` : ""})`
|
|
499
|
+
: "";
|
|
500
|
+
throw new Error(`${ZERO_USABLE_MODEL_CANDIDATES_ERROR}${evidence}`);
|
|
501
|
+
}
|
|
459
502
|
return resolved;
|
|
460
503
|
}
|
|
461
504
|
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 {
|
|
@@ -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
|
};
|
|
@@ -47,8 +47,6 @@ export interface RunnerSubagentStep {
|
|
|
47
47
|
extensions?: string[];
|
|
48
48
|
subagentOnlyExtensions?: string[];
|
|
49
49
|
mcpDirectTools?: string[];
|
|
50
|
-
mcpConfig?: import("./mcp-direct-tool-allowlist.ts").McpConfig;
|
|
51
|
-
runtimeServerNames?: string[];
|
|
52
50
|
mutationTools?: string[];
|
|
53
51
|
completionGuard?: boolean;
|
|
54
52
|
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
|
-
}
|
|
@@ -7,10 +7,6 @@ import { PI_CODING_AGENT_PACKAGE_ROOT_ENV } from "../../shared/utils.ts";
|
|
|
7
7
|
import type { JsonSchemaObject } from "../../shared/types.ts";
|
|
8
8
|
import type { ResolvedAcceptanceReportMode } from "./acceptance.ts";
|
|
9
9
|
|
|
10
|
-
export const STRUCTURED_OUTPUT_SCHEMA_ENV = "PI_SUBAGENT_STRUCTURED_OUTPUT_SCHEMA";
|
|
11
|
-
export const STRUCTURED_OUTPUT_CAPTURE_ENV = "PI_SUBAGENT_STRUCTURED_OUTPUT_CAPTURE";
|
|
12
|
-
export const STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE_ENV = "PI_SUBAGENT_STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE";
|
|
13
|
-
export const STRUCTURED_OUTPUT_ACCEPTANCE_REQUIRED_ENV = "PI_SUBAGENT_STRUCTURED_OUTPUT_ACCEPTANCE_REQUIRED";
|
|
14
10
|
export const MISSING_STRUCTURED_OUTPUT_CALL_ERROR = "Missing structured_output call; this step has outputSchema and must finish by calling structured_output.";
|
|
15
11
|
export const MISSING_STRUCTURED_ACCEPTANCE_REPORT_ERROR = "Missing acceptanceReport in structured_output call; acceptance.report is \"on\".";
|
|
16
12
|
|
|
@@ -200,6 +196,23 @@ export function readStructuredOutputAcceptanceReport(runtime: StructuredOutputRu
|
|
|
200
196
|
}
|
|
201
197
|
}
|
|
202
198
|
|
|
199
|
+
/**
|
|
200
|
+
* Capture callback that persists the structured output (and acceptance report)
|
|
201
|
+
* to the runtime's files, for hosts that read the value back from disk.
|
|
202
|
+
*/
|
|
203
|
+
export function createStructuredOutputFileCapture(runtime: StructuredOutputRuntime): (value: unknown, acceptanceReport: unknown | undefined) => void {
|
|
204
|
+
return (value, acceptanceReport) => {
|
|
205
|
+
fs.mkdirSync(path.dirname(runtime.outputPath), { recursive: true });
|
|
206
|
+
if (runtime.acceptanceReportPath && acceptanceReport !== undefined) {
|
|
207
|
+
fs.mkdirSync(path.dirname(runtime.acceptanceReportPath), { recursive: true });
|
|
208
|
+
fs.writeFileSync(runtime.acceptanceReportPath, JSON.stringify(acceptanceReport), { mode: 0o600 });
|
|
209
|
+
} else if (runtime.acceptanceReportPath && fs.existsSync(runtime.acceptanceReportPath)) {
|
|
210
|
+
fs.unlinkSync(runtime.acceptanceReportPath);
|
|
211
|
+
}
|
|
212
|
+
fs.writeFileSync(runtime.outputPath, JSON.stringify(value), { mode: 0o600 });
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
203
216
|
export function clearStructuredOutputCaptures(runtime: StructuredOutputRuntime): string | undefined {
|
|
204
217
|
let cleanupError: string | undefined;
|
|
205
218
|
for (const filePath of [runtime.outputPath, runtime.acceptanceReportPath]) {
|
|
@@ -45,11 +45,12 @@ export function resolveControlConfig(
|
|
|
45
45
|
const enabled = override?.enabled ?? globalConfig?.enabled ?? DEFAULT_CONTROL_CONFIG.enabled;
|
|
46
46
|
const overrideNeedsAttentionAfterMs = parsePositiveInt(override?.needsAttentionAfterMs);
|
|
47
47
|
const globalNeedsAttentionAfterMs = parsePositiveInt(globalConfig?.needsAttentionAfterMs);
|
|
48
|
+
const globalNeedsAttentionAfterMsIsExplicit = (globalConfig as Partial<ResolvedControlConfig> | undefined)?.needsAttentionAfterMsIsExplicit ?? globalNeedsAttentionAfterMs !== undefined;
|
|
48
49
|
const needsAttentionAfterMs = overrideNeedsAttentionAfterMs
|
|
49
50
|
?? globalNeedsAttentionAfterMs
|
|
50
51
|
?? DEFAULT_CONTROL_CONFIG.needsAttentionAfterMs;
|
|
51
52
|
const needsAttentionAfterMsIsExplicit = overrideNeedsAttentionAfterMs !== undefined
|
|
52
|
-
||
|
|
53
|
+
|| globalNeedsAttentionAfterMsIsExplicit;
|
|
53
54
|
const activeNoticeAfterMs = parsePositiveInt(override?.activeNoticeAfterMs)
|
|
54
55
|
?? parsePositiveInt(globalConfig?.activeNoticeAfterMs)
|
|
55
56
|
?? DEFAULT_CONTROL_CONFIG.activeNoticeAfterMs;
|
|
@@ -98,11 +99,12 @@ export function deriveActivityState(input: {
|
|
|
98
99
|
config: ResolvedControlConfig;
|
|
99
100
|
startedAt: number;
|
|
100
101
|
lastActivityAt?: number;
|
|
102
|
+
turnCount?: number;
|
|
101
103
|
currentTool?: string;
|
|
102
104
|
thinking?: string | false;
|
|
103
105
|
now?: number;
|
|
104
106
|
}): ActivityState | undefined {
|
|
105
|
-
if (!input.config.enabled || input.currentTool) return undefined;
|
|
107
|
+
if (!input.config.enabled || input.currentTool || (input.turnCount ?? 0) === 0) return undefined;
|
|
106
108
|
const now = input.now ?? Date.now();
|
|
107
109
|
const lastActivity = input.lastActivityAt ?? input.startedAt;
|
|
108
110
|
const ageMs = Math.max(0, now - lastActivity);
|
|
@@ -136,6 +138,7 @@ export function buildControlEvent(input: {
|
|
|
136
138
|
tokens?: number;
|
|
137
139
|
toolCount?: number;
|
|
138
140
|
currentTool?: string;
|
|
141
|
+
toolCallId?: string;
|
|
139
142
|
currentToolDurationMs?: number;
|
|
140
143
|
currentPath?: string;
|
|
141
144
|
elapsedMs?: number;
|
|
@@ -168,6 +171,7 @@ export function buildControlEvent(input: {
|
|
|
168
171
|
...(input.tokens !== undefined ? { tokens: input.tokens } : {}),
|
|
169
172
|
...(input.toolCount !== undefined ? { toolCount: input.toolCount } : {}),
|
|
170
173
|
...(input.currentTool ? { currentTool: input.currentTool } : {}),
|
|
174
|
+
...(input.toolCallId ? { toolCallId: input.toolCallId } : {}),
|
|
171
175
|
...(input.currentToolDurationMs !== undefined ? { currentToolDurationMs: input.currentToolDurationMs } : {}),
|
|
172
176
|
...(input.currentPath ? { currentPath: input.currentPath } : {}),
|
|
173
177
|
...(elapsedMs !== undefined ? { elapsedMs } : {}),
|