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
|
@@ -14,12 +14,14 @@ import { appendAgentRefinementOverlay } from "../../agents/agent-refinements.ts"
|
|
|
14
14
|
import { createAtomicJsonWriter, writePrivateAtomicJson } from "../../shared/atomic-json.ts";
|
|
15
15
|
import { currentCompletionOwnerId } from "../../shared/completion-owner.ts";
|
|
16
16
|
import { planChildLaunch, resolveStepBehavior, suppressProgressForReadOnlyTask, type ResolvedStepBehavior } from "../shared/child-launch-plan.ts";
|
|
17
|
-
import { applyThinkingSuffix, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan } from "../shared/
|
|
17
|
+
import { applyThinkingSuffix, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan } from "../shared/child-tool-plan.ts";
|
|
18
18
|
import { injectOutputPathSystemPrompt, injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
|
|
19
|
+
import { applyWatchdogLaunchRules, sendRuleViolationWarning } from "../../watchdog/rules.ts";
|
|
19
20
|
import { buildChainInstructions, isDynamicParallelStep, isParallelStep, resolveExistingReadInstructionPaths, resolveExistingReadPaths, writeInitialProgressFile, type ChainStep, type SequentialStep, type StepOverrides } from "../../shared/settings.ts";
|
|
20
21
|
import type { RunnerStep } from "../shared/parallel-utils.ts";
|
|
21
22
|
import type { ContextMode } from "../shared/context-mode.ts";
|
|
22
|
-
import { resolveInstalledPiPackageRoot, resolvePiPackageRoot } from "../shared/pi-spawn.ts";
|
|
23
|
+
import { PI_CODING_AGENT_PACKAGE, resolveInstalledPiPackageRoot, resolvePiPackageRoot } from "../shared/pi-spawn.ts";
|
|
24
|
+
import { JITI_ALIAS_ENV, resolveHostPeerAliases } from "./runner-aliases.ts";
|
|
23
25
|
import { preflightLaunchCwd } from "../shared/launch-cwd.ts";
|
|
24
26
|
import { resolveNodeExecutable } from "../../shared/node-executable.ts";
|
|
25
27
|
import { backgroundProcessOptions } from "../shared/background-process-options.ts";
|
|
@@ -30,7 +32,7 @@ import { buildModelCandidates, resolveEffectiveSubagentModel, resolveModelOrigin
|
|
|
30
32
|
import { resolveToolTimeoutMs, toolTimeoutFromEnv } from "../shared/tool-timeout.ts";
|
|
31
33
|
import { resolveModelScopesForAgent, type ModelScopeConfig } from "../shared/model-scope.ts";
|
|
32
34
|
import { findModelInfo, resolveEffectiveThinking } from "../../shared/model-info.ts";
|
|
33
|
-
import { assertThinkingWithinCeiling,
|
|
35
|
+
import { assertThinkingWithinCeiling, intersectThinkingCeilings, type ThinkingLevel } from "../../shared/thinking-ceiling.ts";
|
|
34
36
|
import { resolveExpectedWorktreeAgentCwd, resolveWorktreeProvider, shouldDeferWorktreeCwd, WORKTREE_AGENT_CWD_PLACEHOLDER } from "../shared/worktree.ts";
|
|
35
37
|
import { buildWorkflowGraphSnapshot } from "../shared/workflow-graph.ts";
|
|
36
38
|
import { ChainOutputValidationError, validateChainOutputBindings } from "../shared/chain-outputs.ts";
|
|
@@ -64,7 +66,10 @@ import {
|
|
|
64
66
|
getAsyncConfigPath,
|
|
65
67
|
resolveChildMaxSubagentDepth,
|
|
66
68
|
} from "../../shared/types.ts";
|
|
67
|
-
import {
|
|
69
|
+
import { inheritedNestedParentAddressOf, inheritedNestedRouteOf, nestedResultsPath, nestedSummaryFromAsyncStatus, writeNestedEvent } from "../shared/nested-events.ts";
|
|
70
|
+
import type { ChildRuntimeConfig } from "../shared/child-runtime-config.ts";
|
|
71
|
+
import { childSessionFactoryModule } from "../shared/child-session.ts";
|
|
72
|
+
import { inheritedChildRuntime } from "../shared/child-launch.ts";
|
|
68
73
|
import { resultFilePath } from "./result-files.ts";
|
|
69
74
|
import { validateToolBudgetConfig } from "../shared/tool-budget.ts";
|
|
70
75
|
import { usageBudgetState } from "../shared/usage-budget.ts";
|
|
@@ -74,7 +79,7 @@ import { finalizeProcessTerminal, initializeProcessTerminal, readProcessTerminal
|
|
|
74
79
|
import type { ActiveAsyncCapacityHandle } from "./active-async-capacity.ts";
|
|
75
80
|
import { statusStepDescription } from "./chain-append.ts";
|
|
76
81
|
import { SUBAGENT_PROCESS_TERMINAL_EVENT } from "../../shared/types.ts";
|
|
77
|
-
import { assertAgentAllowedByCapabilityCeiling,
|
|
82
|
+
import { assertAgentAllowedByCapabilityCeiling, intersectSubagentCapabilityCeilings, resolveCurrentSubagentCapabilityCeiling, type ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
|
|
78
83
|
import { agentDefinitionDigest, launchBindingDigest } from "../../shared/launch-contract.ts";
|
|
79
84
|
import { resolvePermissionRules, type PermissionConfig } from "../shared/permissions.ts";
|
|
80
85
|
import { normalizeExtensionBindings, omitExtensionBindingsEnv, type ExtensionBindings } from "../shared/extension-bindings.ts";
|
|
@@ -143,6 +148,8 @@ interface AsyncExecutionContext {
|
|
|
143
148
|
modelScope?: ModelScopeConfig;
|
|
144
149
|
/** Whether the parent session has an interactive UI. */
|
|
145
150
|
interactive?: boolean;
|
|
151
|
+
/** The executor's own child runtime when the launch comes from an in-process child. */
|
|
152
|
+
childRuntime?: ChildRuntimeConfig;
|
|
146
153
|
}
|
|
147
154
|
|
|
148
155
|
export const DEFAULT_ASYNC_TIMEOUT_MS = 30 * 60 * 1000;
|
|
@@ -178,6 +185,7 @@ interface AsyncChainParams {
|
|
|
178
185
|
worktreeSetupHook?: string;
|
|
179
186
|
worktreeSetupHookTimeoutMs?: number;
|
|
180
187
|
worktreeBaseDir?: string;
|
|
188
|
+
baseRef?: string;
|
|
181
189
|
worktreeProvider?: import("../../shared/types.ts").WorktreeProvider;
|
|
182
190
|
worktreeBranchPrefix?: string;
|
|
183
191
|
controlConfig?: ResolvedControlConfig;
|
|
@@ -247,6 +255,7 @@ interface AsyncSingleParams {
|
|
|
247
255
|
worktreeSetupHook?: string;
|
|
248
256
|
worktreeSetupHookTimeoutMs?: number;
|
|
249
257
|
worktreeBaseDir?: string;
|
|
258
|
+
baseRef?: string;
|
|
250
259
|
worktreeProvider?: import("../../shared/types.ts").WorktreeProvider;
|
|
251
260
|
worktreeBranchPrefix?: string;
|
|
252
261
|
worktree?: boolean;
|
|
@@ -535,6 +544,13 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, initialStatus: Om
|
|
|
535
544
|
if (!jitiCliPath) {
|
|
536
545
|
return { error: "upstream jiti for TypeScript execution could not be found; ensure package dependencies are installed" };
|
|
537
546
|
}
|
|
547
|
+
if (!piPackageRoot) {
|
|
548
|
+
return { error: `Background children require pi installed as the npm package (${PI_CODING_AGENT_PACKAGE}); a standalone pi binary has no package directory, so the async runner cannot create child sessions. Run this child in the foreground (async: false) or install pi from npm.` };
|
|
549
|
+
}
|
|
550
|
+
const hostPeerAliases = resolveHostPeerAliases(piPackageRoot);
|
|
551
|
+
if (hostPeerAliases.missing.length > 0) {
|
|
552
|
+
return { error: `Background children require pi installed as the npm package (${PI_CODING_AGENT_PACKAGE}) with its dependencies; ${piPackageRoot} does not provide ${hostPeerAliases.missing.join(", ")}, so the async runner cannot create child sessions. A standalone pi binary cannot run background children.` };
|
|
553
|
+
}
|
|
538
554
|
|
|
539
555
|
fs.mkdirSync(TEMP_ROOT_DIR, { recursive: true });
|
|
540
556
|
const cfgPath = getAsyncConfigPath(suffix);
|
|
@@ -576,7 +592,8 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, initialStatus: Om
|
|
|
576
592
|
stdio: ["ignore", stdoutFd ?? "ignore", stderrFd ?? "ignore"],
|
|
577
593
|
env: {
|
|
578
594
|
...omitExtensionBindingsEnv(process.env),
|
|
579
|
-
|
|
595
|
+
[PI_CODING_AGENT_PACKAGE_ROOT_ENV]: piPackageRoot,
|
|
596
|
+
[JITI_ALIAS_ENV]: JSON.stringify(hostPeerAliases.aliases),
|
|
580
597
|
},
|
|
581
598
|
});
|
|
582
599
|
closeFd(stdoutFd);
|
|
@@ -832,7 +849,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
832
849
|
if (unsupported.length > 0) throw new AsyncStartValidationError(`Agent '${a.name}' uses runner.type='${externalRunnerType}' and does not support: ${unsupported.join(", ")}.`);
|
|
833
850
|
}
|
|
834
851
|
try {
|
|
835
|
-
assertAgentAllowedByCapabilityCeiling(a.name, intersectSubagentCapabilityCeilings(params.capabilityCeiling,
|
|
852
|
+
assertAgentAllowedByCapabilityCeiling(a.name, intersectSubagentCapabilityCeilings(params.capabilityCeiling, ctx.childRuntime?.capabilityCeiling));
|
|
836
853
|
} catch (error) {
|
|
837
854
|
throw new AsyncStartValidationError(error instanceof Error ? error.message : String(error));
|
|
838
855
|
}
|
|
@@ -912,7 +929,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
912
929
|
const thinkingCeiling = externalRunner ? undefined : intersectThinkingCeilings(
|
|
913
930
|
params.thinkingCeiling,
|
|
914
931
|
a.maxThinking,
|
|
915
|
-
|
|
932
|
+
ctx.childRuntime?.thinkingCeiling,
|
|
916
933
|
);
|
|
917
934
|
if (!externalRunner) {
|
|
918
935
|
try {
|
|
@@ -939,6 +956,8 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
939
956
|
throw new AsyncStartValidationError(error instanceof Error ? error.message : String(error));
|
|
940
957
|
}
|
|
941
958
|
}
|
|
959
|
+
const launchRuleError = applyWatchdogLaunchRules({ cwd: stepCwd, agent: a.name, model: modelCandidates[0] ?? model, warn: (violation) => sendRuleViolationWarning(ctx.pi, violation) });
|
|
960
|
+
if (launchRuleError) throw new AsyncStartValidationError(launchRuleError);
|
|
942
961
|
const fast = s.fast ?? params.fast ?? a.fast;
|
|
943
962
|
const toolPlan = resolvePiLaunchToolPlan({
|
|
944
963
|
tools: a.tools,
|
|
@@ -954,7 +973,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
954
973
|
model,
|
|
955
974
|
modelCandidates,
|
|
956
975
|
capabilityCeiling: params.capabilityCeiling,
|
|
957
|
-
inheritedCapabilityCeiling:
|
|
976
|
+
inheritedCapabilityCeiling: ctx.childRuntime?.capabilityCeiling,
|
|
958
977
|
agentName: a.name,
|
|
959
978
|
permissionRules,
|
|
960
979
|
runtimeSnapshotHost: ctx.pi,
|
|
@@ -999,8 +1018,6 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
999
1018
|
thinking: resolveEffectiveThinking(model, effectiveThinking),
|
|
1000
1019
|
...(thinkingCeiling ? { thinkingCeiling } : {}),
|
|
1001
1020
|
launchResolvedExtensions,
|
|
1002
|
-
...(toolPlan.mcpConfig ? { mcpConfig: toolPlan.mcpConfig } : {}),
|
|
1003
|
-
...(toolPlan.runtimeServerNames ? { runtimeServerNames: toolPlan.runtimeServerNames } : {}),
|
|
1004
1021
|
modelCandidates: externalRunner ? undefined : modelCandidates,
|
|
1005
1022
|
...(primaryModelFromParent ? { skipPrimaryModelVerification: true } : {}),
|
|
1006
1023
|
...(availableModels && availableModels.length > 0 ? { modelVerificationRegistry: availableModels } : {}),
|
|
@@ -1204,6 +1221,7 @@ export function executeAsyncChain(
|
|
|
1204
1221
|
worktreeSetupHook,
|
|
1205
1222
|
worktreeSetupHookTimeoutMs,
|
|
1206
1223
|
worktreeBaseDir,
|
|
1224
|
+
baseRef,
|
|
1207
1225
|
worktreeProvider,
|
|
1208
1226
|
worktreeBranchPrefix,
|
|
1209
1227
|
controlConfig,
|
|
@@ -1221,8 +1239,8 @@ export function executeAsyncChain(
|
|
|
1221
1239
|
});
|
|
1222
1240
|
if (acceptanceErrors.length > 0) return formatAsyncStartError(resultMode, acceptanceErrors.join(" "));
|
|
1223
1241
|
const capabilityCeiling = params.capabilityCeiling ?? resolveCurrentSubagentCapabilityCeiling(ctx.currentSessionId);
|
|
1224
|
-
const inheritedNestedRoute =
|
|
1225
|
-
const nestedAddress = inheritedNestedRoute ?
|
|
1242
|
+
const inheritedNestedRoute = inheritedNestedRouteOf(ctx.childRuntime);
|
|
1243
|
+
const nestedAddress = inheritedNestedRoute ? inheritedNestedParentAddressOf(ctx.childRuntime) : undefined;
|
|
1226
1244
|
const asyncDir = inheritedNestedRoute
|
|
1227
1245
|
? path.join(TEMP_ROOT_DIR, "nested-subagent-runs", inheritedNestedRoute.rootRunId, id)
|
|
1228
1246
|
: path.join(DIRS.async, id);
|
|
@@ -1335,10 +1353,12 @@ export function executeAsyncChain(
|
|
|
1335
1353
|
completionOwnerId: ctx.completionOwnerId ?? currentCompletionOwnerId(),
|
|
1336
1354
|
...(capabilityCeiling ? { capabilityCeiling } : {}),
|
|
1337
1355
|
piPackageRoot,
|
|
1338
|
-
|
|
1356
|
+
childSessionFactoryModule: childSessionFactoryModule(),
|
|
1357
|
+
inheritedChildRuntime: inheritedChildRuntime(ctx.childRuntime),
|
|
1339
1358
|
worktreeSetupHook,
|
|
1340
1359
|
worktreeSetupHookTimeoutMs,
|
|
1341
1360
|
worktreeBaseDir,
|
|
1361
|
+
baseRef,
|
|
1342
1362
|
worktreeProvider,
|
|
1343
1363
|
worktreeBranchPrefix,
|
|
1344
1364
|
controlConfig,
|
|
@@ -1451,7 +1471,7 @@ export function executeAsyncChain(
|
|
|
1451
1471
|
path: nestedAddress.path,
|
|
1452
1472
|
asyncDir,
|
|
1453
1473
|
pid: spawnResult.pid,
|
|
1454
|
-
ownerIntercomTarget:
|
|
1474
|
+
ownerIntercomTarget: ctx.childRuntime?.intercomSessionName,
|
|
1455
1475
|
leafIntercomTarget: childIntercomTargets?.[0],
|
|
1456
1476
|
intercomTarget: childIntercomTargets?.[0],
|
|
1457
1477
|
ownerState: "live",
|
|
@@ -1538,6 +1558,7 @@ export function executeAsyncSingle(
|
|
|
1538
1558
|
worktreeSetupHook,
|
|
1539
1559
|
worktreeSetupHookTimeoutMs,
|
|
1540
1560
|
worktreeBaseDir,
|
|
1561
|
+
baseRef,
|
|
1541
1562
|
worktreeProvider,
|
|
1542
1563
|
worktreeBranchPrefix,
|
|
1543
1564
|
controlConfig,
|
|
@@ -1578,7 +1599,7 @@ export function executeAsyncSingle(
|
|
|
1578
1599
|
if (extensionBindings !== undefined) unsupported.push("extension bindings");
|
|
1579
1600
|
if (unsupported.length > 0) return formatAsyncStartError("single", `Agent '${agentConfig.name}' uses runner.type='${externalRunnerType}' and does not support: ${unsupported.join(", ")}.`);
|
|
1580
1601
|
}
|
|
1581
|
-
const capabilityCeiling = intersectSubagentCapabilityCeilings(params.capabilityCeiling ?? resolveCurrentSubagentCapabilityCeiling(ctx.currentSessionId),
|
|
1602
|
+
const capabilityCeiling = intersectSubagentCapabilityCeilings(params.capabilityCeiling ?? resolveCurrentSubagentCapabilityCeiling(ctx.currentSessionId), ctx.childRuntime?.capabilityCeiling);
|
|
1582
1603
|
try {
|
|
1583
1604
|
assertAgentAllowedByCapabilityCeiling(agentConfig.name, capabilityCeiling);
|
|
1584
1605
|
} catch (error) {
|
|
@@ -1621,8 +1642,8 @@ export function executeAsyncSingle(
|
|
|
1621
1642
|
}
|
|
1622
1643
|
systemPrompt = appendAgentRefinementOverlay(systemPrompt, { cwd: runnerCwd, agentName: agentConfig.name });
|
|
1623
1644
|
|
|
1624
|
-
const inheritedNestedRoute =
|
|
1625
|
-
const nestedAddress = inheritedNestedRoute ?
|
|
1645
|
+
const inheritedNestedRoute = inheritedNestedRouteOf(ctx.childRuntime);
|
|
1646
|
+
const nestedAddress = inheritedNestedRoute ? inheritedNestedParentAddressOf(ctx.childRuntime) : undefined;
|
|
1626
1647
|
const asyncDir = inheritedNestedRoute
|
|
1627
1648
|
? path.join(TEMP_ROOT_DIR, "nested-subagent-runs", inheritedNestedRoute.rootRunId, id)
|
|
1628
1649
|
: path.join(DIRS.async, id);
|
|
@@ -1687,7 +1708,7 @@ export function executeAsyncSingle(
|
|
|
1687
1708
|
const thinkingCeiling = externalRunner ? undefined : intersectThinkingCeilings(
|
|
1688
1709
|
params.thinkingCeiling,
|
|
1689
1710
|
agentConfig.maxThinking,
|
|
1690
|
-
|
|
1711
|
+
ctx.childRuntime?.thinkingCeiling,
|
|
1691
1712
|
);
|
|
1692
1713
|
if (!externalRunner) {
|
|
1693
1714
|
try {
|
|
@@ -1747,7 +1768,7 @@ export function executeAsyncSingle(
|
|
|
1747
1768
|
model,
|
|
1748
1769
|
modelCandidates,
|
|
1749
1770
|
capabilityCeiling,
|
|
1750
|
-
inheritedCapabilityCeiling:
|
|
1771
|
+
inheritedCapabilityCeiling: ctx.childRuntime?.capabilityCeiling,
|
|
1751
1772
|
agentName: agentConfig.name,
|
|
1752
1773
|
permissionRules: resolvePermissionRules(ctx.permissions, agentConfig.permissions),
|
|
1753
1774
|
runtimeSnapshotHost: ctx.pi,
|
|
@@ -1844,6 +1865,7 @@ export function executeAsyncSingle(
|
|
|
1844
1865
|
...(controlConfig ? { controlConfig } : {}),
|
|
1845
1866
|
...(params.context ? { context: params.context } : {}),
|
|
1846
1867
|
...(params.intercomBridge !== undefined ? { intercomBridge: params.intercomBridge } : {}),
|
|
1868
|
+
...(params.baseRef !== undefined ? { baseRef: params.baseRef } : {}),
|
|
1847
1869
|
...(deadlineAt !== undefined ? { absoluteDeadlineAt: deadlineAt } : {}),
|
|
1848
1870
|
...(resolvedToolBudget.budget ? { initialToolBudget: resolvedToolBudget.budget } : {}),
|
|
1849
1871
|
maxSubagentDepth: resolveChildMaxSubagentDepth(maxSubagentDepth, recoveryAgentConfig.maxSubagentDepth),
|
|
@@ -1894,8 +1916,6 @@ export function executeAsyncSingle(
|
|
|
1894
1916
|
extensions: agentConfig.extensions,
|
|
1895
1917
|
subagentOnlyExtensions: agentConfig.subagentOnlyExtensions,
|
|
1896
1918
|
mcpDirectTools: agentConfig.mcpDirectTools,
|
|
1897
|
-
...(toolPlan.mcpConfig ? { mcpConfig: toolPlan.mcpConfig } : {}),
|
|
1898
|
-
...(toolPlan.runtimeServerNames ? { runtimeServerNames: toolPlan.runtimeServerNames } : {}),
|
|
1899
1919
|
mutationTools: agentConfig.mutationTools,
|
|
1900
1920
|
completionGuard: agentConfig.completionGuard,
|
|
1901
1921
|
systemPrompt,
|
|
@@ -1941,10 +1961,12 @@ export function executeAsyncSingle(
|
|
|
1941
1961
|
completionOwnerId: ctx.completionOwnerId ?? currentCompletionOwnerId(),
|
|
1942
1962
|
...(capabilityCeiling ? { capabilityCeiling } : {}),
|
|
1943
1963
|
piPackageRoot,
|
|
1944
|
-
|
|
1964
|
+
childSessionFactoryModule: childSessionFactoryModule(),
|
|
1965
|
+
inheritedChildRuntime: inheritedChildRuntime(ctx.childRuntime),
|
|
1945
1966
|
worktreeSetupHook,
|
|
1946
1967
|
worktreeSetupHookTimeoutMs,
|
|
1947
1968
|
worktreeBaseDir,
|
|
1969
|
+
baseRef,
|
|
1948
1970
|
worktreeProvider,
|
|
1949
1971
|
worktreeBranchPrefix,
|
|
1950
1972
|
controlConfig,
|
|
@@ -2027,7 +2049,7 @@ export function executeAsyncSingle(
|
|
|
2027
2049
|
path: nestedAddress.path,
|
|
2028
2050
|
asyncDir,
|
|
2029
2051
|
pid: spawnResult.pid,
|
|
2030
|
-
ownerIntercomTarget:
|
|
2052
|
+
ownerIntercomTarget: ctx.childRuntime?.intercomSessionName,
|
|
2031
2053
|
leafIntercomTarget: childIntercomTarget?.(agent, 0),
|
|
2032
2054
|
intercomTarget: childIntercomTarget?.(agent, 0),
|
|
2033
2055
|
ownerState: "live",
|
|
@@ -39,6 +39,8 @@ interface AsyncJobTrackerOptions {
|
|
|
39
39
|
watch?: typeof fs.watch;
|
|
40
40
|
kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean;
|
|
41
41
|
now?: () => number;
|
|
42
|
+
/** Resolve native supervisor requests without scanning supervisor mailboxes. */
|
|
43
|
+
supervisorRequestState?: (event: ControlEvent) => "pending" | "resolved" | "unknown";
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
const CONTROL_EVENT_READ_CHUNK_BYTES = 64 * 1024;
|
|
@@ -293,6 +295,15 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
293
295
|
if ((parsed as { type?: unknown }).type !== "subagent.control") return;
|
|
294
296
|
const record = parsed as { event?: ControlEvent; channels?: string[]; childIntercomTarget?: string; noticeText?: string; intercom?: { to?: string; message?: string } };
|
|
295
297
|
if (!record.event || !Array.isArray(record.channels)) return;
|
|
298
|
+
if (record.event.type === "needs_attention" && record.event.reason === "supervisor_request" && options.supervisorRequestState) {
|
|
299
|
+
let requestState: "pending" | "resolved" | "unknown" = "unknown";
|
|
300
|
+
try {
|
|
301
|
+
requestState = options.supervisorRequestState(record.event);
|
|
302
|
+
} catch (error) {
|
|
303
|
+
console.error(`Failed to resolve supervisor request state for async control event in '${job.asyncDir}':`, error);
|
|
304
|
+
}
|
|
305
|
+
if (requestState === "resolved") return;
|
|
306
|
+
}
|
|
296
307
|
const payload = {
|
|
297
308
|
event: record.event,
|
|
298
309
|
source: "async" as const,
|
|
@@ -12,6 +12,7 @@ import { reconcileAsyncRun } from "./stale-run-reconciler.ts";
|
|
|
12
12
|
import { resultFilePath, resultPayloadPathForIndexedRun } from "./result-files.ts";
|
|
13
13
|
import { canScanAsyncRunPrefix, MIN_SAFE_ASYNC_RUN_PREFIX_LENGTH } from "./run-id-query.ts";
|
|
14
14
|
import { parallelHandoffPath, resolveRetainedWorktreeCwd } from "../shared/parallel-handoff.ts";
|
|
15
|
+
import { normalizeWorktreeBaseRef } from "../shared/worktree.ts";
|
|
15
16
|
import { intersectThinkingCeilings, parseThinkingLevel, type ThinkingLevel } from "../../shared/thinking-ceiling.ts";
|
|
16
17
|
import { assertWorkflowGraphHostSteps } from "../shared/host-step-status.ts";
|
|
17
18
|
|
|
@@ -320,7 +321,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
|
|
|
320
321
|
"subagentOnlyExtensions", "mcpDirectTools", "excludeTools", "mutationTools", "systemPrompt", "systemPromptMode", "inheritProjectContext", "inheritGlobalContext", "inheritSkills", "skills",
|
|
321
322
|
"skillPath", "agentFilePath", "completionGuard", "memory", "outputPath", "outputMode", "structuredOutputSchema", "acceptance", "sessionDir", "artifactConfig",
|
|
322
323
|
"artifactsDir", "maxOutput", "controlConfig", "context", "intercomBridge", "absoluteDeadlineAt", "initialTurnBudget", "initialToolBudget", "maxSubagentDepth", "share", "capabilityCeiling",
|
|
323
|
-
"launchResolvedExtensions", "runFanoutBudget", "lane",
|
|
324
|
+
"launchResolvedExtensions", "runFanoutBudget", "lane", "baseRef",
|
|
324
325
|
"extensionBindings",
|
|
325
326
|
]);
|
|
326
327
|
for (const field of Object.keys(parsed)) {
|
|
@@ -366,6 +367,14 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
|
|
|
366
367
|
for (const field of ["launchContractDigest", "sessionFile", "model", "modelProvider", "thinking", "agentFilePath", "outputPath", "sessionDir", "artifactsDir"] as const) {
|
|
367
368
|
if (parsed[field] !== undefined && (typeof parsed[field] !== "string" || !(parsed[field] as string).trim())) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${field} must be a non-empty string.`);
|
|
368
369
|
}
|
|
370
|
+
if (parsed.baseRef !== undefined) {
|
|
371
|
+
if (typeof parsed.baseRef !== "string") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': baseRef must be a string.`);
|
|
372
|
+
try {
|
|
373
|
+
parsed.baseRef = normalizeWorktreeBaseRef(parsed.baseRef);
|
|
374
|
+
} catch (error) {
|
|
375
|
+
throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${error instanceof Error ? error.message : String(error)}`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
369
378
|
if (parsed.completionGuard !== undefined && typeof parsed.completionGuard !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': completionGuard must be a boolean.`);
|
|
370
379
|
if (parsed.structuredOutputSchema !== undefined && (!parsed.structuredOutputSchema || typeof parsed.structuredOutputSchema !== "object" || Array.isArray(parsed.structuredOutputSchema))) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': structuredOutputSchema must be an object.`);
|
|
371
380
|
if (parsed.memory !== undefined) {
|
|
@@ -623,7 +632,7 @@ export function buildRevivedAsyncTask(target: AsyncResumeTarget, message: string
|
|
|
623
632
|
`Original agent: ${target.agent}`,
|
|
624
633
|
target.sessionFile ? `Original session file: ${target.sessionFile}` : undefined,
|
|
625
634
|
"",
|
|
626
|
-
"Use the stored session context as background. Answer the orchestrator's follow-up below. Do not assume the original child
|
|
635
|
+
"Use the stored session context as background. Answer the orchestrator's follow-up below. Do not assume the original child session is still running.",
|
|
627
636
|
"",
|
|
628
637
|
"Follow-up:",
|
|
629
638
|
message,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Cross-OS control channel for async subagent runs.
|
|
3
3
|
*
|
|
4
|
-
* Background runs use
|
|
4
|
+
* Background runs use a detached runner process. Unix detaches it from the parent process.
|
|
5
5
|
* The original control path delivered an interrupt with
|
|
6
6
|
* `process.kill(pid, SIGUSR2|SIGBREAK)`, but Windows cannot
|
|
7
7
|
* deliver those signals cross-process via `process.kill` and throws `ENOSYS`,
|
|
@@ -74,33 +74,10 @@ export interface SteerRequest {
|
|
|
74
74
|
source?: string;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
export interface SteerCapability {
|
|
78
|
-
type: "steer-capability";
|
|
79
|
-
protocolVersion: 1;
|
|
80
|
-
index: number;
|
|
81
|
-
pid: number;
|
|
82
|
-
readyAt: number;
|
|
83
|
-
supported: boolean;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export interface SteerAck {
|
|
87
|
-
type: "steer-ack";
|
|
88
|
-
protocolVersion: 1;
|
|
89
|
-
requestId: string;
|
|
90
|
-
index: number;
|
|
91
|
-
ts: number;
|
|
92
|
-
state: "delivered" | "queued" | "failed";
|
|
93
|
-
deliveryStatus?: SteerDeliveryStatus;
|
|
94
|
-
message: string;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
77
|
const STEER_REQUESTS_DIR = "steer-requests";
|
|
98
78
|
const STOP_REQUESTS_DIR = "stop-requests";
|
|
99
79
|
const REVIVAL_BRIEFS_DIR = "revival-briefs";
|
|
100
80
|
export const MAX_STEER_QUEUE_SIZE = 20;
|
|
101
|
-
const STEER_TARGETS_DIR = "steer-targets";
|
|
102
|
-
const STEER_CAPABILITIES_DIR = "steer-capabilities";
|
|
103
|
-
const STEER_ACKS_DIR = "steer-acks";
|
|
104
81
|
const STEER_INBOX_CLOSED_FILE = "steer-inbox-closed.json";
|
|
105
82
|
const MAX_STEER_MESSAGE_BYTES = 128 * 1024;
|
|
106
83
|
const MAX_STEER_REQUEST_ID_LENGTH = 256;
|
|
@@ -143,35 +120,6 @@ export function closeSteerInbox(asyncDir: string, state: string, write: (filePat
|
|
|
143
120
|
write(steerInboxClosedPath(asyncDir), { version: 1, closedAt: Date.now(), state });
|
|
144
121
|
}
|
|
145
122
|
|
|
146
|
-
/** Per-child inbox consumed by the child prompt runtime inside the Pi process. */
|
|
147
|
-
export function stepSteerInboxDir(asyncDir: string, index: number): string {
|
|
148
|
-
assertChildIndex(index);
|
|
149
|
-
return path.join(controlInboxDir(asyncDir), STEER_TARGETS_DIR, String(index));
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
export function steerCapabilitiesDir(asyncDir: string): string {
|
|
153
|
-
return path.join(controlInboxDir(asyncDir), STEER_CAPABILITIES_DIR);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
export function steerCapabilityPath(asyncDir: string, index: number): string {
|
|
157
|
-
assertChildIndex(index);
|
|
158
|
-
return path.join(steerCapabilitiesDir(asyncDir), `${index}.json`);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
export function steerAcksDir(asyncDir: string, index: number): string {
|
|
162
|
-
assertChildIndex(index);
|
|
163
|
-
return path.join(controlInboxDir(asyncDir), STEER_ACKS_DIR, String(index));
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function steerAckFileName(requestId: string): string {
|
|
167
|
-
return `${Buffer.from(requestId).toString("base64url")}.json`;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
export function steerAckPathFromDir(dir: string, requestId: string): string {
|
|
171
|
-
if (!/^[^\s]+$/.test(requestId) || requestId.length > 256) throw new Error("steer acknowledgment requestId is invalid.");
|
|
172
|
-
return path.join(dir, steerAckFileName(requestId));
|
|
173
|
-
}
|
|
174
|
-
|
|
175
123
|
function assertChildIndex(index: number): void {
|
|
176
124
|
if (!Number.isInteger(index) || index < 0 || index > 1_000_000) throw new Error("child index must be a non-negative integer.");
|
|
177
125
|
}
|
|
@@ -229,45 +177,6 @@ export function writeSteerRequestToExistingDir(dir: string, request: SteerReques
|
|
|
229
177
|
return requestPath;
|
|
230
178
|
}
|
|
231
179
|
|
|
232
|
-
export function writeSteerCapabilityAt(filePath: string, capability: Omit<SteerCapability, "type" | "protocolVersion">): string {
|
|
233
|
-
assertChildIndex(capability.index);
|
|
234
|
-
if (!Number.isInteger(capability.pid) || capability.pid <= 0) throw new Error("steer capability pid must be a positive integer.");
|
|
235
|
-
if (!Number.isFinite(capability.readyAt) || capability.readyAt <= 0) throw new Error("steer capability readyAt must be a finite timestamp.");
|
|
236
|
-
const record: SteerCapability = { type: "steer-capability", protocolVersion: 1, ...capability };
|
|
237
|
-
writeAtomicJson(filePath, record);
|
|
238
|
-
return filePath;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
export function writeSteerCapability(asyncDir: string, capability: Omit<SteerCapability, "type" | "protocolVersion">): string {
|
|
242
|
-
return writeSteerCapabilityAt(steerCapabilityPath(asyncDir, capability.index), capability);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function steerAckWritePath(filePath: string, ack: Omit<SteerAck, "type" | "protocolVersion">): string {
|
|
246
|
-
const parsed = path.parse(filePath);
|
|
247
|
-
const stateOrder = ack.state === "queued" ? "0" : ack.state === "delivered" ? "1" : "2";
|
|
248
|
-
const timestamp = String(Math.trunc(ack.ts)).padStart(13, "0");
|
|
249
|
-
for (let suffix = 0; suffix < 1_000; suffix += 1) {
|
|
250
|
-
const candidate = path.join(parsed.dir, `${parsed.name}-${timestamp}-${stateOrder}-${ack.state}${suffix === 0 ? "" : `-${suffix}`}${parsed.ext}`);
|
|
251
|
-
if (!fs.existsSync(candidate)) return candidate;
|
|
252
|
-
}
|
|
253
|
-
throw new Error("steer acknowledgment queue is full.");
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
export function writeSteerAckAt(filePath: string, ack: Omit<SteerAck, "type" | "protocolVersion">): string {
|
|
257
|
-
assertChildIndex(ack.index);
|
|
258
|
-
if (!/^[^\s]+$/.test(ack.requestId) || ack.requestId.length > 256) throw new Error("steer acknowledgment requestId is invalid.");
|
|
259
|
-
if (!Number.isFinite(ack.ts) || ack.ts <= 0) throw new Error("steer acknowledgment ts must be a finite timestamp.");
|
|
260
|
-
if (!ack.message.trim() || ack.message.length > 1000) throw new Error("steer acknowledgment message is invalid.");
|
|
261
|
-
const record: SteerAck = { type: "steer-ack", protocolVersion: 1, ...ack, message: ack.message.trim() };
|
|
262
|
-
const ackPath = steerAckWritePath(filePath, ack);
|
|
263
|
-
writeAtomicJson(ackPath, record);
|
|
264
|
-
return ackPath;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
export function writeSteerAck(asyncDir: string, ack: Omit<SteerAck, "type" | "protocolVersion">): string {
|
|
268
|
-
return writeSteerAckAt(path.join(steerAcksDir(asyncDir, ack.index), steerAckFileName(ack.requestId)), ack);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
180
|
/**
|
|
272
181
|
* Parent side: drop a portable interrupt request the runner's inbox watcher will
|
|
273
182
|
* pick up regardless of OS. Written atomically (temp + rename), dir auto-created.
|
|
@@ -350,97 +259,6 @@ export function requestAsyncSteer(
|
|
|
350
259
|
return requestPath;
|
|
351
260
|
}
|
|
352
261
|
|
|
353
|
-
export function enqueueStepSteer(asyncDir: string, index: number, request: SteerRequest): string {
|
|
354
|
-
assertChildIndex(index);
|
|
355
|
-
const { targetIndexes: _targetIndexes, ...singleTargetRequest } = request;
|
|
356
|
-
return writeSteerRequestToDir(stepSteerInboxDir(asyncDir, index), { ...singleTargetRequest, targetIndex: index, type: "steer" });
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
function parseSteerCapability(raw: unknown): SteerCapability | undefined {
|
|
360
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
|
|
361
|
-
const input = raw as Partial<SteerCapability>;
|
|
362
|
-
if (input.type !== "steer-capability" || input.protocolVersion !== 1) return undefined;
|
|
363
|
-
const { index, pid, readyAt, supported } = input;
|
|
364
|
-
if (typeof index !== "number" || !Number.isInteger(index) || index < 0 || index > 1_000_000) return undefined;
|
|
365
|
-
if (typeof pid !== "number" || typeof readyAt !== "number" || !Number.isInteger(pid) || pid <= 0 || !Number.isFinite(readyAt) || readyAt <= 0 || typeof supported !== "boolean") return undefined;
|
|
366
|
-
return { type: "steer-capability", protocolVersion: 1, index, pid, readyAt, supported };
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
function parseSteerAck(raw: unknown): SteerAck | undefined {
|
|
370
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
|
|
371
|
-
const input = raw as Partial<SteerAck>;
|
|
372
|
-
if (input.type !== "steer-ack" || input.protocolVersion !== 1 || typeof input.requestId !== "string" || !/^[^\s]+$/.test(input.requestId) || input.requestId.length > 256) return undefined;
|
|
373
|
-
const { index, ts, state, message } = input;
|
|
374
|
-
if (typeof index !== "number" || typeof ts !== "number" || !Number.isInteger(index) || index < 0 || index > 1_000_000 || !Number.isFinite(ts) || ts <= 0) return undefined;
|
|
375
|
-
if (state !== "delivered" && state !== "queued" && state !== "failed") return undefined;
|
|
376
|
-
if (input.deliveryStatus !== undefined && input.deliveryStatus !== "delivered" && input.deliveryStatus !== "queued") return undefined;
|
|
377
|
-
if (typeof message !== "string" || !message.trim() || message.length > 1000) return undefined;
|
|
378
|
-
return { type: "steer-ack", protocolVersion: 1, requestId: input.requestId, index, ts, state, ...(input.deliveryStatus ? { deliveryStatus: input.deliveryStatus } : {}), message: message.trim() };
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
export function readSteerCapability(asyncDir: string, index: number): SteerCapability | undefined {
|
|
382
|
-
try {
|
|
383
|
-
return parseSteerCapability(JSON.parse(fs.readFileSync(steerCapabilityPath(asyncDir, index), "utf-8")));
|
|
384
|
-
} catch {
|
|
385
|
-
return undefined;
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
export function consumeSteerCapabilities(asyncDir: string, fsImpl: Pick<typeof fs, "existsSync" | "readdirSync" | "readFileSync"> = fs): SteerCapability[] {
|
|
390
|
-
const dir = steerCapabilitiesDir(asyncDir);
|
|
391
|
-
if (!fsImpl.existsSync(dir)) return [];
|
|
392
|
-
const capabilities: SteerCapability[] = [];
|
|
393
|
-
for (const entry of fsImpl.readdirSync(dir).filter((name) => /^\d+\.json$/.test(name)).sort()) {
|
|
394
|
-
try {
|
|
395
|
-
const capability = parseSteerCapability(JSON.parse(fsImpl.readFileSync(path.join(dir, entry), "utf-8")));
|
|
396
|
-
if (capability) capabilities.push(capability);
|
|
397
|
-
} catch {
|
|
398
|
-
// A partially written or malformed capability is ignored until a valid one arrives.
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
return capabilities;
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
export function consumeSteerAckFromDir(
|
|
405
|
-
dir: string,
|
|
406
|
-
requestId: string,
|
|
407
|
-
fsImpl: Pick<typeof fs, "existsSync" | "readdirSync" | "readFileSync" | "rmSync"> = fs,
|
|
408
|
-
): SteerAck | undefined {
|
|
409
|
-
if (!fsImpl.existsSync(dir)) return undefined;
|
|
410
|
-
let entries: string[];
|
|
411
|
-
try { entries = fsImpl.readdirSync(dir).filter((name) => name.endsWith(".json")).sort(); } catch { return undefined; }
|
|
412
|
-
for (const entry of entries) {
|
|
413
|
-
const target = path.join(dir, entry);
|
|
414
|
-
let ack: SteerAck | undefined;
|
|
415
|
-
try { ack = parseSteerAck(JSON.parse(fsImpl.readFileSync(target, "utf-8"))); } catch { ack = undefined; }
|
|
416
|
-
if (ack?.requestId !== requestId) continue;
|
|
417
|
-
try { fsImpl.rmSync(target, { force: true }); } catch { return undefined; }
|
|
418
|
-
return ack;
|
|
419
|
-
}
|
|
420
|
-
return undefined;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
export function consumeSteerAcks(asyncDir: string, fsImpl: Pick<typeof fs, "existsSync" | "readdirSync" | "readFileSync" | "rmSync"> = fs): SteerAck[] {
|
|
424
|
-
const root = path.join(controlInboxDir(asyncDir), STEER_ACKS_DIR);
|
|
425
|
-
if (!fsImpl.existsSync(root)) return [];
|
|
426
|
-
const acks: SteerAck[] = [];
|
|
427
|
-
let indexNames: string[];
|
|
428
|
-
try { indexNames = fsImpl.readdirSync(root).filter((name) => /^\d+$/.test(name)); } catch { return []; }
|
|
429
|
-
for (const indexName of indexNames) {
|
|
430
|
-
const dir = path.join(root, indexName);
|
|
431
|
-
let entries: string[];
|
|
432
|
-
try { entries = fsImpl.readdirSync(dir).filter((name) => name.endsWith(".json")).sort(); } catch { continue; }
|
|
433
|
-
for (const entry of entries) {
|
|
434
|
-
const target = path.join(dir, entry);
|
|
435
|
-
let ack: SteerAck | undefined;
|
|
436
|
-
try { ack = parseSteerAck(JSON.parse(fsImpl.readFileSync(target, "utf-8"))); } catch { ack = undefined; }
|
|
437
|
-
try { fsImpl.rmSync(target, { force: true }); } catch { continue; }
|
|
438
|
-
if (ack) acks.push(ack);
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
return acks;
|
|
442
|
-
}
|
|
443
|
-
|
|
444
262
|
function parseSteerRequest(raw: unknown): SteerRequest | undefined {
|
|
445
263
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
|
|
446
264
|
const input = raw as Partial<SteerRequest>;
|
|
@@ -665,8 +483,6 @@ export function watchAsyncControlInbox(
|
|
|
665
483
|
onTimeout?: () => void;
|
|
666
484
|
onStop?: (request: StopRequest) => void;
|
|
667
485
|
onSteer?: (request: SteerRequest) => void;
|
|
668
|
-
onSteerCapability?: (capability: SteerCapability) => void;
|
|
669
|
-
onSteerAck?: (ack: SteerAck) => void;
|
|
670
486
|
pollIntervalMs?: number;
|
|
671
487
|
safetyPollIntervalMs?: number;
|
|
672
488
|
platform?: NodeJS.Platform;
|
|
@@ -691,8 +507,6 @@ export function watchAsyncControlInbox(
|
|
|
691
507
|
if (consumeTimeoutRequest(asyncDir, fsImpl)) opts.onTimeout?.();
|
|
692
508
|
if (consumeInterruptRequest(asyncDir, fsImpl)) opts.onInterrupt();
|
|
693
509
|
for (const request of consumeSteerRequests(asyncDir, fsImpl)) opts.onSteer?.(request);
|
|
694
|
-
for (const capability of consumeSteerCapabilities(asyncDir, fsImpl)) opts.onSteerCapability?.(capability);
|
|
695
|
-
for (const ack of consumeSteerAcks(asyncDir, fsImpl)) opts.onSteerAck?.(ack);
|
|
696
510
|
} catch {
|
|
697
511
|
// Never let inbox errors crash the runner.
|
|
698
512
|
}
|
|
@@ -718,32 +532,16 @@ export function watchAsyncControlInbox(
|
|
|
718
532
|
const watchDir = (target: string, create = false): void => {
|
|
719
533
|
if (disposed || watchedDirs.has(target)) return;
|
|
720
534
|
if (create) fsImpl.mkdirSync(target, { recursive: true });
|
|
721
|
-
const watcher = fsImpl.watch(resolveWatchPath(target, fsImpl.realpathSync.native), () =>
|
|
722
|
-
watchExistingSteerAckDirs();
|
|
723
|
-
check();
|
|
724
|
-
});
|
|
535
|
+
const watcher = fsImpl.watch(resolveWatchPath(target, fsImpl.realpathSync.native), () => check());
|
|
725
536
|
watcher.on?.("error", startPolling);
|
|
726
537
|
watchers.push(watcher);
|
|
727
538
|
watchedDirs.add(target);
|
|
728
539
|
};
|
|
729
|
-
const watchExistingSteerAckDirs = (): void => {
|
|
730
|
-
const ackRoot = path.join(dir, STEER_ACKS_DIR);
|
|
731
|
-
let entries: string[];
|
|
732
|
-
try {
|
|
733
|
-
entries = fsImpl.readdirSync(ackRoot).filter((name) => /^\d+$/.test(name));
|
|
734
|
-
} catch {
|
|
735
|
-
return;
|
|
736
|
-
}
|
|
737
|
-
for (const entry of entries) watchDir(path.join(ackRoot, entry));
|
|
738
|
-
};
|
|
739
540
|
try {
|
|
740
541
|
if (shouldUseNativeFsWatch("runner-control-inbox", opts.platform)) {
|
|
741
542
|
watchDir(dir);
|
|
742
543
|
watchDir(stopRequestsDir(asyncDir), true);
|
|
743
544
|
watchDir(steerRequestsDir(asyncDir), true);
|
|
744
|
-
watchDir(steerCapabilitiesDir(asyncDir), true);
|
|
745
|
-
watchDir(path.join(dir, STEER_ACKS_DIR), true);
|
|
746
|
-
watchExistingSteerAckDirs();
|
|
747
545
|
startSafetyPolling();
|
|
748
546
|
} else {
|
|
749
547
|
startPolling();
|