pi-subagents 0.46.0 → 0.47.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/agents/reviewer.md +3 -4
  3. package/docs/agents.md +1 -1
  4. package/docs/configuration.md +14 -6
  5. package/docs/extension-api.md +1 -1
  6. package/docs/missions.md +5 -3
  7. package/docs/models.md +3 -1
  8. package/docs/observability.md +3 -3
  9. package/docs/tool-reference.md +2 -2
  10. package/docs/watchdog.md +1 -1
  11. package/package.json +1 -1
  12. package/skills/pi-subagents/references/execution-controls.md +4 -4
  13. package/skills/pi-subagents/references/management-authoring-rpc.md +1 -1
  14. package/src/extension/config.ts +13 -0
  15. package/src/extension/fanout-child.ts +5 -4
  16. package/src/extension/index.ts +39 -14
  17. package/src/extension/rpc.ts +7 -6
  18. package/src/extension/schemas.ts +27 -7
  19. package/src/extension/tool-description.ts +26 -8
  20. package/src/inspectors/herdr/project-panes.ts +2 -1
  21. package/src/intercom/native-supervisor-channel.ts +102 -4
  22. package/src/missions/store.ts +2 -1
  23. package/src/missions/workflow-state.ts +19 -13
  24. package/src/runs/background/active-run-index.ts +42 -0
  25. package/src/runs/background/async-execution.ts +10 -5
  26. package/src/runs/background/async-job-tracker.ts +293 -133
  27. package/src/runs/background/async-resume.ts +19 -3
  28. package/src/runs/background/async-status.ts +33 -5
  29. package/src/runs/background/control-channel.ts +91 -17
  30. package/src/runs/background/result-watcher.ts +102 -7
  31. package/src/runs/background/run-status.ts +18 -0
  32. package/src/runs/background/scheduled-runs.ts +25 -1
  33. package/src/runs/background/stale-run-reconciler.ts +12 -24
  34. package/src/runs/background/subagent-runner.ts +68 -10
  35. package/src/runs/foreground/async-dismiss-action.ts +85 -0
  36. package/src/runs/foreground/async-steering-action.ts +6 -7
  37. package/src/runs/foreground/chain-execution.ts +3 -0
  38. package/src/runs/foreground/execution.ts +3 -0
  39. package/src/runs/foreground/subagent-executor.ts +129 -20
  40. package/src/runs/foreground/workflow-foreground-steering.ts +187 -0
  41. package/src/runs/shared/dynamic-fanout.ts +1 -1
  42. package/src/runs/shared/model-fallback.ts +8 -4
  43. package/src/runs/shared/model-scope.ts +12 -2
  44. package/src/runs/shared/parallel-utils.ts +1 -0
  45. package/src/runs/shared/subagent-prompt-runtime.ts +24 -5
  46. package/src/runs/shared/worktree.ts +3 -2
  47. package/src/shared/artifacts.ts +15 -15
  48. package/src/shared/display-text.ts +100 -0
  49. package/src/shared/file-coalescer.ts +9 -0
  50. package/src/shared/formatters.ts +4 -6
  51. package/src/shared/settings.ts +15 -2
  52. package/src/shared/types.ts +16 -2
  53. package/src/shared/utils.ts +43 -33
  54. package/src/slash/slash-commands.ts +3 -1
  55. package/src/tui/fleet-status.ts +14 -10
  56. package/src/tui/render.ts +30 -26
  57. package/src/watchdog/change-signature.ts +4 -3
  58. package/src/workflows/scripted-workflow.ts +4 -1
@@ -5,6 +5,8 @@ import * as path from "node:path";
5
5
  import { pathToFileURL } from "node:url";
6
6
  import type { Message } from "@earendil-works/pi-ai";
7
7
  import { writeAtomicJson } from "../../shared/atomic-json.ts";
8
+ import { createFileCoalescer } from "../../shared/file-coalescer.ts";
9
+ import { isActiveAsyncState, updateActiveRunIndex } from "./active-run-index.ts";
8
10
  import { createChildTranscriptWriter, type ChildTranscriptWriter } from "../../shared/child-transcript.ts";
9
11
  import { closeSteerInbox, consumeInterruptRequest, consumeSteerRequests, deliverInterruptRequest, deliverStopRequest, deliverTimeoutRequest, enqueueStepSteer, steerAcksDir, steerCapabilityPath, stepSteerInboxDir, watchAsyncControlInbox, type SteerAck, type SteerCapability, type SteerRequest } from "./control-channel.ts";
10
12
  import { appendJsonl as appendRawJsonl, formatOutputArtifactContent, getArtifactPaths, writeArtifact, writeMetadata } from "../../shared/artifacts.ts";
@@ -1955,6 +1957,47 @@ function combinedAbortSignal(signals: Array<AbortSignal | undefined>): AbortSign
1955
1957
  return controller.signal;
1956
1958
  }
1957
1959
 
