pi-subagents 0.67.0 → 0.68.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 +70 -0
- package/README.md +1 -1
- package/docs/agents.md +37 -12
- package/docs/configuration.md +61 -19
- package/docs/extension-api.md +5 -1
- package/docs/missions.md +2 -2
- package/docs/models.md +11 -79
- package/docs/observability.md +18 -8
- package/docs/standalone-background.md +13 -3
- package/docs/tool-reference.md +15 -12
- package/docs/watchdog.md +10 -12
- package/docs/workflows.md +11 -1
- package/index.ts +5 -2
- package/package.json +4 -2
- package/runner-peer-loader.mjs +24 -0
- package/runner-peer-preload.mjs +25 -11
- package/skills/pi-subagents/SKILL.md +18 -21
- package/skills/pi-subagents/references/constraints-and-recipes.md +3 -2
- package/skills/pi-subagents/references/execution-controls.md +4 -4
- package/skills/pi-subagents/references/management-authoring-rpc.md +0 -1
- package/skills/pi-subagents/references/multi-lane-orchestration.md +1 -1
- package/skills/pi-subagents/references/prompting-and-roles.md +16 -12
- package/skills/pi-subagents/references/review-and-validation.md +3 -3
- package/src/agents/agent-management.ts +57 -58
- package/src/agents/agent-serializer.ts +4 -3
- package/src/agents/agents.ts +184 -71
- package/src/agents/chain-serializer.ts +5 -0
- package/src/agents/runtime-agent-registry.ts +7 -6
- package/src/api/preflight.ts +20 -16
- package/src/api/required-child-extensions.ts +6 -0
- package/src/extension/config.ts +10 -37
- package/src/extension/fanout-child.ts +3 -0
- package/src/extension/herdr-pi-bridge.ts +160 -0
- package/src/extension/index.ts +42 -31
- package/src/extension/public-execution.ts +3 -3
- package/src/extension/schemas.ts +16 -5
- package/src/extension/tool-description.ts +8 -7
- package/src/intercom/native-supervisor-channel.ts +22 -18
- package/src/policy/authority.ts +4 -0
- package/src/profiles/profiles.ts +12 -6
- package/src/runs/background/active-run-index.ts +17 -1
- package/src/runs/background/async-execution.ts +309 -126
- package/src/runs/background/async-job-tracker.ts +8 -6
- package/src/runs/background/async-resume.ts +13 -4
- package/src/runs/background/async-status.ts +15 -4
- package/src/runs/background/auto-drain.ts +20 -10
- package/src/runs/background/binary-bootstrap.ts +5 -0
- package/src/runs/background/chain-append.ts +1 -1
- package/src/runs/background/chain-root-attachment.ts +14 -33
- package/src/runs/background/notify.ts +74 -6
- package/src/runs/background/result-files.ts +8 -4
- package/src/runs/background/result-watcher.ts +19 -2
- package/src/runs/background/run-child-session.ts +20 -29
- package/src/runs/background/runner-aliases.ts +4 -33
- package/src/runs/background/runner-child-launch.ts +4 -1
- package/src/runs/background/runner-child-sessions.ts +2 -2
- package/src/runs/background/runner-http-dispatcher.ts +119 -0
- package/src/runs/background/scheduled-runs.ts +11 -5
- package/src/runs/background/stale-run-reconciler.ts +35 -11
- package/src/runs/background/subagent-runner.ts +396 -275
- package/src/runs/background/subagent-wait.ts +128 -23
- package/src/runs/background/wait-completions.ts +75 -27
- package/src/runs/background/wait-subscriptions.ts +9 -3
- package/src/runs/background/wait-tool.ts +4 -2
- package/src/runs/foreground/async-stop-action.ts +93 -3
- package/src/runs/foreground/execution.ts +91 -218
- package/src/runs/foreground/foreground-history.ts +2 -1
- package/src/runs/foreground/subagent-executor.ts +266 -80
- package/src/runs/shared/acceptance.ts +34 -10
- package/src/runs/shared/async-status-projection.ts +123 -33
- package/src/runs/shared/child-launch-plan.ts +15 -3
- package/src/runs/shared/child-launch.ts +19 -6
- package/src/runs/shared/child-runtime-config.ts +5 -0
- package/src/runs/shared/child-session.ts +94 -50
- package/src/runs/shared/child-tool-plan.ts +28 -16
- package/src/runs/shared/dynamic-fanout.ts +2 -2
- package/src/runs/shared/external-cli-contract.ts +11 -1
- package/src/runs/shared/external-cli-preflight.ts +6 -2
- package/src/runs/shared/herdr-connection.ts +134 -0
- package/src/runs/shared/herdr-external-adapters.ts +169 -0
- package/src/runs/shared/herdr-machine.ts +279 -0
- package/src/runs/shared/herdr-pi-protocol.ts +59 -0
- package/src/runs/shared/herdr-placed-run.ts +263 -0
- package/src/runs/shared/model-resolution-diagnostic.ts +76 -0
- package/src/runs/shared/{model-fallback.ts → model-resolution.ts} +22 -237
- package/src/runs/shared/model-scope.ts +1 -1
- package/src/runs/shared/nested-events.ts +11 -2
- package/src/runs/shared/parallel-utils.ts +7 -2
- package/src/runs/shared/pi-spawn.ts +1 -1
- package/src/runs/shared/subagent-prompt-runtime.ts +4 -2
- package/src/runs/shared/worktree-setup-command.ts +27 -4
- package/src/runs/shared/worktree.ts +3 -3
- package/src/shared/child-cache-retention.ts +43 -0
- package/src/shared/launch-contract.ts +6 -9
- package/src/shared/pruned-fork.ts +1 -1
- package/src/shared/required-child-extensions.ts +81 -0
- package/src/shared/settings.ts +5 -2
- package/src/shared/shortcuts.ts +0 -4
- package/src/shared/types.ts +70 -29
- package/src/slash/slash-commands.ts +0 -6
- package/src/slash/subagents-admin.ts +13 -9
- package/src/tui/render.ts +20 -10
- package/src/watchdog/child-status.ts +28 -36
- package/src/watchdog/lsp-diagnostics.ts +1 -1
- package/src/watchdog/model-selection.ts +1 -1
- package/src/watchdog/register-child.ts +10 -3
- package/src/watchdog/register-main.ts +20 -20
- package/src/watchdog/render.ts +1 -1
- package/src/watchdog/review.ts +14 -30
- package/src/watchdog/rules.ts +1 -1
- package/src/watchdog/runtime.ts +23 -12
- package/src/watchdog/settings.ts +3 -6
- package/src/watchdog/types.ts +3 -5
- package/src/watchdog/warning-format.ts +1 -1
- package/src/workflows/scripted-workflow.ts +42 -3
- package/src/workflows/workflow-receipt.ts +21 -3
- package/src/workflows/workflow-resources.ts +13 -2
- package/src/runs/shared/model-exclusions.ts +0 -374
- package/src/runs/shared/readonly-model-continuation.ts +0 -69
- package/src/runs/shared/readonly-session-evidence.ts +0 -307
|
@@ -5,21 +5,15 @@
|
|
|
5
5
|
* parent pi process for foreground children, the detached runner process for
|
|
6
6
|
* background children. The factory is injectable so tests can script a child
|
|
7
7
|
* without the real runtime; the default implementation wraps
|
|
8
|
-
* `createAgentSession` from a pi package module
|
|
9
|
-
* across every child it creates.
|
|
8
|
+
* `createAgentSession` from a pi package module.
|
|
10
9
|
*/
|
|
11
10
|
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
12
11
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import { pinChildCacheRetention } from "../../shared/child-cache-retention.ts";
|
|
13
13
|
import { getAgentDir } from "../../shared/utils.ts";
|
|
14
14
|
import type { ChildRuntimeConfig } from "./child-runtime-config.ts";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
// Private runtime authority for host continuation planning; injected factories have none.
|
|
19
|
-
const readonlyModels = new WeakMap<ChildSession, { current: ModelInfo; resolve(reference: string): ModelInfo | undefined; requestBytes: number }>();
|
|
20
|
-
export function getReadonlyChildModels(child: ChildSession) {
|
|
21
|
-
return readonlyModels.get(child);
|
|
22
|
-
}
|
|
15
|
+
import type { RequiredChildExtensionSnapshot } from "../../shared/required-child-extensions.ts";
|
|
16
|
+
import type { HerdrMachineReference, HerdrRemoteGitStatus } from "../../shared/types.ts";
|
|
23
17
|
|
|
24
18
|
export interface ChildSessionEvent {
|
|
25
19
|
type: string;
|
|
@@ -54,6 +48,12 @@ export type ChildSessionStorage =
|
|
|
54
48
|
|
|
55
49
|
export interface ChildSessionLaunch {
|
|
56
50
|
cwd: string;
|
|
51
|
+
/** Resolved pane-native placement. Local launches omit this field. */
|
|
52
|
+
machine?: HerdrMachineReference;
|
|
53
|
+
/** Process-local provider source owned by the invoking foreground parent. */
|
|
54
|
+
parentProviderRegistry?: ParentProviderRegistry;
|
|
55
|
+
/** Logical names resolved only by the remote ambient package. */
|
|
56
|
+
remoteResources?: { agent: string; skills?: string[]; toolCeiling?: string[]; reads?: string[] | false };
|
|
57
57
|
storage: ChildSessionStorage;
|
|
58
58
|
/** Model reference as the agent config names it (`provider/id`, optionally `:thinking`). */
|
|
59
59
|
model?: string;
|
|
@@ -62,6 +62,8 @@ export interface ChildSessionLaunch {
|
|
|
62
62
|
excludeTools?: string[];
|
|
63
63
|
/** Extension files loaded for this child in addition to the inline hooks. */
|
|
64
64
|
extensionPaths: string[];
|
|
65
|
+
/** Canonical required paths and safe evidence identities for fail-closed loading. */
|
|
66
|
+
requiredExtensions?: RequiredChildExtensionSnapshot;
|
|
65
67
|
/**
|
|
66
68
|
* Discover the ambient extensions (agent dir, project, settings) the way a
|
|
67
69
|
* `pi` process would. False loads only `extensionPaths` and `hooks`.
|
|
@@ -99,6 +101,9 @@ export interface ChildSession {
|
|
|
99
101
|
readonly sessionFile: string | undefined;
|
|
100
102
|
readonly sessionId: string;
|
|
101
103
|
readonly modelId: string | undefined;
|
|
104
|
+
readonly machineEvidence?: { machineId: string; initial?: HerdrRemoteGitStatus; final?: HerdrRemoteGitStatus };
|
|
105
|
+
/** Event-updated pane-native status; reading it performs no network work. */
|
|
106
|
+
readonly placementSnapshot?: unknown;
|
|
102
107
|
/** Set by the foreground host once the run detached; `factory.dispose()` leaves such children running. */
|
|
103
108
|
detached?: boolean;
|
|
104
109
|
/** Set by `factory.dispose()` before it aborts the child, so the host can report the stop truthfully. */
|
|
@@ -134,6 +139,34 @@ export interface DefaultChildSessionFactoryOptions {
|
|
|
134
139
|
|
|
135
140
|
type ModelRuntimeInstance = Awaited<ReturnType<PiCodingAgentModule["ModelRuntime"]["create"]>>;
|
|
136
141
|
|
|
142
|
+
export type ParentProviderRegistry = Pick<ModelRuntimeInstance, "getRegisteredProviderIds" | "getRegisteredProviderConfig" | "getRegisteredNativeProvider">;
|
|
143
|
+
|
|
144
|
+
function inheritParentProviders(modelRuntime: ModelRuntimeInstance, parentProviders: ParentProviderRegistry, claimedProviderIds: ReadonlySet<string>, onError: ((error: ChildSessionExtensionError) => void) | undefined): boolean {
|
|
145
|
+
let providerIds: readonly string[];
|
|
146
|
+
try {
|
|
147
|
+
providerIds = parentProviders.getRegisteredProviderIds();
|
|
148
|
+
} catch (error) {
|
|
149
|
+
onError?.({ extensionPath: "<parent-providers>", event: "inherit_provider", error });
|
|
150
|
+
throw new Error(`Failed to enumerate parent providers: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
151
|
+
}
|
|
152
|
+
let registered = false;
|
|
153
|
+
for (const providerId of new Set(providerIds)) {
|
|
154
|
+
if (claimedProviderIds.has(providerId)) continue;
|
|
155
|
+
try {
|
|
156
|
+
const native = parentProviders.getRegisteredNativeProvider(providerId);
|
|
157
|
+
const config = native ? undefined : parentProviders.getRegisteredProviderConfig(providerId);
|
|
158
|
+
if (native) modelRuntime.registerNativeProvider(native);
|
|
159
|
+
else if (config) modelRuntime.registerProvider(providerId, config);
|
|
160
|
+
else throw new Error(`Parent provider '${providerId}' has no registered native provider or config.`);
|
|
161
|
+
registered = true;
|
|
162
|
+
} catch (error) {
|
|
163
|
+
onError?.({ extensionPath: `<parent-provider:${providerId}>`, event: "inherit_provider", error });
|
|
164
|
+
throw new Error(`Failed to inherit parent provider '${providerId}': ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return registered;
|
|
168
|
+
}
|
|
169
|
+
|
|
137
170
|
const CHILD_PROMPT_RUNTIME_EXTENSION_PATH = "<inline:pi-subagents:prompt-runtime>";
|
|
138
171
|
|
|
139
172
|
/** The prompt runtime filters parent-only context before ambient extensions inspect
|
|
@@ -173,34 +206,39 @@ function applyProcessEnv(values: Record<string, string | undefined> | undefined)
|
|
|
173
206
|
}
|
|
174
207
|
}
|
|
175
208
|
|
|
176
|
-
|
|
177
|
-
|
|
209
|
+
function flushQueuedProviderRegistrations(loader: InstanceType<PiCodingAgentModule["DefaultResourceLoader"]>, modelRuntime: ModelRuntimeInstance, onError: ((error: ChildSessionExtensionError) => void) | undefined, requiredPaths: ReadonlySet<string>): { claimedProviderIds: Set<string>; registered: boolean } {
|
|
210
|
+
const claimedProviderIds = new Set<string>();
|
|
211
|
+
if (!("getExtensions" in loader) || typeof loader.getExtensions !== "function") return { claimedProviderIds, registered: false };
|
|
178
212
|
const { runtime } = loader.getExtensions();
|
|
179
213
|
let registered = false;
|
|
180
214
|
for (const { name, config, extensionPath } of runtime.pendingProviderRegistrations ?? []) {
|
|
215
|
+
claimedProviderIds.add(name);
|
|
181
216
|
try {
|
|
182
217
|
modelRuntime.registerProvider(name, config);
|
|
183
218
|
registered = true;
|
|
184
219
|
} catch (error) {
|
|
185
220
|
onError?.({ extensionPath, event: "register_provider", error });
|
|
221
|
+
if (requiredPaths.has(extensionPath)) throw new Error(`Required child extension provider registration failed for '${extensionPath}': ${error instanceof Error ? error.message : String(error)}`);
|
|
186
222
|
}
|
|
187
223
|
}
|
|
188
224
|
if (Array.isArray(runtime.pendingProviderRegistrations)) runtime.pendingProviderRegistrations = [];
|
|
189
225
|
for (const { provider, extensionPath } of runtime.pendingNativeProviderRegistrations ?? []) {
|
|
226
|
+
claimedProviderIds.add(provider.id);
|
|
190
227
|
try {
|
|
191
228
|
modelRuntime.registerNativeProvider(provider);
|
|
192
229
|
registered = true;
|
|
193
230
|
} catch (error) {
|
|
194
231
|
onError?.({ extensionPath, event: "register_provider", error });
|
|
232
|
+
if (requiredPaths.has(extensionPath)) throw new Error(`Required child extension provider registration failed for '${extensionPath}': ${error instanceof Error ? error.message : String(error)}`);
|
|
195
233
|
}
|
|
196
234
|
}
|
|
197
235
|
if (Array.isArray(runtime.pendingNativeProviderRegistrations)) runtime.pendingNativeProviderRegistrations = [];
|
|
198
|
-
|
|
236
|
+
return { claimedProviderIds, registered };
|
|
199
237
|
}
|
|
200
238
|
|
|
201
239
|
/**
|
|
202
|
-
* Default factory:
|
|
203
|
-
*
|
|
240
|
+
* Default factory: detached/background sessions retain the existing shared
|
|
241
|
+
* runtime; each parent-bound foreground launch gets an isolated runtime.
|
|
204
242
|
*/
|
|
205
243
|
export function createDefaultChildSessionFactory(options: DefaultChildSessionFactoryOptions = {}): ChildSessionFactory {
|
|
206
244
|
const loadPiCodingAgent = options.loadPiCodingAgent ?? (() => import("@earendil-works/pi-coding-agent"));
|
|
@@ -218,9 +256,10 @@ export function createDefaultChildSessionFactory(options: DefaultChildSessionFac
|
|
|
218
256
|
};
|
|
219
257
|
return {
|
|
220
258
|
async create(launch) {
|
|
221
|
-
const observeReadonly = prepareReadonlySessionEvidence(launch);
|
|
222
259
|
const pi = await loadPiCodingAgent();
|
|
223
|
-
const modelRuntime =
|
|
260
|
+
const modelRuntime = launch.parentProviderRegistry
|
|
261
|
+
? await pi.ModelRuntime.create()
|
|
262
|
+
: await sharedRuntime(pi);
|
|
224
263
|
const agentDir = getAgentDir();
|
|
225
264
|
const settingsManager = pi.SettingsManager.create(launch.cwd, agentDir);
|
|
226
265
|
// Foreground children share Pi's global theme with the parent, so reinitializing it
|
|
@@ -245,13 +284,25 @@ export function createDefaultChildSessionFactory(options: DefaultChildSessionFac
|
|
|
245
284
|
...(launch.appendSystemPrompt !== undefined ? { appendSystemPrompt: [launch.appendSystemPrompt] } : {}),
|
|
246
285
|
});
|
|
247
286
|
const open = async () => {
|
|
287
|
+
const requiredPaths = new Set((launch.requiredExtensions ?? []).map(({ path }) => path));
|
|
248
288
|
applyProcessEnv(launch.processEnv);
|
|
249
289
|
if (!resetExtensionCacheOnReload(loader) && (launch.ambientExtensions || launch.extensionPaths.length)) launch.onExtensionError?.({ extensionPath: "<loader>", event: "load", error: new Error("pi's extension cache reset is unavailable; extensions loaded into this child share module state with other sessions in this process.") });
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
290
|
+
await loader.reload();
|
|
291
|
+
const loadErrors = requiredPaths.size > 0
|
|
292
|
+
? loader.getExtensions().errors.filter(({ path }) => requiredPaths.has(path)) : [];
|
|
293
|
+
if (loadErrors.length > 0) throw new Error(`Required child extension failed to load: ${loadErrors.map(({ path, error }) => `${path}: ${error}`).join("; ")}`);
|
|
294
|
+
const queued = flushQueuedProviderRegistrations(loader, modelRuntime, launch.onExtensionError, requiredPaths);
|
|
295
|
+
const inherited = launch.parentProviderRegistry
|
|
296
|
+
? inheritParentProviders(modelRuntime, launch.parentProviderRegistry, queued.claimedProviderIds, launch.onExtensionError)
|
|
297
|
+
: false;
|
|
298
|
+
if (queued.registered || inherited) {
|
|
299
|
+
try {
|
|
300
|
+
await modelRuntime.refresh({ allowNetwork: false });
|
|
301
|
+
} catch (error) {
|
|
302
|
+
launch.onExtensionError?.({ extensionPath: "<provider-refresh>", event: "refresh_providers", error });
|
|
303
|
+
throw new Error(`Failed to refresh child providers: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
304
|
+
}
|
|
305
|
+
}
|
|
255
306
|
const sessionManager = launch.storage.kind === "file"
|
|
256
307
|
? pi.SessionManager.open(launch.storage.sessionFile, undefined, launch.cwd)
|
|
257
308
|
: launch.storage.kind === "dir"
|
|
@@ -259,7 +310,6 @@ export function createDefaultChildSessionFactory(options: DefaultChildSessionFac
|
|
|
259
310
|
: launch.storage.kind === "memory"
|
|
260
311
|
? pi.SessionManager.inMemory(launch.cwd)
|
|
261
312
|
: pi.SessionManager.create(launch.cwd);
|
|
262
|
-
observeReadonly?.opened(sessionManager);
|
|
263
313
|
const resolvedModel = launch.model
|
|
264
314
|
? pi.resolveCliModel({ cliModel: launch.model, modelRuntime })
|
|
265
315
|
: undefined;
|
|
@@ -277,6 +327,7 @@ export function createDefaultChildSessionFactory(options: DefaultChildSessionFac
|
|
|
277
327
|
settingsManager,
|
|
278
328
|
sessionStartEvent: { type: "session_start", reason: "startup" },
|
|
279
329
|
});
|
|
330
|
+
pinChildCacheRetention(session.agent);
|
|
280
331
|
try {
|
|
281
332
|
await session.bindExtensions({
|
|
282
333
|
mode: "print",
|
|
@@ -291,9 +342,6 @@ export function createDefaultChildSessionFactory(options: DefaultChildSessionFac
|
|
|
291
342
|
const opened = loading.catch(() => {}).then(open);
|
|
292
343
|
loading = opened;
|
|
293
344
|
const session = await opened;
|
|
294
|
-
let evidence: ReturnType<NonNullable<typeof observeReadonly>["observe"]>;
|
|
295
|
-
try { evidence = observeReadonly?.observe(pi, modelRuntime, session); }
|
|
296
|
-
catch (error) { session.dispose(); throw error; }
|
|
297
345
|
let pending: Promise<void> | undefined;
|
|
298
346
|
// pi's own hosts emit `session_shutdown` before disposing a session so the
|
|
299
347
|
// extensions loaded into it (ambient extensions included) release their
|
|
@@ -302,28 +350,20 @@ export function createDefaultChildSessionFactory(options: DefaultChildSessionFac
|
|
|
302
350
|
try {
|
|
303
351
|
const runner = session.extensionRunner;
|
|
304
352
|
if (runner.hasHandlers("session_shutdown")) {
|
|
305
|
-
|
|
306
|
-
const settled = await Promise.race([runner.emit({ type: "session_shutdown", reason: "quit" }).then(() => true), new Promise<false>((resolve) => setTimeout(() => resolve(false), shutdownTimeoutMs).unref?.())]);
|
|
307
|
-
if (!settled) evidence?.invalidate();
|
|
353
|
+
await Promise.race([runner.emit({ type: "session_shutdown", reason: "quit" }), new Promise<void>((resolve) => setTimeout(resolve, shutdownTimeoutMs).unref?.())]);
|
|
308
354
|
}
|
|
309
355
|
} catch (error) {
|
|
310
|
-
evidence?.invalidate();
|
|
311
356
|
launch.onExtensionError?.({ extensionPath: "<session>", event: "session_shutdown", error });
|
|
312
357
|
} finally {
|
|
313
358
|
session.dispose();
|
|
314
|
-
evidence?.finish(child);
|
|
315
359
|
}
|
|
316
360
|
};
|
|
317
361
|
const child: ChildSession = {
|
|
318
362
|
subscribe: (listener) => session.subscribe((event) => listener(event as unknown as ChildSessionEvent)),
|
|
319
|
-
prompt: (text) =>
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
},
|
|
324
|
-
steer: (text) => { evidence?.invalidate(); return session.steer(text); },
|
|
325
|
-
followUp: (text) => { evidence?.invalidate(); return session.followUp(text); },
|
|
326
|
-
abort: () => { evidence?.invalidate(); return session.abort(); },
|
|
363
|
+
prompt: (text) => session.prompt(text),
|
|
364
|
+
steer: (text) => session.steer(text),
|
|
365
|
+
followUp: (text) => session.followUp(text),
|
|
366
|
+
abort: () => session.abort(),
|
|
327
367
|
hasQueuedMessages: () => session.agent?.hasQueuedMessages?.() === true,
|
|
328
368
|
dispose: () => {
|
|
329
369
|
if (!pending) {
|
|
@@ -340,16 +380,6 @@ export function createDefaultChildSessionFactory(options: DefaultChildSessionFac
|
|
|
340
380
|
get sessionId() { return session.sessionId; },
|
|
341
381
|
get modelId() { return session.model ? `${session.model.provider}/${session.model.id}` : undefined; },
|
|
342
382
|
};
|
|
343
|
-
if (evidence && session.model) readonlyModels.set(child, {
|
|
344
|
-
current: toModelInfo(session.model),
|
|
345
|
-
requestBytes: Buffer.byteLength(session.systemPrompt) + Buffer.byteLength(JSON.stringify(session.agent.state.tools)),
|
|
346
|
-
resolve(reference) {
|
|
347
|
-
try {
|
|
348
|
-
const resolved = pi.resolveCliModel({ cliModel: reference, modelRuntime });
|
|
349
|
-
return !resolved.error && resolved.model ? toModelInfo(resolved.model) : undefined;
|
|
350
|
-
} catch { return undefined; }
|
|
351
|
-
},
|
|
352
|
-
});
|
|
353
383
|
live.add(child);
|
|
354
384
|
return child;
|
|
355
385
|
},
|
|
@@ -371,10 +401,24 @@ let activeFactoryModule: string | undefined;
|
|
|
371
401
|
|
|
372
402
|
/** The process-wide factory foreground runs use unless a run passes its own. */
|
|
373
403
|
export function childSessionFactory(): ChildSessionFactory {
|
|
374
|
-
activeFactory ??= createDefaultChildSessionFactory();
|
|
404
|
+
activeFactory ??= createLazyPlacementFactory(createDefaultChildSessionFactory());
|
|
375
405
|
return activeFactory;
|
|
376
406
|
}
|
|
377
407
|
|
|
408
|
+
function createLazyPlacementFactory(local: ChildSessionFactory): ChildSessionFactory {
|
|
409
|
+
let placed: ChildSessionFactory | undefined;
|
|
410
|
+
const factory = async () => placed ??= (await import("./herdr-placed-run.ts")).createPlacementAwareChildSessionFactory(local);
|
|
411
|
+
return {
|
|
412
|
+
async create(launch) { return launch.machine ? (await factory()).create(launch) : local.create(launch); },
|
|
413
|
+
async dispose() { if (placed) await placed.dispose(); else await local.dispose(); },
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** Default factory including pane-native placement; detached runners use the same boundary. */
|
|
418
|
+
export function createPlacementChildSessionFactory(options: DefaultChildSessionFactoryOptions = {}): ChildSessionFactory {
|
|
419
|
+
return createLazyPlacementFactory(createDefaultChildSessionFactory(options));
|
|
420
|
+
}
|
|
421
|
+
|
|
378
422
|
/**
|
|
379
423
|
* Replace the process-wide factory. Tests install a scripted factory; passing
|
|
380
424
|
* undefined restores the default on next use.
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
import { THINKING_LEVELS } from "../../shared/model-info.ts";
|
|
23
23
|
import { getAgentDir } from "../../shared/utils.ts";
|
|
24
24
|
import type { PermissionRules } from "./permissions.ts";
|
|
25
|
+
import { snapshotRequiredChildExtensions, type RequiredChildExtensionSnapshot } from "../../shared/required-child-extensions.ts";
|
|
25
26
|
import {
|
|
26
27
|
capabilityCeilingAgentRestrictionSources,
|
|
27
28
|
intersectSubagentCapabilityCeilings,
|
|
@@ -33,18 +34,18 @@ import {
|
|
|
33
34
|
const MAX_LAUNCH_RESOLVED_EXTENSION_IDS = 32;
|
|
34
35
|
const PROMPT_RUNTIME_EXTENSION_PATH = path.join(
|
|
35
36
|
path.dirname(fileURLToPath(import.meta.url)),
|
|
36
|
-
|
|
37
|
+
`subagent-prompt-runtime${path.extname(fileURLToPath(import.meta.url))}`,
|
|
37
38
|
);
|
|
38
39
|
const FANOUT_CHILD_EXTENSION_PATH = path.join(
|
|
39
40
|
path.dirname(fileURLToPath(import.meta.url)),
|
|
40
41
|
"..",
|
|
41
42
|
"..",
|
|
42
43
|
"extension",
|
|
43
|
-
|
|
44
|
+
`fanout-child${path.extname(fileURLToPath(import.meta.url))}`,
|
|
44
45
|
);
|
|
45
46
|
const FAST_MODE_EXTENSION_PATH = path.join(
|
|
46
47
|
path.dirname(fileURLToPath(import.meta.url)),
|
|
47
|
-
|
|
48
|
+
`fast-mode-extension${path.extname(fileURLToPath(import.meta.url))}`,
|
|
48
49
|
);
|
|
49
50
|
const SUBAGENT_RUNTIME_EXTENSION_PATHS = new Set([
|
|
50
51
|
PROMPT_RUNTIME_EXTENSION_PATH,
|
|
@@ -64,8 +65,6 @@ const OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH = 64;
|
|
|
64
65
|
const PI_BUILTIN_TOOL_NAMES = new Set(["read", "bash", "powershell", "edit", "write", "grep", "find", "ls"]);
|
|
65
66
|
const REPOSITORY_INSPECTION_TOOLS = new Set(["read", "grep", "find", "ls", "bash", "powershell"]);
|
|
66
67
|
const REVIEW_OR_SCOUT_AGENT_PATTERN = /\b(?:reviewer|scout)\b/i;
|
|
67
|
-
// These providers come from child hooks, not the host's builtin tool registry.
|
|
68
|
-
const NATIVE_COORDINATION_TOOL_NAMES = new Set(["subagent", "contact_supervisor", "subagent_supervisor"]);
|
|
69
68
|
|
|
70
69
|
export function isReviewOrScoutLaneAgent(agentName: string | undefined): boolean {
|
|
71
70
|
return typeof agentName === "string" && REVIEW_OR_SCOUT_AGENT_PATTERN.test(agentName);
|
|
@@ -149,9 +148,9 @@ function stripThinkingSuffix(model: string): string {
|
|
|
149
148
|
: model;
|
|
150
149
|
}
|
|
151
150
|
|
|
152
|
-
function resolveFastModeExtension(input: Pick<ResolvePiLaunchToolPlanInput, "fast" | "model" | "
|
|
151
|
+
function resolveFastModeExtension(input: Pick<ResolvePiLaunchToolPlanInput, "fast" | "model" | "agentName">): string[] {
|
|
153
152
|
if (!input.fast) return [];
|
|
154
|
-
const candidates = (input.
|
|
153
|
+
const candidates = (input.model ? [input.model] : [])
|
|
155
154
|
.map(stripThinkingSuffix);
|
|
156
155
|
if (candidates.length === 0) {
|
|
157
156
|
throw new Error(`fast mode requires an explicit supported native OpenAI-Codex model${input.agentName ? ` for agent '${input.agentName}'` : ""}.`);
|
|
@@ -169,6 +168,7 @@ export interface ResolvePiLaunchToolPlanInput {
|
|
|
169
168
|
allowNestedSubagents?: boolean;
|
|
170
169
|
extensions?: string[];
|
|
171
170
|
subagentOnlyExtensions?: string[];
|
|
171
|
+
requiredExtensions?: RequiredChildExtensionSnapshot;
|
|
172
172
|
mcpDirectTools?: string[];
|
|
173
173
|
cwd?: string;
|
|
174
174
|
requireReadTool?: boolean;
|
|
@@ -181,19 +181,19 @@ export interface ResolvePiLaunchToolPlanInput {
|
|
|
181
181
|
};
|
|
182
182
|
fast?: boolean;
|
|
183
183
|
model?: string;
|
|
184
|
-
modelCandidates?: readonly string[];
|
|
185
184
|
capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
|
|
186
185
|
inheritedCapabilityCeiling?: ResolvedSubagentCapabilityCeiling;
|
|
187
186
|
agentName?: string;
|
|
188
187
|
permissionRules?: PermissionRules;
|
|
189
188
|
runtimeSnapshotHost?: McpRuntimeSnapshotHost;
|
|
190
189
|
/**
|
|
191
|
-
* When provided, child tool plans intersect
|
|
192
|
-
* this set.
|
|
190
|
+
* When provided, child tool plans intersect known Pi core tool slots with
|
|
191
|
+
* this set. Core tools the agent declares but the host does not provide are
|
|
193
192
|
* omitted with a non-fatal warning (tracked in `unavailableHostBuiltins`).
|
|
194
193
|
* Review/scout lanes fail closed when a requested, still-permitted
|
|
195
194
|
* repository inspection tool is among those host omissions. Intentionally
|
|
196
195
|
* empty or ceiling-restricted allowlists are not a minimum-tool contract.
|
|
196
|
+
* Non-core names remain allowed and are validated in the child's registry.
|
|
197
197
|
*/
|
|
198
198
|
hostAvailableBuiltins?: readonly string[];
|
|
199
199
|
}
|
|
@@ -214,6 +214,7 @@ export interface PiLaunchToolPlan {
|
|
|
214
214
|
fanoutAuthorized: boolean;
|
|
215
215
|
runtimeExtensions: string[];
|
|
216
216
|
configuredExtensions: string[];
|
|
217
|
+
requiredExtensions: RequiredChildExtensionSnapshot;
|
|
217
218
|
extensionArgs: string[];
|
|
218
219
|
disableAmbientExtensions: boolean;
|
|
219
220
|
capabilityAudit?: SubagentCapabilityAudit;
|
|
@@ -258,6 +259,7 @@ export function projectLaunchResolvedChildExtensions(
|
|
|
258
259
|
PiLaunchToolPlan,
|
|
259
260
|
| "runtimeExtensions"
|
|
260
261
|
| "configuredExtensions"
|
|
262
|
+
| "requiredExtensions"
|
|
261
263
|
| "extensionArgs"
|
|
262
264
|
| "disableAmbientExtensions"
|
|
263
265
|
>,
|
|
@@ -271,10 +273,12 @@ export function projectLaunchResolvedChildExtensions(
|
|
|
271
273
|
disableAmbientExtensions: toolPlan.disableAmbientExtensions,
|
|
272
274
|
runtime: runtime.ids,
|
|
273
275
|
configured: configured.ids,
|
|
276
|
+
required: toolPlan.requiredExtensions.map(({ id }) => id),
|
|
274
277
|
effective: effective.ids,
|
|
275
278
|
omitted: {
|
|
276
279
|
runtime: runtime.omitted,
|
|
277
280
|
configured: configured.omitted,
|
|
281
|
+
required: 0,
|
|
278
282
|
effective: effective.omitted,
|
|
279
283
|
},
|
|
280
284
|
};
|
|
@@ -339,7 +343,8 @@ export function resolvePermissionSystemExtension(): string | undefined {
|
|
|
339
343
|
/**
|
|
340
344
|
* Extract the names of builtin tools the host provides. Use this to pass
|
|
341
345
|
* `hostAvailableBuiltins` to `resolvePiLaunchToolPlan` so child tool plans
|
|
342
|
-
* intersect
|
|
346
|
+
* intersect known core slots with what the host actually supports. Wrapped
|
|
347
|
+
* core slots count regardless of source; host-specific builtins count too.
|
|
343
348
|
*
|
|
344
349
|
* Returns `undefined` when builtin tool discovery fails or yields nothing,
|
|
345
350
|
* so callers skip the intersection (fail-safe to allowing all declared tools).
|
|
@@ -352,7 +357,7 @@ export function getHostBuiltinToolNames(pi: Pick<ExtensionAPI, "getAllTools">):
|
|
|
352
357
|
.getAllTools()
|
|
353
358
|
.filter((tool) => {
|
|
354
359
|
const source = (tool.sourceInfo as { source?: string } | undefined)?.source;
|
|
355
|
-
return source === "builtin" ||
|
|
360
|
+
return source === "builtin" || PI_BUILTIN_TOOL_NAMES.has(tool.name);
|
|
356
361
|
})
|
|
357
362
|
.map((tool) => tool.name);
|
|
358
363
|
return builtins.length > 0 ? builtins : undefined;
|
|
@@ -368,6 +373,10 @@ export function resolvePiLaunchToolPlan(
|
|
|
368
373
|
input.capabilityCeiling,
|
|
369
374
|
input.inheritedCapabilityCeiling,
|
|
370
375
|
);
|
|
376
|
+
const requiredExtensions = snapshotRequiredChildExtensions(input.requiredExtensions ?? []);
|
|
377
|
+
if (requiredExtensions.length > 0 && capabilityCeiling?.denyExtensions) {
|
|
378
|
+
throw new Error(`Capability ceiling from ${capabilityCeiling.sources.join(", ") || "unknown source"} denies extensions but this host requires: ${requiredExtensions.map(({ id }) => id).join(", ")}.`);
|
|
379
|
+
}
|
|
371
380
|
const allowedToolSet =
|
|
372
381
|
capabilityCeiling?.allowedTools === undefined
|
|
373
382
|
? undefined
|
|
@@ -405,10 +414,10 @@ export function resolvePiLaunchToolPlan(
|
|
|
405
414
|
: requestedBuiltinTools
|
|
406
415
|
).filter((tool) => !allowedToolSet || allowedToolSet.has(tool));
|
|
407
416
|
const declaredBuiltinTools = hostAvailableSet
|
|
408
|
-
? ceilingFilteredBuiltinTools.filter((tool) =>
|
|
417
|
+
? ceilingFilteredBuiltinTools.filter((tool) => !PI_BUILTIN_TOOL_NAMES.has(tool) || hostAvailableSet.has(tool))
|
|
409
418
|
: ceilingFilteredBuiltinTools;
|
|
410
419
|
const unavailableHostBuiltins = hostAvailableSet
|
|
411
|
-
? ceilingFilteredBuiltinTools.filter((tool) =>
|
|
420
|
+
? ceilingFilteredBuiltinTools.filter((tool) => PI_BUILTIN_TOOL_NAMES.has(tool) && !hostAvailableSet.has(tool))
|
|
412
421
|
: [];
|
|
413
422
|
const excludeTools = [...new Set((input.excludeTools ?? []).map((tool) => tool.trim()).filter(Boolean))];
|
|
414
423
|
const excludedToolSet = new Set(excludeTools);
|
|
@@ -483,7 +492,7 @@ export function resolvePiLaunchToolPlan(
|
|
|
483
492
|
? resolvePermissionSystemExtension()
|
|
484
493
|
: undefined;
|
|
485
494
|
if (input.fast && capabilityCeiling?.denyExtensions) throw new Error("fast mode requires a child runtime extension, but this launch denies extensions.");
|
|
486
|
-
const fastModeExtensions = resolveFastModeExtension({ fast: input.fast, model: input.model,
|
|
495
|
+
const fastModeExtensions = resolveFastModeExtension({ fast: input.fast, model: input.model, agentName: input.agentName });
|
|
487
496
|
const runtimeExtensions = [
|
|
488
497
|
PROMPT_RUNTIME_EXTENSION_PATH,
|
|
489
498
|
...fastModeExtensions,
|
|
@@ -510,7 +519,7 @@ export function resolvePiLaunchToolPlan(
|
|
|
510
519
|
...(input.extensions ?? []),
|
|
511
520
|
...(input.subagentOnlyExtensions ?? []),
|
|
512
521
|
];
|
|
513
|
-
const
|
|
522
|
+
const ordinaryExtensionArgs = disableAmbientExtensions
|
|
514
523
|
? [...new Set([...runtimeExtensions, ...configuredExtensions])]
|
|
515
524
|
: [
|
|
516
525
|
...new Set([
|
|
@@ -519,6 +528,8 @@ export function resolvePiLaunchToolPlan(
|
|
|
519
528
|
...(input.subagentOnlyExtensions ?? []),
|
|
520
529
|
]),
|
|
521
530
|
];
|
|
531
|
+
// Host-required paths have final precedence and cannot be removed by agent defaults or overrides.
|
|
532
|
+
const extensionArgs = [...new Set([...ordinaryExtensionArgs, ...requiredExtensions.map(({ path }) => path)])];
|
|
522
533
|
const requestedToolNames =
|
|
523
534
|
input.tools !== undefined
|
|
524
535
|
? [
|
|
@@ -610,6 +621,7 @@ export function resolvePiLaunchToolPlan(
|
|
|
610
621
|
fanoutAuthorized,
|
|
611
622
|
runtimeExtensions,
|
|
612
623
|
configuredExtensions,
|
|
624
|
+
requiredExtensions,
|
|
613
625
|
extensionArgs,
|
|
614
626
|
disableAmbientExtensions,
|
|
615
627
|
warnings,
|
|
@@ -46,12 +46,12 @@ const DYNAMIC_STEP_KEYS = new Set(["expand", "parallel", "collect", "concurrency
|
|
|
46
46
|
const RUNNER_DYNAMIC_STEP_KEYS = new Set([...DYNAMIC_STEP_KEYS, "effectiveAcceptance", "acceptanceInput", "acceptanceRole", "sessionFiles", "thinkingOverrides"]);
|
|
47
47
|
const DYNAMIC_EXPAND_KEYS = new Set(["from", "item", "key", "maxItems", "onEmpty"]);
|
|
48
48
|
const DYNAMIC_EXPAND_FROM_KEYS = new Set(["output", "path"]);
|
|
49
|
-
const DYNAMIC_PARALLEL_KEYS = new Set(["agent", "task", "phase", "label", "outputSchema", "cwd", "output", "outputMode", "reads", "progress", "skill", "model", "fast", "toolBudget", "acceptance", "agentContract", "gateOn"]);
|
|
49
|
+
const DYNAMIC_PARALLEL_KEYS = new Set(["agent", "task", "phase", "label", "outputSchema", "cwd", "machine", "output", "outputMode", "reads", "progress", "skill", "model", "fast", "toolBudget", "acceptance", "agentContract", "gateOn"]);
|
|
50
50
|
const RUNNER_DYNAMIC_PARALLEL_KEYS = new Set([
|
|
51
51
|
...DYNAMIC_PARALLEL_KEYS,
|
|
52
52
|
"outputName", "structured", "inheritProjectContext", "inheritGlobalContext", "inheritSkills", "skills", "outputPath", "namespaceOutputPath", "maxSubagentDepth", "timeoutMs", "waitToolEnabled", "waitToolDefaultTimeoutMs",
|
|
53
53
|
"structuredOutput", "structuredOutputSchema", "tools", "excludeTools", "allowNestedSubagents", "extensions", "subagentOnlyExtensions", "mcpDirectTools", "mutationTools", "capabilityCeiling", "completionGuard", "systemPrompt",
|
|
54
|
-
"systemPromptMode", "thinking", "
|
|
54
|
+
"systemPromptMode", "thinking", "requestedModel", "sessionFile", "effectiveAcceptance", "acceptanceInput", "acceptanceRole", "parentSessionId", "requiredExtensions", "launchResolvedExtensions", "requestedCwd", "machineEnv",
|
|
55
55
|
]);
|
|
56
56
|
const DYNAMIC_COLLECT_KEYS = new Set(["as", "outputSchema"]);
|
|
57
57
|
|
|
@@ -2,7 +2,9 @@ import type {
|
|
|
2
2
|
ExternalCliReceiptMetadata,
|
|
3
3
|
ExternalCliCapabilityNarrowing,
|
|
4
4
|
ExternalCliRunnerStatus,
|
|
5
|
+
ExternalCliMachineStatus,
|
|
5
6
|
ExternalProcessStatus,
|
|
7
|
+
HerdrMachineReference,
|
|
6
8
|
} from "../../shared/types.ts";
|
|
7
9
|
|
|
8
10
|
const UNSUPPORTED = {
|
|
@@ -80,6 +82,7 @@ export function resolveExternalCliRunnerStatus(input: {
|
|
|
80
82
|
args?: string[];
|
|
81
83
|
promptDelivery?: "stdin";
|
|
82
84
|
capabilities?: ExternalCliCapabilityNarrowing;
|
|
85
|
+
machine?: HerdrMachineReference;
|
|
83
86
|
}): ExternalCliRunnerStatus {
|
|
84
87
|
const codexExec = input.adapter === "codex-exec";
|
|
85
88
|
const codexExecWriter = input.adapter === "codex-exec-writer";
|
|
@@ -101,6 +104,7 @@ export function resolveExternalCliRunnerStatus(input: {
|
|
|
101
104
|
...(claudeCodeWriter ? { safety: { access: "workspace-write" as const, authentication: "existing-cli-required" as const, permissionMode: "acceptEdits" as const, tools: "Read,Write,Edit,Glob,Grep" as const, mcp: "empty-strict" as const, settingSources: "user" as const, userSettingsTrust: "required" as const, sessionPersistence: false as const } } : {}),
|
|
102
105
|
...(cursorAgent ? { safety: { access: "read-only" as const, authentication: "cursor-api-key-or-existing-login" as const, mode: "ask" as const, sandbox: "enabled" as const, workspaceTrust: "existing-required" as const, sessionReuse: false as const } } : {}),
|
|
103
106
|
...(cursorAgentWriter ? { safety: { access: "workspace-write" as const, authentication: "cursor-api-key-or-existing-login" as const, mode: "print" as const, sandbox: "enabled" as const, workspaceTrust: "existing-required" as const, sessionReuse: false as const } } : {}),
|
|
107
|
+
...(input.machine ? { machine: input.machine } : {}),
|
|
104
108
|
capabilities: {
|
|
105
109
|
stop: true,
|
|
106
110
|
steer: false,
|
|
@@ -124,6 +128,9 @@ export function normalizeExternalCliRunnerStatus(value: unknown): ExternalCliRun
|
|
|
124
128
|
? input.args
|
|
125
129
|
: undefined;
|
|
126
130
|
const promptDelivery = input.promptDelivery === "stdin" ? "stdin" : undefined;
|
|
131
|
+
const machine = input.machine && typeof input.machine === "object" && !Array.isArray(input.machine)
|
|
132
|
+
? input.machine as HerdrMachineReference
|
|
133
|
+
: undefined;
|
|
127
134
|
const adapterId = input.adapter && typeof input.adapter === "object" && !Array.isArray(input.adapter)
|
|
128
135
|
? (input.adapter as Record<string, unknown>).id
|
|
129
136
|
: undefined;
|
|
@@ -131,6 +138,7 @@ export function normalizeExternalCliRunnerStatus(value: unknown): ExternalCliRun
|
|
|
131
138
|
return {
|
|
132
139
|
type: "external-cli",
|
|
133
140
|
command: input.command,
|
|
141
|
+
...(machine ? { machine } : {}),
|
|
134
142
|
args: args ?? [],
|
|
135
143
|
promptDelivery: "prompt-file",
|
|
136
144
|
adapter: { id: "grok-build", version: 1, executionMode: "one-shot-prompt-file" },
|
|
@@ -140,7 +148,7 @@ export function normalizeExternalCliRunnerStatus(value: unknown): ExternalCliRun
|
|
|
140
148
|
};
|
|
141
149
|
}
|
|
142
150
|
const adapter = isCodeOwnedExternalCliAdapterId(adapterId) ? adapterId : undefined;
|
|
143
|
-
return resolveExternalCliRunnerStatus({ ...(adapter ? { adapter } : {}), command: input.command, ...(args ? { args } : {}), ...(promptDelivery ? { promptDelivery } : {}) });
|
|
151
|
+
return resolveExternalCliRunnerStatus({ ...(adapter ? { adapter } : {}), command: input.command, ...(args ? { args } : {}), ...(promptDelivery ? { promptDelivery } : {}), ...(machine ? { machine } : {}) });
|
|
144
152
|
}
|
|
145
153
|
|
|
146
154
|
export function externalCliReceiptMetadata(input: {
|
|
@@ -149,9 +157,11 @@ export function externalCliReceiptMetadata(input: {
|
|
|
149
157
|
outputReference?: string;
|
|
150
158
|
}): ExternalCliReceiptMetadata {
|
|
151
159
|
const { runner } = input;
|
|
160
|
+
const machine: ExternalCliMachineStatus | undefined = input.externalProcess?.machine ?? runner.machine;
|
|
152
161
|
return {
|
|
153
162
|
adapter: { ...runner.adapter },
|
|
154
163
|
capabilities: { ...runner.capabilities },
|
|
164
|
+
...(machine ? { machine: { ...machine, ...(machine.remoteGit ? { remoteGit: { ...machine.remoteGit } } : {}) } } : {}),
|
|
155
165
|
...(runner.safety ? { safety: { ...runner.safety } } : {}),
|
|
156
166
|
...(input.externalProcess ? {
|
|
157
167
|
outputArtifacts: {
|
|
@@ -4,6 +4,7 @@ import * as path from "node:path";
|
|
|
4
4
|
|
|
5
5
|
const MAX_PROBE_OUTPUT_BYTES = 256 * 1024;
|
|
6
6
|
const MAX_PROBE_TIMEOUT_MS = 5_000;
|
|
7
|
+
const MAX_REMOTE_PROBE_TIMEOUT_MS = 15_000;
|
|
7
8
|
const MAX_CACHE_ENTRIES = 64;
|
|
8
9
|
const MAX_AVAILABILITY_REASON_LENGTH = 256;
|
|
9
10
|
|
|
@@ -16,6 +17,8 @@ export interface ExternalCliPreflightSpec {
|
|
|
16
17
|
evidenceArgs?: readonly string[];
|
|
17
18
|
evidenceLabel?: string;
|
|
18
19
|
probeTimeoutMs?: number;
|
|
20
|
+
/** Remote probes include SSH handshakes; values may only narrow the code-owned remote ceiling. */
|
|
21
|
+
remote?: boolean;
|
|
19
22
|
validate?: (result: ExternalCliPreflightResult) => void;
|
|
20
23
|
}
|
|
21
24
|
|
|
@@ -90,7 +93,7 @@ function narrowPositiveInteger(value: number | undefined, ceiling: number, label
|
|
|
90
93
|
}
|
|
91
94
|
|
|
92
95
|
function specKey(spec: ExternalCliPreflightSpec): string {
|
|
93
|
-
return JSON.stringify([spec.id, spec.versionArgs, spec.helpArgs, spec.evidenceArgs, spec.evidenceLabel, spec.probeTimeoutMs]);
|
|
96
|
+
return JSON.stringify([spec.id, spec.versionArgs, spec.helpArgs, spec.evidenceArgs, spec.evidenceLabel, spec.probeTimeoutMs, spec.remote === true]);
|
|
94
97
|
}
|
|
95
98
|
|
|
96
99
|
export function preflightExternalCli(command: string, spec: ExternalCliPreflightSpec, env: NodeJS.ProcessEnv, cwd?: string): ExternalCliPreflightResult {
|
|
@@ -99,7 +102,8 @@ export function preflightExternalCli(command: string, spec: ExternalCliPreflight
|
|
|
99
102
|
const lookupKey = JSON.stringify([binaryPath, binaryMtimeMs, specKey(spec)]);
|
|
100
103
|
const cachedKey = lookup.get(lookupKey);
|
|
101
104
|
const cached = cachedKey ? cache.get(cachedKey) : undefined;
|
|
102
|
-
const
|
|
105
|
+
const ceiling = spec.remote === true ? MAX_REMOTE_PROBE_TIMEOUT_MS : MAX_PROBE_TIMEOUT_MS;
|
|
106
|
+
const probeTimeoutMs = narrowPositiveInteger(spec.probeTimeoutMs, ceiling, "probeTimeoutMs");
|
|
103
107
|
const base = cached ?? {
|
|
104
108
|
binaryPath,
|
|
105
109
|
binaryMtimeMs,
|