pi-subagents 0.64.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 +33 -0
- 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 +1 -1
- 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 +2 -2
- 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 +43 -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/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 +184 -901
- package/src/runs/foreground/async-steering-action.ts +1 -17
- package/src/runs/foreground/execution.ts +189 -377
- package/src/runs/foreground/foreground-control.ts +4 -0
- package/src/runs/foreground/subagent-executor.ts +100 -57
- package/src/runs/foreground/workflow-foreground-steering.ts +24 -98
- package/src/runs/shared/abort-recovery.ts +3 -3
- 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 +2 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +87 -384
- package/src/runs/shared/tool-availability.ts +18 -62
- package/src/runs/shared/tool-budget.ts +0 -14
- package/src/runs/shared/worktree-cleanup-plan.ts +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 +55 -28
- 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 +233 -31
- package/src/watchdog/child-status.ts +0 -1
- package/src/watchdog/register-child.ts +12 -12
- package/src/workflows/scripted-workflow.ts +48 -1
- package/src/runs/shared/child-protocol.ts +0 -415
- package/src/runs/shared/pi-args.ts +0 -1059
- package/src/runs/shared/subagent-startup-retry.ts +0 -116
- package/src/shared/post-exit-stdio-guard.ts +0 -85
|
@@ -14,13 +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
19
|
import { applyWatchdogLaunchRules, sendRuleViolationWarning } from "../../watchdog/rules.ts";
|
|
20
20
|
import { buildChainInstructions, isDynamicParallelStep, isParallelStep, resolveExistingReadInstructionPaths, resolveExistingReadPaths, writeInitialProgressFile, type ChainStep, type SequentialStep, type StepOverrides } from "../../shared/settings.ts";
|
|
21
21
|
import type { RunnerStep } from "../shared/parallel-utils.ts";
|
|
22
22
|
import type { ContextMode } from "../shared/context-mode.ts";
|
|
23
|
-
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";
|
|
24
25
|
import { preflightLaunchCwd } from "../shared/launch-cwd.ts";
|
|
25
26
|
import { resolveNodeExecutable } from "../../shared/node-executable.ts";
|
|
26
27
|
import { backgroundProcessOptions } from "../shared/background-process-options.ts";
|
|
@@ -31,7 +32,7 @@ import { buildModelCandidates, resolveEffectiveSubagentModel, resolveModelOrigin
|
|
|
31
32
|
import { resolveToolTimeoutMs, toolTimeoutFromEnv } from "../shared/tool-timeout.ts";
|
|
32
33
|
import { resolveModelScopesForAgent, type ModelScopeConfig } from "../shared/model-scope.ts";
|
|
33
34
|
import { findModelInfo, resolveEffectiveThinking } from "../../shared/model-info.ts";
|
|
34
|
-
import { assertThinkingWithinCeiling,
|
|
35
|
+
import { assertThinkingWithinCeiling, intersectThinkingCeilings, type ThinkingLevel } from "../../shared/thinking-ceiling.ts";
|
|
35
36
|
import { resolveExpectedWorktreeAgentCwd, resolveWorktreeProvider, shouldDeferWorktreeCwd, WORKTREE_AGENT_CWD_PLACEHOLDER } from "../shared/worktree.ts";
|
|
36
37
|
import { buildWorkflowGraphSnapshot } from "../shared/workflow-graph.ts";
|
|
37
38
|
import { ChainOutputValidationError, validateChainOutputBindings } from "../shared/chain-outputs.ts";
|
|
@@ -65,7 +66,10 @@ import {
|
|
|
65
66
|
getAsyncConfigPath,
|
|
66
67
|
resolveChildMaxSubagentDepth,
|
|
67
68
|
} from "../../shared/types.ts";
|
|
68
|
-
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";
|
|
69
73
|
import { resultFilePath } from "./result-files.ts";
|
|
70
74
|
import { validateToolBudgetConfig } from "../shared/tool-budget.ts";
|
|
71
75
|
import { usageBudgetState } from "../shared/usage-budget.ts";
|
|
@@ -75,7 +79,7 @@ import { finalizeProcessTerminal, initializeProcessTerminal, readProcessTerminal
|
|
|
75
79
|
import type { ActiveAsyncCapacityHandle } from "./active-async-capacity.ts";
|
|
76
80
|
import { statusStepDescription } from "./chain-append.ts";
|
|
77
81
|
import { SUBAGENT_PROCESS_TERMINAL_EVENT } from "../../shared/types.ts";
|
|
78
|
-
import { assertAgentAllowedByCapabilityCeiling,
|
|
82
|
+
import { assertAgentAllowedByCapabilityCeiling, intersectSubagentCapabilityCeilings, resolveCurrentSubagentCapabilityCeiling, type ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
|
|
79
83
|
import { agentDefinitionDigest, launchBindingDigest } from "../../shared/launch-contract.ts";
|
|
80
84
|
import { resolvePermissionRules, type PermissionConfig } from "../shared/permissions.ts";
|
|
81
85
|
import { normalizeExtensionBindings, omitExtensionBindingsEnv, type ExtensionBindings } from "../shared/extension-bindings.ts";
|
|
@@ -144,6 +148,8 @@ interface AsyncExecutionContext {
|
|
|
144
148
|
modelScope?: ModelScopeConfig;
|
|
145
149
|
/** Whether the parent session has an interactive UI. */
|
|
146
150
|
interactive?: boolean;
|
|
151
|
+
/** The executor's own child runtime when the launch comes from an in-process child. */
|
|
152
|
+
childRuntime?: ChildRuntimeConfig;
|
|
147
153
|
}
|
|
148
154
|
|
|
149
155
|
export const DEFAULT_ASYNC_TIMEOUT_MS = 30 * 60 * 1000;
|
|
@@ -179,6 +185,7 @@ interface AsyncChainParams {
|
|
|
179
185
|
worktreeSetupHook?: string;
|
|
180
186
|
worktreeSetupHookTimeoutMs?: number;
|
|
181
187
|
worktreeBaseDir?: string;
|
|
188
|
+
baseRef?: string;
|
|
182
189
|
worktreeProvider?: import("../../shared/types.ts").WorktreeProvider;
|
|
183
190
|
worktreeBranchPrefix?: string;
|
|
184
191
|
controlConfig?: ResolvedControlConfig;
|
|
@@ -248,6 +255,7 @@ interface AsyncSingleParams {
|
|
|
248
255
|
worktreeSetupHook?: string;
|
|
249
256
|
worktreeSetupHookTimeoutMs?: number;
|
|
250
257
|
worktreeBaseDir?: string;
|
|
258
|
+
baseRef?: string;
|
|
251
259
|
worktreeProvider?: import("../../shared/types.ts").WorktreeProvider;
|
|
252
260
|
worktreeBranchPrefix?: string;
|
|
253
261
|
worktree?: boolean;
|
|
@@ -536,6 +544,13 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, initialStatus: Om
|
|
|
536
544
|
if (!jitiCliPath) {
|
|
537
545
|
return { error: "upstream jiti for TypeScript execution could not be found; ensure package dependencies are installed" };
|
|
538
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
|
+
}
|
|
539
554
|
|
|
540
555
|
fs.mkdirSync(TEMP_ROOT_DIR, { recursive: true });
|
|
541
556
|
const cfgPath = getAsyncConfigPath(suffix);
|
|
@@ -577,7 +592,8 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, initialStatus: Om
|
|
|
577
592
|
stdio: ["ignore", stdoutFd ?? "ignore", stderrFd ?? "ignore"],
|
|
578
593
|
env: {
|
|
579
594
|
...omitExtensionBindingsEnv(process.env),
|
|
580
|
-
|
|
595
|
+
[PI_CODING_AGENT_PACKAGE_ROOT_ENV]: piPackageRoot,
|
|
596
|
+
[JITI_ALIAS_ENV]: JSON.stringify(hostPeerAliases.aliases),
|
|
581
597
|
},
|
|
582
598
|
});
|
|
583
599
|
closeFd(stdoutFd);
|
|
@@ -833,7 +849,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
833
849
|
if (unsupported.length > 0) throw new AsyncStartValidationError(`Agent '${a.name}' uses runner.type='${externalRunnerType}' and does not support: ${unsupported.join(", ")}.`);
|
|
834
850
|
}
|
|
835
851
|
try {
|
|
836
|
-
assertAgentAllowedByCapabilityCeiling(a.name, intersectSubagentCapabilityCeilings(params.capabilityCeiling,
|
|
852
|
+
assertAgentAllowedByCapabilityCeiling(a.name, intersectSubagentCapabilityCeilings(params.capabilityCeiling, ctx.childRuntime?.capabilityCeiling));
|
|
837
853
|
} catch (error) {
|
|
838
854
|
throw new AsyncStartValidationError(error instanceof Error ? error.message : String(error));
|
|
839
855
|
}
|
|
@@ -913,7 +929,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
913
929
|
const thinkingCeiling = externalRunner ? undefined : intersectThinkingCeilings(
|
|
914
930
|
params.thinkingCeiling,
|
|
915
931
|
a.maxThinking,
|
|
916
|
-
|
|
932
|
+
ctx.childRuntime?.thinkingCeiling,
|
|
917
933
|
);
|
|
918
934
|
if (!externalRunner) {
|
|
919
935
|
try {
|
|
@@ -957,7 +973,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
957
973
|
model,
|
|
958
974
|
modelCandidates,
|
|
959
975
|
capabilityCeiling: params.capabilityCeiling,
|
|
960
|
-
inheritedCapabilityCeiling:
|
|
976
|
+
inheritedCapabilityCeiling: ctx.childRuntime?.capabilityCeiling,
|
|
961
977
|
agentName: a.name,
|
|
962
978
|
permissionRules,
|
|
963
979
|
runtimeSnapshotHost: ctx.pi,
|
|
@@ -1002,8 +1018,6 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
1002
1018
|
thinking: resolveEffectiveThinking(model, effectiveThinking),
|
|
1003
1019
|
...(thinkingCeiling ? { thinkingCeiling } : {}),
|
|
1004
1020
|
launchResolvedExtensions,
|
|
1005
|
-
...(toolPlan.mcpConfig ? { mcpConfig: toolPlan.mcpConfig } : {}),
|
|
1006
|
-
...(toolPlan.runtimeServerNames ? { runtimeServerNames: toolPlan.runtimeServerNames } : {}),
|
|
1007
1021
|
modelCandidates: externalRunner ? undefined : modelCandidates,
|
|
1008
1022
|
...(primaryModelFromParent ? { skipPrimaryModelVerification: true } : {}),
|
|
1009
1023
|
...(availableModels && availableModels.length > 0 ? { modelVerificationRegistry: availableModels } : {}),
|
|
@@ -1207,6 +1221,7 @@ export function executeAsyncChain(
|
|
|
1207
1221
|
worktreeSetupHook,
|
|
1208
1222
|
worktreeSetupHookTimeoutMs,
|
|
1209
1223
|
worktreeBaseDir,
|
|
1224
|
+
baseRef,
|
|
1210
1225
|
worktreeProvider,
|
|
1211
1226
|
worktreeBranchPrefix,
|
|
1212
1227
|
controlConfig,
|
|
@@ -1224,8 +1239,8 @@ export function executeAsyncChain(
|
|
|
1224
1239
|
});
|
|
1225
1240
|
if (acceptanceErrors.length > 0) return formatAsyncStartError(resultMode, acceptanceErrors.join(" "));
|
|
1226
1241
|
const capabilityCeiling = params.capabilityCeiling ?? resolveCurrentSubagentCapabilityCeiling(ctx.currentSessionId);
|
|
1227
|
-
const inheritedNestedRoute =
|
|
1228
|
-
const nestedAddress = inheritedNestedRoute ?
|
|
1242
|
+
const inheritedNestedRoute = inheritedNestedRouteOf(ctx.childRuntime);
|
|
1243
|
+
const nestedAddress = inheritedNestedRoute ? inheritedNestedParentAddressOf(ctx.childRuntime) : undefined;
|
|
1229
1244
|
const asyncDir = inheritedNestedRoute
|
|
1230
1245
|
? path.join(TEMP_ROOT_DIR, "nested-subagent-runs", inheritedNestedRoute.rootRunId, id)
|
|
1231
1246
|
: path.join(DIRS.async, id);
|
|
@@ -1338,10 +1353,12 @@ export function executeAsyncChain(
|
|
|
1338
1353
|
completionOwnerId: ctx.completionOwnerId ?? currentCompletionOwnerId(),
|
|
1339
1354
|
...(capabilityCeiling ? { capabilityCeiling } : {}),
|
|
1340
1355
|
piPackageRoot,
|
|
1341
|
-
|
|
1356
|
+
childSessionFactoryModule: childSessionFactoryModule(),
|
|
1357
|
+
inheritedChildRuntime: inheritedChildRuntime(ctx.childRuntime),
|
|
1342
1358
|
worktreeSetupHook,
|
|
1343
1359
|
worktreeSetupHookTimeoutMs,
|
|
1344
1360
|
worktreeBaseDir,
|
|
1361
|
+
baseRef,
|
|
1345
1362
|
worktreeProvider,
|
|
1346
1363
|
worktreeBranchPrefix,
|
|
1347
1364
|
controlConfig,
|
|
@@ -1454,7 +1471,7 @@ export function executeAsyncChain(
|
|
|
1454
1471
|
path: nestedAddress.path,
|
|
1455
1472
|
asyncDir,
|
|
1456
1473
|
pid: spawnResult.pid,
|
|
1457
|
-
ownerIntercomTarget:
|
|
1474
|
+
ownerIntercomTarget: ctx.childRuntime?.intercomSessionName,
|
|
1458
1475
|
leafIntercomTarget: childIntercomTargets?.[0],
|
|
1459
1476
|
intercomTarget: childIntercomTargets?.[0],
|
|
1460
1477
|
ownerState: "live",
|
|
@@ -1541,6 +1558,7 @@ export function executeAsyncSingle(
|
|
|
1541
1558
|
worktreeSetupHook,
|
|
1542
1559
|
worktreeSetupHookTimeoutMs,
|
|
1543
1560
|
worktreeBaseDir,
|
|
1561
|
+
baseRef,
|
|
1544
1562
|
worktreeProvider,
|
|
1545
1563
|
worktreeBranchPrefix,
|
|
1546
1564
|
controlConfig,
|
|
@@ -1581,7 +1599,7 @@ export function executeAsyncSingle(
|
|
|
1581
1599
|
if (extensionBindings !== undefined) unsupported.push("extension bindings");
|
|
1582
1600
|
if (unsupported.length > 0) return formatAsyncStartError("single", `Agent '${agentConfig.name}' uses runner.type='${externalRunnerType}' and does not support: ${unsupported.join(", ")}.`);
|
|
1583
1601
|
}
|
|
1584
|
-
const capabilityCeiling = intersectSubagentCapabilityCeilings(params.capabilityCeiling ?? resolveCurrentSubagentCapabilityCeiling(ctx.currentSessionId),
|
|
1602
|
+
const capabilityCeiling = intersectSubagentCapabilityCeilings(params.capabilityCeiling ?? resolveCurrentSubagentCapabilityCeiling(ctx.currentSessionId), ctx.childRuntime?.capabilityCeiling);
|
|
1585
1603
|
try {
|
|
1586
1604
|
assertAgentAllowedByCapabilityCeiling(agentConfig.name, capabilityCeiling);
|
|
1587
1605
|
} catch (error) {
|
|
@@ -1624,8 +1642,8 @@ export function executeAsyncSingle(
|
|
|
1624
1642
|
}
|
|
1625
1643
|
systemPrompt = appendAgentRefinementOverlay(systemPrompt, { cwd: runnerCwd, agentName: agentConfig.name });
|
|
1626
1644
|
|
|
1627
|
-
const inheritedNestedRoute =
|
|
1628
|
-
const nestedAddress = inheritedNestedRoute ?
|
|
1645
|
+
const inheritedNestedRoute = inheritedNestedRouteOf(ctx.childRuntime);
|
|
1646
|
+
const nestedAddress = inheritedNestedRoute ? inheritedNestedParentAddressOf(ctx.childRuntime) : undefined;
|
|
1629
1647
|
const asyncDir = inheritedNestedRoute
|
|
1630
1648
|
? path.join(TEMP_ROOT_DIR, "nested-subagent-runs", inheritedNestedRoute.rootRunId, id)
|
|
1631
1649
|
: path.join(DIRS.async, id);
|
|
@@ -1690,7 +1708,7 @@ export function executeAsyncSingle(
|
|
|
1690
1708
|
const thinkingCeiling = externalRunner ? undefined : intersectThinkingCeilings(
|
|
1691
1709
|
params.thinkingCeiling,
|
|
1692
1710
|
agentConfig.maxThinking,
|
|
1693
|
-
|
|
1711
|
+
ctx.childRuntime?.thinkingCeiling,
|
|
1694
1712
|
);
|
|
1695
1713
|
if (!externalRunner) {
|
|
1696
1714
|
try {
|
|
@@ -1750,7 +1768,7 @@ export function executeAsyncSingle(
|
|
|
1750
1768
|
model,
|
|
1751
1769
|
modelCandidates,
|
|
1752
1770
|
capabilityCeiling,
|
|
1753
|
-
inheritedCapabilityCeiling:
|
|
1771
|
+
inheritedCapabilityCeiling: ctx.childRuntime?.capabilityCeiling,
|
|
1754
1772
|
agentName: agentConfig.name,
|
|
1755
1773
|
permissionRules: resolvePermissionRules(ctx.permissions, agentConfig.permissions),
|
|
1756
1774
|
runtimeSnapshotHost: ctx.pi,
|
|
@@ -1847,6 +1865,7 @@ export function executeAsyncSingle(
|
|
|
1847
1865
|
...(controlConfig ? { controlConfig } : {}),
|
|
1848
1866
|
...(params.context ? { context: params.context } : {}),
|
|
1849
1867
|
...(params.intercomBridge !== undefined ? { intercomBridge: params.intercomBridge } : {}),
|
|
1868
|
+
...(params.baseRef !== undefined ? { baseRef: params.baseRef } : {}),
|
|
1850
1869
|
...(deadlineAt !== undefined ? { absoluteDeadlineAt: deadlineAt } : {}),
|
|
1851
1870
|
...(resolvedToolBudget.budget ? { initialToolBudget: resolvedToolBudget.budget } : {}),
|
|
1852
1871
|
maxSubagentDepth: resolveChildMaxSubagentDepth(maxSubagentDepth, recoveryAgentConfig.maxSubagentDepth),
|
|
@@ -1897,8 +1916,6 @@ export function executeAsyncSingle(
|
|
|
1897
1916
|
extensions: agentConfig.extensions,
|
|
1898
1917
|
subagentOnlyExtensions: agentConfig.subagentOnlyExtensions,
|
|
1899
1918
|
mcpDirectTools: agentConfig.mcpDirectTools,
|
|
1900
|
-
...(toolPlan.mcpConfig ? { mcpConfig: toolPlan.mcpConfig } : {}),
|
|
1901
|
-
...(toolPlan.runtimeServerNames ? { runtimeServerNames: toolPlan.runtimeServerNames } : {}),
|
|
1902
1919
|
mutationTools: agentConfig.mutationTools,
|
|
1903
1920
|
completionGuard: agentConfig.completionGuard,
|
|
1904
1921
|
systemPrompt,
|
|
@@ -1944,10 +1961,12 @@ export function executeAsyncSingle(
|
|
|
1944
1961
|
completionOwnerId: ctx.completionOwnerId ?? currentCompletionOwnerId(),
|
|
1945
1962
|
...(capabilityCeiling ? { capabilityCeiling } : {}),
|
|
1946
1963
|
piPackageRoot,
|
|
1947
|
-
|
|
1964
|
+
childSessionFactoryModule: childSessionFactoryModule(),
|
|
1965
|
+
inheritedChildRuntime: inheritedChildRuntime(ctx.childRuntime),
|
|
1948
1966
|
worktreeSetupHook,
|
|
1949
1967
|
worktreeSetupHookTimeoutMs,
|
|
1950
1968
|
worktreeBaseDir,
|
|
1969
|
+
baseRef,
|
|
1951
1970
|
worktreeProvider,
|
|
1952
1971
|
worktreeBranchPrefix,
|
|
1953
1972
|
controlConfig,
|
|
@@ -2030,7 +2049,7 @@ export function executeAsyncSingle(
|
|
|
2030
2049
|
path: nestedAddress.path,
|
|
2031
2050
|
asyncDir,
|
|
2032
2051
|
pid: spawnResult.pid,
|
|
2033
|
-
ownerIntercomTarget:
|
|
2052
|
+
ownerIntercomTarget: ctx.childRuntime?.intercomSessionName,
|
|
2034
2053
|
leafIntercomTarget: childIntercomTarget?.(agent, 0),
|
|
2035
2054
|
intercomTarget: childIntercomTarget?.(agent, 0),
|
|
2036
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();
|
|
@@ -115,7 +115,7 @@ export function writeProcessTerminalCandidate(asyncDir: string, candidate: Proce
|
|
|
115
115
|
writePrivateAtomicJson(processTerminalCandidatePath(asyncDir), candidate);
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
/** Establish ownership before authorizing a runner to
|
|
118
|
+
/** Establish ownership before authorizing a runner to start any child session. */
|
|
119
119
|
export function initializeProcessTerminal(asyncDir: string, runId: string, runnerProcessInstanceId: string): void {
|
|
120
120
|
writeProcessTerminalCandidate(asyncDir, {
|
|
121
121
|
version: 1,
|