1960
+ async function runSingleStepWithTimeout(
1961
+ step: SubagentStep,
1962
+ ctx: SingleStepContext,
1963
+ parentDeadlineAt?: number,
1964
+ ): Promise<SingleStepResult> {
1965
+ if (step.timeoutMs === undefined) return runSingleStep(step, ctx);
1966
+
1967
+ const parentRemainingMs = parentDeadlineAt === undefined ? undefined : Math.max(0, parentDeadlineAt - Date.now());
1968
+ const timeoutMs = parentRemainingMs === undefined ? step.timeoutMs : Math.min(step.timeoutMs, parentRemainingMs);
1969
+ const timeoutMessage = parentRemainingMs !== undefined && parentRemainingMs <= step.timeoutMs
1970
+ ? ctx.timeoutMessage
1971
+ : `Subagent timed out after ${step.timeoutMs}ms.`;
1972
+ const timeoutController = new AbortController();
1973
+ let timeoutAction: (() => void) | undefined;
1974
+ let timeoutTriggered = false;
1975
+ const triggerTimeout = (): void => {
1976
+ if (timeoutTriggered) return;
1977
+ timeoutTriggered = true;
1978
+ timeoutController.abort();
1979
+ timeoutAction?.();
1980
+ };
1981
+ const registerTimeout = (action: (() => void) | undefined): void => {
1982
+ timeoutAction = action;
1983
+ ctx.registerTimeout?.(action ? triggerTimeout : undefined);
1984
+ if (action && timeoutTriggered) action();
1985
+ };
1986
+ const timer = setTimeout(triggerTimeout, timeoutMs);
1987
+ timer.unref?.();
1988
+ try {
1989
+ return await runSingleStep(step, {
1990
+ ...ctx,
1991
+ registerTimeout,
1992
+ timeoutSignal: combinedAbortSignal([ctx.timeoutSignal, timeoutController.signal]),
1993
+ timeoutMessage,
1994
+ });
1995
+ } finally {
1996
+ clearTimeout(timer);
1997
+ ctx.registerTimeout?.(undefined);
1998
+ }
1999
+ }
2000
+
1958
2001
  async function runSubagent(
1959
2002
  config: SubagentRunConfig,
1960
2003
  onWriterProcess?: (writer: { state: "none" | "spawning" } | { state: "running"; pid: number }) => void,
@@ -2139,6 +2182,7 @@ async function runSubagent(
2139
2182
 
2140
2183
  fs.mkdirSync(asyncDir, { recursive: true });
2141
2184
  writeAtomicJson(statusPath, statusPayload);
2185
+ updateActiveRunIndex(asyncDir, statusPayload.state);
2142
2186
  let pendingParallelUsageCost: CostSummary = { inputTokens: 0, outputTokens: 0, costUsd: 0 };
2143
2187
  const currentUsageTotals = (): CostSummary => {
2144
2188
  const cost = results.reduce<CostSummary>((sum, result) => ({
@@ -2211,11 +2255,25 @@ async function runSubagent(
2211
2255
  for (const node of graph.nodes) updateNode(node);
2212
2256
  statusPayload.workflowGraph = graph;
2213
2257
  };
2214
- const writeStatusPayload = (): void => {
2258
+ let lastIndexedActiveState = isActiveAsyncState(statusPayload.state);
2259
+ const writeStatusPayloadNow = (): void => {
2215
2260
  refreshWorkflowGraph();
2216
2261
  writeAtomicJson(statusPath, statusPayload);
2262
+ const activeState = isActiveAsyncState(statusPayload.state);
2263
+ if (activeState !== lastIndexedActiveState) {
2264
+ updateActiveRunIndex(asyncDir, statusPayload.state);
2265
+ lastIndexedActiveState = activeState;
2266
+ }
2217
2267
  emitNestedSelfEvent(statusPayload.state === "running" || statusPayload.state === "queued" ? "subagent.nested.updated" : "subagent.nested.completed");
2218
2268
  };
2269
+ const statusWriteCoalescer = createFileCoalescer(writeStatusPayloadNow, 100);
2270
+ const writeStatusPayload = (immediate = true): void => {
2271
+ if (immediate || statusPayload.state !== "running" || statusPayload.activityState !== undefined) {
2272
+ if (!statusWriteCoalescer.flush(statusPath)) writeStatusPayloadNow();
2273
+ return;
2274
+ }
2275
+ statusWriteCoalescer.schedule(statusPath);
2276
+ };
2219
2277
  const updateExternalProcess = (index: number, process: ExternalProcessStatus): void => {
2220
2278
  requiredStatusStep(statusPayload, index).externalProcess = process;
2221
2279
  statusPayload.lastUpdate = Date.now();
@@ -2742,7 +2800,7 @@ async function runSubagent(
2742
2800
  step.lastActivityAt = now;
2743
2801
  statusPayload.lastActivityAt = now;
2744
2802
  statusPayload.lastUpdate = now;
2745
- writeStatusPayload();
2803
+ writeStatusPayload(false);
2746
2804
  return;
2747
2805
  }
2748
2806
  if (event.type === "tool_execution_start" && event.toolName) {
@@ -2878,7 +2936,7 @@ async function runSubagent(
2878
2936
  statusPayload.lastActivityAt = now;
2879
2937
  statusPayload.lastUpdate = now;
2880
2938
  maybeEmitActiveLongRunning(flatIndex, now);
2881
- writeStatusPayload();
2939
+ writeStatusPayload(false);
2882
2940
  };
2883
2941
  const updateRunnerActivityState = (now: number): boolean => {
2884
2942
  if (!controlConfig.enabled) return false;
@@ -3449,7 +3507,7 @@ async function runSubagent(
3449
3507
  writeStatusPayload();
3450
3508
  appendJsonl(eventsPath, JSON.stringify({ type: "subagent.step.started", ts: taskStartTime, runId: id, stepIndex: fi, agent: task.agent }));
3451
3509
  flushPendingStepSteers(fi);
3452
- const singleResult = await runSingleStep(task, compactOptional<SingleStepContext>({
3510
+ const singleResult = await runSingleStepWithTimeout(task, compactOptional<SingleStepContext>({
3453
3511
  previousOutput, placeholder, cwd, sessionEnabled,
3454
3512
  outputs,
3455
3513
  sessionDir: config.sessionDir ? path.join(config.sessionDir, `dynamic-${stepIndex}-${taskIdx}`) : undefined,
@@ -3479,7 +3537,7 @@ async function runSubagent(
3479
3537
  onWriterProcess,
3480
3538
  onExternalProcess: (process) => updateExternalProcess(fi, process),
3481
3539
  skipAcceptance: () => timedOut || stopped,
3482
- }));
3540
+ }), config.deadlineAt);
3483
3541
  const taskEndTime = Date.now();
3484
3542
  const childInterrupted = singleResult.interrupted === true;
3485
3543
  const childStopped = singleResult.stopped === true;
@@ -3831,7 +3889,7 @@ async function runSubagent(
3831
3889
  const { taskForRun, taskCwd } = prepareParallelTaskRun(task, cwd, worktreeSetup, taskIdx);
3832
3890
  flushPendingStepSteers(fi);
3833
3891
 
3834
- const singleResult = await runSingleStep(taskForRun, compactOptional<SingleStepContext>({
3892
+ const singleResult = await runSingleStepWithTimeout(taskForRun, compactOptional<SingleStepContext>({
3835
3893
  previousOutput, placeholder, cwd: taskCwd, sessionEnabled,
3836
3894
  outputs,
3837
3895
  sessionDir: taskSessionDir,
@@ -3861,7 +3919,7 @@ async function runSubagent(
3861
3919
  onWriterProcess,
3862
3920
  onExternalProcess: (process) => updateExternalProcess(fi, process),
3863
3921
  skipAcceptance: () => timedOut || stopped,
3864
- }));
3922
+ }), config.deadlineAt);
3865
3923
  if (task.sessionFile) {
3866
3924
  latestSessionFile = task.sessionFile;
3867
3925
  }
@@ -4120,7 +4178,7 @@ async function runSubagent(
4120
4178
  }));
4121
4179
 
4122
4180
  flushPendingStepSteers(flatIndex);
4123
- const singleResult = await runSingleStep(seqStep, compactOptional<SingleStepContext>({
4181
+ const singleResult = await runSingleStepWithTimeout(seqStep, compactOptional<SingleStepContext>({
4124
4182
  previousOutput, placeholder, cwd, sessionEnabled,
4125
4183
  outputs: statusPayload.mode === "single" ? undefined : outputs,
4126
4184
  sessionDir: config.sessionDir,
@@ -4150,7 +4208,7 @@ async function runSubagent(
4150
4208
  onWriterProcess,
4151
4209
  onExternalProcess: (process) => updateExternalProcess(flatIndex, process),
4152
4210
  skipAcceptance: () => timedOut || stopped,
4153
- }));
4211
+ }), config.deadlineAt);
4154
4212
  if (seqStep.sessionFile) {
4155
4213
  latestSessionFile = seqStep.sessionFile;
4156
4214
  }
@@ -4439,6 +4497,7 @@ async function runSubagent(
4439
4497
  statusPayload.error = `Step failed: ${failedStep.agent}`;
4440
4498
  }
4441
4499
  }
4500
+ writeStatusPayload();
4442
4501
  try {
4443
4502
  writeAtomicJson(resultPath, {
4444
4503
  lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
@@ -4592,7 +4651,6 @@ async function runSubagent(
4592
4651
  console.error(`Failed to write process-terminal candidate for '${id}':`, error);
4593
4652
  }
4594
4653
  }
4595
- writeStatusPayload();
4596
4654
  }
4597
4655
 
4598
4656
  async function waitForStartupControl(
@@ -0,0 +1,85 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import type { AgentToolResult } from "@earendil-works/pi-agent-core";
4
+ import { writeAtomicJson } from "../../shared/atomic-json.ts";
5
+ import { DIRS, type Details, type SubagentState } from "../../shared/types.ts";
6
+ import { readStatus } from "../../shared/utils.ts";
7
+ import { updateActiveRunIndex } from "../background/active-run-index.ts";
8
+ import { reconcileAsyncRun } from "../background/stale-run-reconciler.ts";
9
+
10
+ export function dismissRecoveredWorkflow(
11
+ state: SubagentState,
12
+ location: { asyncDir: string | null; resolvedId?: string },
13
+ ): AgentToolResult<Details> {
14
+ const asyncDir = location.asyncDir;
15
+ const runId = location.resolvedId ?? (asyncDir ? path.basename(asyncDir) : "requested");
16
+ if (!asyncDir) {
17
+ return {
18
+ content: [{ type: "text", text: `Recovered workflow '${runId}' has no disk status to dismiss.` }],
19
+ isError: true,
20
+ details: { mode: "management", results: [] },
21
+ };
22
+ }
23
+ const status = readStatus(asyncDir);
24
+ if (!status || status.mode !== "workflow") {
25
+ return {
26
+ content: [{ type: "text", text: `Run '${runId}' is not a recovered workflow.` }],
27
+ isError: true,
28
+ details: { mode: "management", results: [] },
29
+ };
30
+ }
31
+ if (!state.currentSessionId || status.sessionId !== state.currentSessionId) {
32
+ return {
33
+ content: [{ type: "text", text: `Recovered workflow '${runId}' was not found in the active session.` }],
34
+ isError: true,
35
+ details: { mode: "management", results: [] },
36
+ };
37
+ }
38
+ if (state.workflowControllers?.has(status.runId) || state.workflowControllers?.has(runId)) {
39
+ return {
40
+ content: [{ type: "text", text: `Workflow '${runId}' still has a live controller and cannot be dismissed.` }],
41
+ isError: true,
42
+ details: { mode: "management", results: [] },
43
+ };
44
+ }
45
+ if (status.state !== "running") {
46
+ return {
47
+ content: [{ type: "text", text: `Recovered workflow '${runId}' is ${status.state}, not running.` }],
48
+ isError: true,
49
+ details: { mode: "management", results: [] },
50
+ };
51
+ }
52
+ let latestStatus = status;
53
+ const resultPath = path.join(DIRS.results, `${status.runId}.json`);
54
+ if (fs.existsSync(resultPath)) {
55
+ const reconciled = reconcileAsyncRun(asyncDir).status;
56
+ if (reconciled && reconciled.state !== "running") {
57
+ return {
58
+ content: [{ type: "text", text: `Recovered workflow '${runId}' is ${reconciled.state}, not running.` }],
59
+ isError: true,
60
+ details: { mode: "management", results: [] },
61
+ };
62
+ }
63
+ if (reconciled) latestStatus = reconciled;
64
+ }
65
+
66
+ const dismissed = { ...latestStatus, displayDismissedAt: Date.now() };
67
+ writeAtomicJson(path.join(asyncDir, "status.json"), dismissed);
68
+ const repaired = reconcileAsyncRun(asyncDir).status;
69
+ if (repaired && repaired.state !== "running") {
70
+ return {
71
+ content: [{ type: "text", text: `Recovered workflow '${runId}' is ${repaired.state}, not running.` }],
72
+ isError: true,
73
+ details: { mode: "management", results: [] },
74
+ };
75
+ }
76
+ updateActiveRunIndex(asyncDir, "complete");
77
+ state.asyncJobs.delete(status.runId);
78
+ state.asyncJobs.delete(runId);
79
+ state.fleetJobs?.delete(status.runId);
80
+ state.fleetJobs?.delete(runId);
81
+ return {
82
+ content: [{ type: "text", text: `Dismissed recovered workflow ${status.runId} from the display. No running work was terminated.` }],
83
+ details: { mode: "management", results: [] },
84
+ };
85
+ }
@@ -219,15 +219,14 @@ export async function steerAsyncRun(input: {
219
219
  const limits = remainingSteeringRecoveryLimits(recoveryTarget.recoveryDescriptor, paused);
220
220
  const revived = await input.recover(limits);
221
221
  if (revived.isError || !revived.details.asyncId) throw new Error(revived.content[0]?.type === "text" ? revived.content[0].text : "Replacement launch failed; source run remains paused.");
222
- const sourceStatus = readStatus(asyncDir);
223
- const targetIndex = input.index ?? status.steps?.findIndex((step) => step.status === "running") ?? -1;
224
- if (sourceStatus?.state === "paused" && sourceStatus.steering && targetIndex >= 0) {
225
- updateSteeringTarget(sourceStatus.steering, requestId, targetIndex, "recovered", Date.now(), { replacementRunId: revived.details.asyncId });
226
- const stepSteering = sourceStatus.steps?.[targetIndex]?.steering;
222
+ const targetIndex = input.index ?? paused.steering?.recent.find((request) => request.id === requestId)?.targets[0]?.index ?? status.steps?.findIndex((step) => step.status === "running") ?? -1;
223
+ if (paused.steering && targetIndex >= 0) {
224
+ updateSteeringTarget(paused.steering, requestId, targetIndex, "recovered", Date.now(), { replacementRunId: revived.details.asyncId });
225
+ const stepSteering = paused.steps?.[targetIndex]?.steering;
227
226
  if (stepSteering) updateSteeringTarget(stepSteering, requestId, targetIndex, "recovered", Date.now(), { replacementRunId: revived.details.asyncId });
228
- writeAtomicJson(path.join(asyncDir, "status.json"), sourceStatus);
227
+ writeAtomicJson(path.join(asyncDir, "status.json"), paused);
229
228
  }
230
- const recovered = sourceStatus?.steering ? actionResultFromSteeringStatus(sourceStatus.steering, status.runId, requestId, revived.details.asyncId) : undefined;
229
+ const recovered = paused.steering ? actionResultFromSteeringStatus(paused.steering, status.runId, requestId, revived.details.asyncId) : undefined;
231
230
  appendSteeringNotice("recovered", `Steering recovered for run ${status.runId}; replacement ${revived.details.asyncId} launched.`);
232
231
  return { content: [{ type: "text", text: `Steering recovered for async run ${status.runId}; replacement ${revived.details.asyncId} launched after the source paused.` }], details: { mode: "management", results: [], steering: recovered ?? { requestId, state: "recovered", sourceRunId: status.runId, replacementRunId: revived.details.asyncId, deliveryStatus: "delivered", targets: [{ index: input.index ?? 0, state: "recovered" }] } } };
233
232
  } catch (error) {
@@ -34,6 +34,7 @@ import {
34
34
  import { discoverAvailableSkills, normalizeSkillInput } from "../../agents/skills.ts";
35
35
  import { INTERCOM_BRIDGE_MARKER } from "../../intercom/intercom-bridge.ts";
36
36
  import { runSync } from "./execution.ts";
37
+ import { workflowForegroundSteeringLaunchOptions } from "./workflow-foreground-steering.ts";
37
38
  import {
38
39
  beginForegroundChild,
39
40
  finishForegroundChild,
@@ -387,6 +388,7 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
387
388
  result = await runSync(input.ctx.cwd, input.agents, task.agent, taskStr, {
388
389
  permissions: input.permissions,
389
390
  parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined,
391
+ ...workflowForegroundSteeringLaunchOptions(input.foregroundControl, childIndex),
390
392
  capabilityCeiling: input.capabilityCeiling,
391
393
  context: input.contextForAgent?.(task.agent),
392
394
  cwd: taskCwd,
@@ -1367,6 +1369,7 @@ ${step.message}` : ""}` }],
1367
1369
  r = await runSync(ctx.cwd, agents, seqStep.agent, stepTask, {
1368
1370
  permissions: params.permissions,
1369
1371
  parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
1372
+ ...workflowForegroundSteeringLaunchOptions(params.foregroundControl, childIndex),
1370
1373
  capabilityCeiling: params.capabilityCeiling,
1371
1374
  context: params.contextForAgent?.(seqStep.agent),
1372
1375
  cwd: resolveChildCwd(cwd ?? ctx.cwd, seqStep.cwd),
@@ -338,6 +338,9 @@ async function runSingleAttempt(
338
338
  parentRootRunId: options.nestedRoute?.rootRunId,
339
339
  parentCapabilityToken: options.nestedRoute?.capabilityToken,
340
340
  parentSessionId: options.parentSessionId,
341
+ steerInboxDir: options.steerInboxDir,
342
+ steerCapabilityPath: options.steerCapabilityPath,
343
+ steerAckDir: options.steerAckDir,
341
344
  structuredOutput: options.structuredOutput,
342
345
  toolBudget: options.toolBudget,
343
346
  allowZeroToolBudget: options.allowZeroToolBudget,
@@ -4,7 +4,7 @@ import * as path from "node:path";
4
4
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
5
5
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
6
6
  import { resolveAgentName, type AgentConfig, type AgentScope } from "../../agents/agents.ts";
7
- import { getArtifactsDir, getChainRunsDir, getProjectArtifactPackagingWarning } from "../../shared/artifacts.ts";
7
+ import { getArtifactsDir, getChainRunsDir, getProjectArtifactPackagingWarning, getProjectSubagentsDir } from "../../shared/artifacts.ts";
8
8
  import { writeAtomicJson } from "../../shared/atomic-json.ts";
9
9
  import { ChainClarifyComponent, type ChainClarifyResult } from "./chain-clarify.ts";
10
10
  import { resolveEffectiveThinking, toModelInfo, type ModelInfo } from "../../shared/model-info.ts";
@@ -39,6 +39,7 @@ import {
39
39
  isParallelStep,
40
40
  isDynamicParallelStep,
41
41
  resolveChainPath,
42
+ resolveExistingReadPaths,
42
43
  resolveStepBehavior,
43
44
  suppressProgressForReadOnlyTask,
44
45
  taskDisallowsFileUpdates,
@@ -51,7 +52,8 @@ import {
51
52
  type StepOverrides,
52
53
  } from "../../shared/settings.ts";
53
54
  import { discoverAvailableSkills, normalizeSkillInput } from "../../agents/skills.ts";
54
- import { buildAsyncRunnerSteps, executeAsyncChain, executeAsyncSingle, formatAsyncStartedMessage, isAsyncAvailable, workflowAwaitedAsyncResultPath } from "../background/async-execution.ts";
55
+ import { buildAsyncRunnerSteps, DEFAULT_ASYNC_TIMEOUT_MS, executeAsyncChain, executeAsyncSingle, formatAsyncStartedMessage, isAsyncAvailable, workflowAwaitedAsyncResultPath } from "../background/async-execution.ts";
56
+ import { updateActiveRunIndex } from "../background/active-run-index.ts";
55
57
  import { isScheduledRunAction, type ScheduledRunAction } from "../background/scheduled-runs.ts";
56
58
  import { enqueueChainAppendRequest, readPendingChainAppendRequests, runnerStepOutputNames } from "../background/chain-append.ts";
57
59
  import { ChainOutputValidationError, validateChainOutputBindingsWithContext } from "../shared/chain-outputs.ts";
@@ -84,7 +86,15 @@ import { applySteeringRecoveryAgentConfig, buildRevivedAsyncTask, resolveAsyncRe
84
86
  import { deliverCheckpointDecisionRequest, deliverInterruptRequest, readRevivalBriefs, requestAsyncSteer, type SteerDeliveryMode } from "../background/control-channel.ts";
85
87
  import { updateSteeringTarget, waitForSteeringAction } from "../background/steering.ts";
86
88
  import { steerAsyncRun } from "./async-steering-action.ts";
89
+ import {
90
+ removeWorkflowForegroundSteeringRoute,
91
+ resolveWorkflowForegroundSteeringTarget,
92
+ steerWorkflowForegroundTarget,
93
+ workflowForegroundSteeringDir,
94
+ workflowForegroundSteeringLaunchOptions,
95
+ } from "./workflow-foreground-steering.ts";
87
96
  import { stopAsyncRun } from "./async-stop-action.ts";
97
+ import { dismissRecoveredWorkflow } from "./async-dismiss-action.ts";
88
98
  import { reconcileAsyncRun } from "../background/stale-run-reconciler.ts";
89
99
  import { resolveAsyncRootResultPath, waitForImportedAsyncRoot } from "../background/chain-root-attachment.ts";
90
100
  import { attachRootChildrenToSteps, createNestedRoute, findNestedControlResult, resolveInheritedNestedRouteFromEnv, resolveNestedAsyncDir, resolveNestedParentAddressFromEnv, snapshotNestedEventFiles, updateForegroundNestedProjection, writeNestedControlRequest, writeNestedEvent, type NestedRunResolutionScope } from "../shared/nested-events.ts";
@@ -154,7 +164,7 @@ import {
154
164
  wrapForkTask,
155
165
  } from "../../shared/types.ts";
156
166
 
157
- const MUTATING_MANAGEMENT_ACTIONS = new Set(["create", "update", "delete", "eject", "disable", "enable", "reset", "grant-spawn-budget", "watchdog.configure", "mission.create", "mission.update", "mission.resolve-decision", "mission.attach-run", "mission.close", "inspector.open", "inspector.close", "project.open", "project.close", "worktree.discard", "refine", "refine.rollback", "schedule.create", "schedule.pause", "schedule.resume", "schedule.run", "schedule.run-due", "schedule.delete"]);
167
+ const MUTATING_MANAGEMENT_ACTIONS = new Set(["create", "update", "delete", "eject", "disable", "enable", "reset", "grant-spawn-budget", "watchdog.configure", "mission.create", "mission.update", "mission.resolve-decision", "mission.attach-run", "mission.close", "inspector.open", "inspector.close", "project.open", "project.close", "worktree.discard", "refine", "refine.rollback", "dismiss", "schedule.create", "schedule.pause", "schedule.resume", "schedule.run", "schedule.run-due", "schedule.delete"]);
158
168
  const DESTRUCTIVE_MANAGEMENT_ACTIONS = new Set(["delete", "eject", "disable", "reset", "mission.close", "worktree.discard", "refine.rollback", "inspector.close", "project.close", "stop", "interrupt", "reject-checkpoint", "schedule.delete"]);
159
169
 
160
170
  function editDistance(left: string, right: string): number {
@@ -403,6 +413,7 @@ function resolveRequestedCwd(runtimeCwd: string, requestedCwd: string | undefine
403
413
  function removeForegroundControlIfIdle(state: SubagentState, runId: string): boolean {
404
414
  const control = state.foregroundControls.get(runId);
405
415
  if (control && (!foregroundSchedulingSettled(control) || (control.activeChildren?.size ?? 0) > 0)) return false;
416
+ if (control) removeWorkflowForegroundSteeringRoute(control);
406
417
  state.foregroundControls.delete(runId);
407
418
  if (state.lastForegroundControlId === runId) state.lastForegroundControlId = null;
408
419
  return true;
@@ -1450,7 +1461,7 @@ async function resumeAsyncRun(input: {
1450
1461
  inheritProjectContext: recoveryDescriptor.inheritProjectContext,
1451
1462
  inheritSkills: recoveryDescriptor.inheritSkills,
1452
1463
  source: "project",
1453
- filePath: recoveryDescriptor.agentFilePath ?? path.join(recoveryDescriptor.cwd, ".pi-subagents-recovery-agent"),
1464
+ filePath: recoveryDescriptor.agentFilePath ?? path.join(getProjectSubagentsDir(recoveryDescriptor.cwd), "recovery-agent"),
1454
1465
  } : undefined);
1455
1466
  if (!agentConfig) {
1456
1467
  return {
@@ -2096,7 +2107,17 @@ function applySingleAgentLaunchDefaults(params: SubagentParamsLike, agents: Agen
2096
2107
 
2097
2108
  export const DEFAULT_FOREGROUND_TIMEOUT_MS = 30 * 60 * 1000;
2098
2109
 
2099
- function resolveForegroundTimeout(params: SubagentParamsLike, defaultTimeoutMs?: number): { timeoutMs?: number; error?: string } {
2110
+ // Async single-agent runs also need a wall-clock backstop: a child whose bash
2111
+ // tool blocks forever (e.g. a background process inheriting the terminal with
2112
+ // no bash `timeout` arg) would otherwise hang the parent indefinitely with
2113
+ // zero signal. Same generous default as foreground; explicit timeoutMs/
2114
+ // maxRuntimeMs and agent-level defaultTimeoutMs remain authoritative.
2115
+ //
2116
+ // Deliberately NOT applied at the workflow level: async scripted workflows
2117
+ // stay unbounded as a whole, while each runner child has its own deadline.
2118
+ export { DEFAULT_ASYNC_TIMEOUT_MS };
2119
+
2120
+ export function resolveForegroundTimeout(params: SubagentParamsLike, defaultTimeoutMs?: number): { timeoutMs?: number; error?: string } {
2100
2121
  const rawTimeout = params.timeoutMs;
2101
2122
  const rawMaxRuntime = params.maxRuntimeMs;
2102
2123
  if (rawTimeout === undefined && rawMaxRuntime === undefined) {
@@ -2115,6 +2136,21 @@ function resolveForegroundTimeout(params: SubagentParamsLike, defaultTimeoutMs?:
2115
2136
  return timeoutMs === undefined ? {} : { timeoutMs };
2116
2137
  }
2117
2138
 
2139
+ /**
2140
+ * Resolve the effective launch timeout for a single-agent run, applying the
2141
+ * async/foreground default when neither the caller nor the agent set one.
2142
+ *
2143
+ * The async default is deliberately applied only to plain single-agent
2144
+ * launches. Composite launches keep their top-level execution unbounded when
2145
+ * no timeout is set; their runner children resolve separate deadlines.
2146
+ * Exported so the executor wiring is directly testable.
2147
+ */
2148
+ export function resolveSingleAgentLaunchTimeout(params: SubagentParamsLike, async: boolean): { timeoutMs?: number; error?: string } {
2149
+ const isComposite = (params.chain?.length ?? 0) > 0 || (params.tasks?.length ?? 0) > 0 || params.workflowScript !== undefined;
2150
+ const defaultTimeoutMs = !async ? DEFAULT_FOREGROUND_TIMEOUT_MS : isComposite ? undefined : DEFAULT_ASYNC_TIMEOUT_MS;
2151
+ return resolveForegroundTimeout(params, defaultTimeoutMs);
2152
+ }
2153
+
2118
2154
  function resolveToolBudget(
2119
2155
  raw: unknown,
2120
2156
  label = "toolBudget",
@@ -3155,6 +3191,7 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
3155
3191
  const result = await runSync(input.ctx.cwd, input.agents, task.agent, taskText, compactOptional<Parameters<typeof runSync>[4]>({
3156
3192
  permissions: input.permissions,
3157
3193
  parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined,
3194
+ ...workflowForegroundSteeringLaunchOptions(input.foregroundControl, index),
3158
3195
  context: input.contextPolicy.contextForAgent(task.agent),
3159
3196
  cwd: taskCwd,
3160
3197
  signal: input.signal,
@@ -3830,8 +3867,9 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3830
3867
  // Reads: caller override > agent defaultReads > none. `~`/`~/` expand to home;
3831
3868
  // absolute paths pass through; relative paths resolve against the child cwd.
3832
3869
  const reads = readsOverride !== undefined ? readsOverride : agentConfig.defaultReads ?? false;
3833
- const readsInstruction = Array.isArray(reads) && reads.length > 0
3834
- ? `[Read from: ${reads.map((f) => resolveChainPath(f, effectiveCwd)).join(", ")}]\n\n`
3870
+ const readPaths = Array.isArray(reads) ? resolveExistingReadPaths(reads, effectiveCwd) : [];
3871
+ const readsInstruction = readPaths.length > 0
3872
+ ? `[Read from: ${readPaths.join(", ")}]\n\n`
3835
3873
  : "";
3836
3874
  task = readsInstruction + task;
3837
3875
  task = injectSingleOutputInstruction(task, outputPath, agentConfig);
@@ -3875,6 +3913,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3875
3913
  r = await runSync(ctx.cwd, agents, params.agent!, task, compactOptional<Parameters<typeof runSync>[4]>({
3876
3914
  permissions: deps.config.permissions,
3877
3915
  parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
3916
+ ...workflowForegroundSteeringLaunchOptions(foregroundControl, 0),
3878
3917
  context: data.contextPolicy.contextForAgent(params.agent!),
3879
3918
  cwd: effectiveCwd,
3880
3919
  signal,
@@ -4427,9 +4466,14 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4427
4466
  workflow: { trace: [], emits: [], console: [] },
4428
4467
  };
4429
4468
  const appendWorkflowEvent = (event: Record<string, unknown>) => fs.appendFileSync(eventsPath, `${JSON.stringify({ ts: Date.now(), runId: workflowRunId, ...event })}\n`, "utf-8");
4469
+ let indexedState: AsyncStatus["state"] | undefined;
4430
4470
  const persist = () => {
4431
4471
  status.lastUpdate = Date.now();
4432
4472
  writeAtomicJson(statusPath, status);
4473
+ if (indexedState !== status.state) {
4474
+ updateActiveRunIndex(asyncDir, status.state);
4475
+ indexedState = status.state;
4476
+ }
4433
4477
  const job = deps.state.asyncJobs.get(workflowRunId);
4434
4478
  if (job) {
4435
4479
  job.status = status.state;
@@ -4483,10 +4527,24 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4483
4527
  void Promise.resolve().then(async () => {
4484
4528
  const workflowResults: SingleResult[] = [];
4485
4529
  const { action: _action, agent: _agent, task: _task, resume: _resume, tasks: _tasks, chain: _chain, concurrency: _concurrency, foregroundOnly: _foregroundOnly, clarify: _clarify, timeoutMs: _timeoutMs, maxRuntimeMs: _maxRuntimeMs, usageBudget: _usageBudget, missionId: _missionId, mission: _mission, ...workflowChildDefaults } = workflowRequest;
4530
+ const workflowSteps = new Map<string, NonNullable<AsyncStatus["steps"]>[number]>();
4531
+ let projectedTraceLength = 0;
4532
+ let projectedTraceTail: NonNullable<Details["workflow"]>["trace"][number] | undefined;
4486
4533
  const updateTrace = (trace: NonNullable<Details["workflow"]>["trace"]) => {
4487
4534
  status.workflow = { ...(status.workflow ?? { emits: [], console: [] }), trace };
4488
- for (const entry of trace.filter((candidate) => candidate.operation === "run")) {
4489
- const existing = status.steps?.find((step) => step.workflowKey === entry.key);
4535
+ const rebuild = trace.length < projectedTraceLength
4536
+ || (projectedTraceLength > 0 && trace[projectedTraceLength - 1] !== projectedTraceTail);
4537
+ if (rebuild) {
4538
+ workflowSteps.clear();
4539
+ for (const step of status.steps ?? []) {
4540
+ if (step.workflowKey) workflowSteps.set(step.workflowKey, step);
4541
+ }
4542
+ projectedTraceLength = 0;
4543
+ }
4544
+ for (let index = projectedTraceLength; index < trace.length; index += 1) {
4545
+ const entry = trace[index]!;
4546
+ if (entry.operation !== "run") continue;
4547
+ const existing = workflowSteps.get(entry.key);
4490
4548
  if (entry.state === "reused" && existing) continue;
4491
4549
  const mapped = entry.state === "started" || entry.state === "reused" ? "running" : entry.state === "completed" ? "completed" : "failed";
4492
4550
  if (existing) {
@@ -4497,9 +4555,13 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4497
4555
  if (entry.durationMs === undefined) delete existing.durationMs;
4498
4556
  else existing.durationMs = entry.durationMs;
4499
4557
  } else {
4500
- status.steps?.push({ agent: entry.agent ?? entry.key, label: entry.key, workflowKey: entry.key, parentWorkflowRunId: workflowRunId, status: mapped, startedAt: Date.now() });
4558
+ const step: NonNullable<AsyncStatus["steps"]>[number] = { agent: entry.agent ?? entry.key, label: entry.key, workflowKey: entry.key, parentWorkflowRunId: workflowRunId, status: mapped, startedAt: Date.now() };
4559
+ status.steps?.push(step);
4560
+ workflowSteps.set(entry.key, step);
4501
4561
  }
4502
4562
  }
4563
+ projectedTraceLength = trace.length;
4564
+ projectedTraceTail = trace.at(-1);
4503
4565
  projectWorkflowActivity();
4504
4566
  persist();
4505
4567
  appendWorkflowEvent({ type: "subagent.workflow.trace", trace });
@@ -5089,6 +5151,12 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5089
5151
  try {
5090
5152
  const location = resolveAsyncRunLocation(paramsWithResolvedCwd, DIRS.async, DIRS.results);
5091
5153
  const runId = location.resolvedId ?? targetRunId ?? path.basename(location.asyncDir ?? paramsWithResolvedCwd.dir);
5154
+ const directoryStatus = location.asyncDir ? readStatus(location.asyncDir) : null;
5155
+ if (directoryStatus?.mode === "workflow") {
5156
+ const route = resolveWorkflowForegroundSteeringTarget({ state: deps.state, workflowRunId: directoryStatus.runId || runId, asyncDirRoot: DIRS.async });
5157
+ if (!route.ok) return { content: [{ type: "text", text: route.message }], isError: true, details: { mode: "management", results: [] } };
5158
+ return steerWorkflowForegroundTarget({ target: route.target, message, mode: paramsWithResolvedCwd.mode, index: paramsWithResolvedCwd.index, signal });
5159
+ }
5092
5160
  if (location.asyncDir) {
5093
5161
  const unsupported = externalRunnerControlError(location.asyncDir, "steer");
5094
5162
  if (unsupported) return unsupported;
@@ -5124,8 +5192,18 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5124
5192
  return { content: [{ type: "text", text }], isError: true, details: { mode: "management", results: [] } };
5125
5193
  }
5126
5194
  if (resolved?.kind === "nested") return steerNestedRun(omitUndefinedProperties({ target: resolved, message, mode: paramsWithResolvedCwd.mode, index: paramsWithResolvedCwd.index, signal }));
5127
- if (resolved?.kind === "foreground") return { content: [{ type: "text", text: "action='steer' currently supports live async Pi child sessions only; use action='interrupt' or action='resume' for foreground runs." }], isError: true, details: { mode: "management", results: [] } };
5195
+ if (resolved?.kind === "foreground") {
5196
+ const route = resolveWorkflowForegroundSteeringTarget({ state: deps.state, childRunId: resolved.id, asyncDirRoot: DIRS.async });
5197
+ if (!route.ok) return { content: [{ type: "text", text: route.message }], isError: true, details: { mode: "management", results: [] } };
5198
+ return steerWorkflowForegroundTarget({ target: route.target, message, mode: paramsWithResolvedCwd.mode, index: paramsWithResolvedCwd.index, signal });
5199
+ }
5128
5200
  if (resolved?.kind !== "async") return { content: [{ type: "text", text: `No async run found for '${targetRunId}'.` }], isError: true, details: { mode: "management", results: [] } };
5201
+ const resolvedStatus = resolved.location.asyncDir ? readStatus(resolved.location.asyncDir) : null;
5202
+ if (resolvedStatus?.mode === "workflow") {
5203
+ const route = resolveWorkflowForegroundSteeringTarget({ state: deps.state, workflowRunId: resolvedStatus.runId || resolved.id, asyncDirRoot: DIRS.async });
5204
+ if (!route.ok) return { content: [{ type: "text", text: route.message }], isError: true, details: { mode: "management", results: [] } };
5205
+ return steerWorkflowForegroundTarget({ target: route.target, message, mode: paramsWithResolvedCwd.mode, index: paramsWithResolvedCwd.index, signal });
5206
+ }
5129
5207
  if (resolved.location.asyncDir) {
5130
5208
  const unsupported = externalRunnerControlError(resolved.location.asyncDir, "steer");
5131
5209
  if (unsupported) return unsupported;
@@ -5178,6 +5256,28 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5178
5256
  }
5179
5257
  return deps.handleScheduledRunAction(paramsWithResolvedCwd, ctx);
5180
5258
  }
5259
+ if (deps.allowMutatingManagementActions === false && MUTATING_MANAGEMENT_ACTIONS.has(action)) {
5260
+ return {
5261
+ content: [{ type: "text", text: `Action '${action}' is not available from child-safe subagent fanout mode.` }],
5262
+ isError: true,
5263
+ details: { mode: "management" as const, results: [] },
5264
+ };
5265
+ }
5266
+ if (action === "dismiss") {
5267
+ const targetRunId = paramsWithResolvedCwd.runId ?? paramsWithResolvedCwd.id;
5268
+ if (!targetRunId) return { content: [{ type: "text", text: "action='dismiss' requires id." }], isError: true, details: { mode: "management", results: [] } };
5269
+ let resolved: ResolvedSubagentRunId | undefined;
5270
+ try {
5271
+ resolved = resolveSubagentRunId(targetRunId, omitUndefinedProperties({ state: deps.state, nested: nestedResolutionScopeForExecutor(deps) }));
5272
+ } catch (error) {
5273
+ const message = error instanceof Error ? error.message : String(error);
5274
+ return { content: [{ type: "text", text: message }], isError: true, details: { mode: "management", results: [] } };
5275
+ }
5276
+ if (resolved?.kind !== "async") {
5277
+ return { content: [{ type: "text", text: `Run '${targetRunId}' is not a recovered workflow.` }], isError: true, details: { mode: "management", results: [] } };
5278
+ }
5279
+ return dismissRecoveredWorkflow(deps.state, resolved.location);
5280
+ }
5181
5281
  if (action === "stop") {
5182
5282
  const targetRunId = paramsWithResolvedCwd.runId ?? paramsWithResolvedCwd.id;
5183
5283
  const workflowController = targetRunId ? deps.state.workflowControllers?.get(targetRunId) : undefined;
@@ -5189,6 +5289,10 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5189
5289
  if (paramsWithResolvedCwd.dir) {
5190
5290
  try {
5191
5291
  const location = resolveAsyncRunLocation(paramsWithResolvedCwd, DIRS.async, DIRS.results);
5292
+ const existingStatus = readStatus(location.asyncDir ?? "");
5293
+ if (existingStatus?.mode === "workflow" && existingStatus.state === "running") {
5294
+ return { content: [{ type: "text", text: `Workflow ${existingStatus.runId} is not controlled by this extension runtime; reload recovery cannot stop it safely.` }], isError: true, details: { mode: "management", results: [] } };
5295
+ }
5192
5296
  const stopResult = stopAsyncRun(deps.state, location.resolvedId ?? targetRunId ?? path.basename(location.asyncDir ?? paramsWithResolvedCwd.dir), deps.kill, location);
5193
5297
  return stopResult ?? { content: [{ type: "text", text: `No running or queued async run was found for '${targetRunId ?? paramsWithResolvedCwd.dir}'.` }], isError: true, details: { mode: "management", results: [] } };
5194
5298
  } catch (error) {
@@ -5273,13 +5377,6 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5273
5377
  details: { mode: "management" as const, results: [] },
5274
5378
  };
5275
5379
  }
5276
- if (deps.allowMutatingManagementActions === false && MUTATING_MANAGEMENT_ACTIONS.has(action)) {
5277
- return {
5278
- content: [{ type: "text", text: `Action '${action}' is not available from child-safe subagent fanout mode.` }],
5279
- isError: true,
5280
- details: { mode: "management" as const, results: [] },
5281
- };
5282
- }
5283
5380
  return handleManagementAction(action, paramsWithResolvedCwd, {
5284
5381
  ...ctx,
5285
5382
  cwd: requestCwd,
@@ -5432,9 +5529,9 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5432
5529
  if (externalAgent && (!effectiveAsync || effectiveParams.foregroundOnly === true)) {
5433
5530
  return buildRequestedModeError(effectiveParams, `Agent '${externalAgent.name}' uses runner.type='external-cli', which currently supports async/background execution only. Omit async or pass async:true; clarify and foregroundOnly are unsupported.`);
5434
5531
  }
5435
- const foregroundTimeout = resolveForegroundTimeout(
5532
+ const foregroundTimeout = resolveSingleAgentLaunchTimeout(
5436
5533
  effectiveParams,
5437
- effectiveAsync ? undefined : DEFAULT_FOREGROUND_TIMEOUT_MS,
5534
+ effectiveAsync,
5438
5535
  );
5439
5536
  if (foregroundTimeout.error) return buildRequestedModeError(effectiveParams, foregroundTimeout.error);
5440
5537
  const controlConfig = resolveControlConfig(deps.config.control, effectiveParams.control);
@@ -5584,6 +5681,17 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5584
5681
  const foregroundDescription = effectiveParams.task?.trim()
5585
5682
  || effectiveParams.tasks?.[0]?.task?.trim()
5586
5683
  || (effectiveParams.chain ? firstRawChainTask(effectiveParams.chain)?.trim() : undefined);
5684
+ const parentWorkflowStatus = effectiveParams.workflowParentRunId
5685
+ ? readStatus(path.join(DIRS.async, effectiveParams.workflowParentRunId))
5686
+ : null;
5687
+ const workflowSteeringDir = effectiveParams.workflowParentRunId
5688
+ && requestSessionId
5689
+ && deps.state.workflowControllers?.has(effectiveParams.workflowParentRunId)
5690
+ && parentWorkflowStatus?.mode === "workflow"
5691
+ && (parentWorkflowStatus.state === "running" || parentWorkflowStatus.state === "queued")
5692
+ && parentWorkflowStatus.sessionId === requestSessionId
5693
+ ? workflowForegroundSteeringDir(DIRS.async, effectiveParams.workflowParentRunId, runId)
5694
+ : undefined;
5587
5695
  const foregroundControl: ForegroundRunControl | undefined = effectiveAsync
5588
5696
  ? undefined
5589
5697
  : compactOptional<ForegroundRunControl>({
@@ -5592,6 +5700,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5592
5700
  mode: foregroundMode,
5593
5701
  ...(effectiveParams.workflowParentRunId ? { parentWorkflowRunId: effectiveParams.workflowParentRunId } : {}),
5594
5702
  ...(effectiveParams.workflowKey ? { workflowKey: effectiveParams.workflowKey } : {}),
5703
+ workflowSteeringDir,
5595
5704
  startedAt: Date.now(),
5596
5705
  updatedAt: Date.now(),
5597
5706
  cwd: effectiveCwd,