pi-subagents 0.66.0 → 0.67.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.
Files changed (115) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/README.md +4 -3
  3. package/agents/evidence-auditor.md +34 -0
  4. package/agents/reviewer.md +3 -2
  5. package/docs/agents.md +6 -3
  6. package/docs/configuration.md +7 -5
  7. package/docs/extension-api.md +33 -18
  8. package/docs/missions.md +8 -0
  9. package/docs/models.md +1 -1
  10. package/docs/observability.md +4 -4
  11. package/docs/standalone-background.md +49 -0
  12. package/docs/tool-reference.md +18 -8
  13. package/docs/watchdog.md +35 -4
  14. package/docs/workflows.md +26 -12
  15. package/inspector-runner.mjs +2 -2
  16. package/package.json +1 -1
  17. package/prompts/parallel-review.md +1 -1
  18. package/{runner-server-preload.mjs → runner-peer-preload.mjs} +8 -3
  19. package/skills/pi-subagents/SKILL.md +14 -0
  20. package/skills/pi-subagents/references/execution-controls.md +7 -5
  21. package/skills/pi-subagents/references/prompting-and-roles.md +2 -2
  22. package/src/agents/advertised-agent-prompt.ts +34 -3
  23. package/src/agents/agents.ts +6 -0
  24. package/src/agents/builtin-names.ts +1 -0
  25. package/src/api/delegation.ts +4 -0
  26. package/src/api/preflight.ts +76 -45
  27. package/src/api/shared-types.ts +2 -0
  28. package/src/extension/fanout-child.ts +63 -4
  29. package/src/extension/index.ts +20 -8
  30. package/src/extension/public-execution.ts +4 -2
  31. package/src/extension/rpc.ts +4 -0
  32. package/src/extension/schemas.ts +67 -78
  33. package/src/extension/tool-description.ts +29 -82
  34. package/src/inspectors/actions.ts +148 -0
  35. package/src/inspectors/ghostty/actions.ts +74 -0
  36. package/src/inspectors/ghostty/plugin.ts +17 -0
  37. package/src/inspectors/herdr/actions.ts +99 -179
  38. package/src/inspectors/herdr/plugin.ts +20 -0
  39. package/src/inspectors/herdr/project-panes.ts +1 -1
  40. package/src/inspectors/{herdr/inspector-runner.ts → inspector-runner.ts} +12 -12
  41. package/src/inspectors/plugins.ts +8 -0
  42. package/src/inspectors/{herdr/session-roots-codec.ts → session-roots-codec.ts} +3 -14
  43. package/src/inspectors/types.ts +51 -0
  44. package/src/intercom/intercom-bridge.ts +50 -8
  45. package/src/intercom/native-supervisor-channel.ts +22 -13
  46. package/src/runs/background/active-async-capacity.ts +4 -0
  47. package/src/runs/background/async-execution.ts +45 -56
  48. package/src/runs/background/async-resume.ts +5 -9
  49. package/src/runs/background/auto-drain.ts +6 -3
  50. package/src/runs/background/binary-bootstrap.ts +33 -0
  51. package/src/runs/background/fleet-view.ts +30 -2
  52. package/src/runs/background/notify.ts +31 -1
  53. package/src/runs/background/owned-process-tree.ts +29 -2
  54. package/src/runs/background/run-child-session.ts +61 -4
  55. package/src/runs/background/run-status.ts +3 -0
  56. package/src/runs/background/runner-aliases.ts +11 -3
  57. package/src/runs/background/runner-child-launch.ts +2 -0
  58. package/src/runs/background/runner-child-sessions.ts +5 -4
  59. package/src/runs/background/scheduled-runs.ts +40 -13
  60. package/src/runs/background/steering.ts +20 -2
  61. package/src/runs/background/subagent-runner.ts +47 -31
  62. package/src/runs/background/subagent-wait.ts +51 -8
  63. package/src/runs/background/wait-tool.ts +1 -1
  64. package/src/runs/foreground/async-steering-action.ts +18 -7
  65. package/src/runs/foreground/execution.ts +44 -31
  66. package/src/runs/foreground/prompt-audit.ts +3 -1
  67. package/src/runs/foreground/subagent-executor.ts +110 -100
  68. package/src/runs/foreground/workflow-detach-reconcile.ts +2 -0
  69. package/src/runs/foreground/workflow-foreground-steering.ts +2 -1
  70. package/src/runs/shared/acceptance.ts +5 -2
  71. package/src/runs/shared/async-status-projection.ts +4 -0
  72. package/src/runs/shared/capability-ceiling.ts +2 -0
  73. package/src/runs/shared/child-hooks.ts +25 -10
  74. package/src/runs/shared/child-launch.ts +12 -2
  75. package/src/runs/shared/child-lifecycle.ts +6 -3
  76. package/src/runs/shared/child-runtime-config.ts +3 -1
  77. package/src/runs/shared/child-session.ts +33 -2
  78. package/src/runs/shared/child-tool-plan.ts +122 -3
  79. package/src/runs/shared/completion-guard.ts +5 -3
  80. package/src/runs/shared/effective-system-prompt.ts +33 -0
  81. package/src/runs/shared/external-cli-runner.ts +9 -7
  82. package/src/runs/shared/llm-intent-arbiter.ts +12 -3
  83. package/src/runs/shared/model-fallback.ts +2 -0
  84. package/src/runs/shared/orca-progress-tabs.ts +1 -1
  85. package/src/runs/shared/pi-spawn.ts +10 -0
  86. package/src/runs/shared/subagent-prompt-runtime.ts +9 -3
  87. package/src/runs/shared/task-intent.ts +46 -13
  88. package/src/runs/shared/workflow-async-child-guidance.ts +18 -0
  89. package/src/runs/shared/worktree.ts +42 -12
  90. package/src/shared/fork-context.ts +15 -72
  91. package/src/shared/launch-contract.ts +65 -2
  92. package/src/shared/opencode-session-headers.ts +30 -0
  93. package/src/shared/types.ts +4 -1
  94. package/src/slash/delegation-adapters.ts +3 -1
  95. package/src/slash/delegation-request.ts +14 -0
  96. package/src/slash/slash-commands.ts +2 -1
  97. package/src/slash/subagents-admin.ts +11 -4
  98. package/src/tui/fleet-status.ts +164 -19
  99. package/src/tui/fleet.ts +16 -14
  100. package/src/tui/render.ts +149 -28
  101. package/src/watchdog/child-status.ts +8 -0
  102. package/src/watchdog/model-selection.ts +20 -0
  103. package/src/watchdog/permission-arbiter.ts +3 -1
  104. package/src/watchdog/register-child.ts +1 -0
  105. package/src/watchdog/register-main.ts +31 -27
  106. package/src/watchdog/review.ts +132 -67
  107. package/src/watchdog/runtime.ts +82 -20
  108. package/src/watchdog/scope.ts +1 -1
  109. package/src/watchdog/settings.ts +9 -3
  110. package/src/watchdog/tool-actions.ts +13 -12
  111. package/src/watchdog/turn-delta.ts +23 -0
  112. package/src/watchdog/types.ts +4 -0
  113. package/src/workflows/scripted-workflow.ts +237 -7
  114. package/src/workflows/workflow-checklist.ts +2 -2
  115. /package/src/inspectors/{herdr/shell-command.ts → shell-command.ts} +0 -0
@@ -96,8 +96,8 @@ import { applyThinkingSuffix, projectLaunchResolvedChildExtensions, resolvePiLau
96
96
  import type { InheritedChildRuntime } from "../shared/child-launch.ts";
97
97
  import { buildRunnerChildLaunch } from "./runner-child-launch.ts";
98
98
  import { normalizeExtensionBindings } from "../shared/extension-bindings.ts";
99
- import type { ChildSessionFactory } from "../shared/child-session.ts";
100
- import { getSettledReadonlyChild, runChildSession, type ChildEvent, type RunChildSessionInput, type RunChildSessionResult, type StepSteerHandler } from "./run-child-session.ts";
99
+ import type { ChildSessionFactory, DefaultChildSessionFactoryOptions } from "../shared/child-session.ts";
100
+ import { getSettledReadonlyChild, runChildSession, type ChildEvent, type RunChildSessionInput, type RunChildSessionResult, type SteerDelivery, type StepSteerHandler } from "./run-child-session.ts";
101
101
  import { planReadonlyModelContinuation, READONLY_CONTINUATION_PROMPT, type LogicalRecoveryState } from "../shared/readonly-model-continuation.ts";
102
102
  import { getReadonlySessionEvidence } from "../shared/readonly-session-evidence.ts";
103
103
  import { loadRunnerChildSessionFactory } from "./runner-child-sessions.ts";
@@ -114,7 +114,7 @@ import { claimRunFanoutBatch, getRunFanoutBudgetSnapshot } from "../shared/run-f
114
114
  import { nestedSummaryFromAsyncStatus, projectNestedEvents, resolveNestedAsyncDir, writeNestedEvent } from "../shared/nested-events.ts";
115
115
  import { formatModelAttemptNote, formatSubagentModelVerificationError, isContextOverflow, isRetryableModelFailureAttempt, recordRetryableModelFailure } from "../shared/model-fallback.ts";
116
116
  import { markProcessTerminalCandidateLeaseRelease, processTerminalPath, writeProcessTerminalCandidate, type ProcessTerminalCandidate } from "./process-terminal.ts";
117
- import { createSteeringStatus, recordSteeringRequest, steeringStatus, terminalSteeringNoticeState, updateSteeringTarget } from "./steering.ts";
117
+ import { createSteeringStatus, recordSteeringRequest, steeringStatus, terminalSteeringNoticeState, unconsumedSteerReason, updateSteeringTarget } from "./steering.ts";
118
118
  import { PROMPT_REDACTED, detectSubagentError, extractTextFromContent, extractToolArgsPreview, formatEmptyTerminalAssistantResponseError, getFinalOutput, hasEmptyTerminalAssistantResponse, readStatus } from "../../shared/utils.ts";
119
119
  import { evaluateCompletionMutationGuard, expectsImplementationMutation, hasMutationToolCapability, validateImplementationToolContract } from "../shared/completion-guard.ts";
120
120
  import { planCompletionEvidence, projectSettlementDiagnostic } from "../shared/completion-evidence.ts";
@@ -147,7 +147,7 @@ import {
147
147
  } from "../shared/worktree.ts";
148
148
  import { findModelInfo, resolveEffectiveThinking, splitKnownThinkingSuffix } from "../../shared/model-info.ts";
149
149
  import { assertThinkingWithinCeiling } from "../../shared/thinking-ceiling.ts";
150
- import { launchBindingDigest } from "../../shared/launch-contract.ts";
150
+ import { resolveLaunchBinding } from "../../shared/launch-contract.ts";
151
151
  import { writeInitialProgressFile } from "../../shared/settings.ts";
152
152
  import { resolveSubagentIntercomTarget } from "../../intercom/intercom-bridge.ts";
153
153
  import { acceptanceFailureMessage, aggregateAcceptanceReport, buildSkippedAcceptanceLedger, evaluateAcceptance, formatAcceptancePrompt, resolveAcceptanceReportMode, resolveEffectiveAcceptance, stripAcceptanceReport } from "../shared/acceptance.ts";
@@ -184,7 +184,7 @@ const INTERCOM_DETACH_RECEIPT = "Detached for intercom coordination before task
184
184
  // child host.
185
185
  process.env[SUBAGENT_CHILD_ENV] = "1";
186
186
 
187
- interface SubagentRunConfig {
187
+ export interface SubagentRunConfig {
188
188
  id: string;
189
189
  steps: RunnerStep[];
190
190
  resultPath: string;
@@ -232,6 +232,8 @@ interface SubagentRunConfig {
232
232
  globalConcurrencyLimit?: number;
233
233
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
234
234
  runFanoutBudget?: RunFanoutBudgetDescriptor;
235
+ /** Builtin tool names the host runtime provides; used to intersect agent-declared tools. */
236
+ hostAvailableBuiltins?: readonly string[];
235
237
  launchContractDigest?: string;
236
238
  launchResolvedExtensions?: LaunchResolvedChildExtensions;
237
239
  runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensions;
@@ -682,6 +684,8 @@ interface SingleStepContext {
682
684
  registerStop?: (stop: (() => void) | undefined) => void;
683
685
  /** Receives the live child's steer handler while its session runs. */
684
686
  registerSteer?: (steer: StepSteerHandler | undefined) => void;
687
+ /** Reports a live child's later steer consumption or unconsumed settlement. */
688
+ onSteerOutcome?: (request: SteerRequest, delivery: SteerDelivery) => void;
685
689
  timeoutSignal?: AbortSignal;
686
690
  stopSignal?: AbortSignal;
687
691
  timeoutMessage?: string;
@@ -695,6 +699,7 @@ interface SingleStepContext {
695
699
  nestedRoute?: NestedRouteInfo;
696
700
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
697
701
  runFanoutBudget?: RunFanoutBudgetDescriptor;
702
+ hostAvailableBuiltins?: readonly string[];
698
703
  onAttemptStart?: (attempt: { model?: string; thinking?: string; contextLimit?: number }) => void;
699
704
  onChildEvent?: (event: ChildEvent) => void;
700
705
  onExternalProcess?: (process: ExternalProcessStatus) => void;
@@ -811,6 +816,7 @@ export async function runSingleStepInner(
811
816
  capabilityCeiling: step.capabilityCeiling ?? ctx.capabilityCeiling,
812
817
  inheritedCapabilityCeiling: ctx.inheritedChildRuntime?.capabilityCeiling,
813
818
  permissionRules: step.permissionRules,
819
+ hostAvailableBuiltins: ctx.hostAvailableBuiltins,
814
820
  }));
815
821
  const contractTools = resolvedTaskToolPlan.explicitToolAllowlist ? resolvedTaskToolPlan.effectiveToolAllowlist : undefined;
816
822
  const contractError = validateImplementationToolContract({
@@ -1149,31 +1155,27 @@ export async function runSingleStepInner(
1149
1155
  capabilityCeiling: step.capabilityCeiling ?? ctx.capabilityCeiling,
1150
1156
  inheritedCapabilityCeiling: ctx.inheritedChildRuntime?.capabilityCeiling,
1151
1157
  permissionRules: step.permissionRules,
1158
+ hostAvailableBuiltins: ctx.hostAvailableBuiltins,
1152
1159
  }));
1153
1160
  launchResolvedExtensions = projectLaunchResolvedChildExtensions(toolPlan);
1154
- actualLaunchContractDigest = launchBindingDigest(omitUndefinedProperties({
1161
+ actualLaunchContractDigest = resolveLaunchBinding({
1155
1162
  definitionDigest: step.definitionDigest,
1156
- task: step.launchBindingTask ?? task,
1157
- ...(candidate ? { model: candidate } : {}),
1158
- modelCandidates: candidates as string[],
1159
- ...(step.fast !== undefined ? { fast: step.fast } : {}),
1160
- ...(resolveEffectiveThinking(candidate, step.thinking) ? { thinking: resolveEffectiveThinking(candidate, step.thinking) } : {}),
1161
- ...(step.thinkingCeiling ? { thinkingCeiling: step.thinkingCeiling } : {}),
1162
- systemPrompt: step.systemPrompt ?? "",
1163
1163
  systemPromptMode: step.systemPromptMode,
1164
1164
  inheritProjectContext: step.inheritProjectContext,
1165
1165
  inheritGlobalContext: step.inheritGlobalContext,
1166
1166
  inheritSkills: step.inheritSkills,
1167
+ task: step.launchBindingTask ?? task,
1168
+ modelCandidates: candidates as string[],
1169
+ fast: step.fast,
1170
+ thinking: resolveEffectiveThinking(candidate, step.thinking),
1171
+ systemPrompt: step.systemPrompt ?? "",
1167
1172
  skills: step.skills,
1168
- tools: toolPlan.effectiveToolAllowlist,
1169
- ...(toolPlan.excludeTools.length > 0 ? { excludeTools: toolPlan.excludeTools } : {}),
1170
- extensions: toolPlan.extensionArgs,
1171
- mcpDirectTools: toolPlan.effectiveMcpTools,
1172
- ...(step.outputPath ? { outputPath: step.outputPath } : {}),
1173
- ...(step.outputMode ? { outputMode: step.outputMode } : {}),
1174
- ...(step.structuredOutputSchema ? { structuredOutputSchema: step.structuredOutputSchema } : {}),
1175
- ...(extensionBindings ? { extensionBindings } : {}),
1176
- }));
1173
+ toolPlan,
1174
+ outputPath: step.outputPath,
1175
+ outputMode: step.outputMode,
1176
+ structuredOutputSchema: step.structuredOutputSchema,
1177
+ extensionBindings,
1178
+ }).launchContractDigest;
1177
1179
  }
1178
1180
  capabilityAudit = attemptCapabilityAudit;
1179
1181
  // Each attempt rewrites the step output log; synchronous appends keep a
@@ -1201,6 +1203,7 @@ export async function runSingleStepInner(
1201
1203
  registerTimeout: ctx.registerTimeout,
1202
1204
  registerStop: ctx.registerStop,
1203
1205
  registerSteer: ctx.registerSteer,
1206
+ onSteerOutcome: ctx.onSteerOutcome,
1204
1207
  registerWatchdogStatus: (sink) => { watchdogSink = sink; },
1205
1208
  timeoutMessage: ctx.timeoutMessage,
1206
1209
  stopMessage: ctx.stopMessage,
@@ -1213,6 +1216,9 @@ export async function runSingleStepInner(
1213
1216
  modelResponseAliases: step.modelResponseAliases,
1214
1217
  mutationTools: step.mutationTools,
1215
1218
  }));
1219
+ // A parked run still owes completion evidence when it actually finishes.
1220
+ // Stopped/timedOut runs already have terminal failures; checking completion evidence there is meaningless.
1221
+ const completionDiagnosticsEligible = !run.interrupted && !run.stopped && !run.timedOut;
1216
1222
  const toolDiagnostic = run.exitCode === 0 && !run.error ? launch.capture.toolDiagnostic() : undefined;
1217
1223
  const toolAvailabilityError = toolDiagnostic ? formatChildToolDiagnostic(toolDiagnostic) : undefined;
1218
1224
  const runtimeAcknowledgedExtensions = launch.capture.runtimeAcknowledgedExtensions();
@@ -1228,7 +1234,7 @@ export async function runSingleStepInner(
1228
1234
  let structuredOutput: unknown;
1229
1235
  let structuredError: string | undefined;
1230
1236
  let validatedStructuredOutput = false;
1231
- if (effectiveStructuredOutput && run.exitCode === 0 && !run.error && !toolAvailabilityError && !midToolExitError) {
1237
+ if (completionDiagnosticsEligible && effectiveStructuredOutput && run.exitCode === 0 && !run.error && !toolAvailabilityError && !midToolExitError) {
1232
1238
  if (!run.structuredOutputToolInvoked) {
1233
1239
  structuredError = MISSING_STRUCTURED_OUTPUT_CALL_ERROR;
1234
1240
  } else {
@@ -1250,13 +1256,13 @@ export async function runSingleStepInner(
1250
1256
  const errorMessages = validatedStructuredOutput
1251
1257
  ? run.messages.slice(run.structuredOutputMessageStartIndex ?? run.messages.length)
1252
1258
  : run.messages;
1253
- const hiddenError = run.exitCode === 0 && !run.error && !toolAvailabilityError && !structuredError && !midToolExitError
1259
+ const hiddenError = completionDiagnosticsEligible && run.exitCode === 0 && !run.error && !toolAvailabilityError && !structuredError && !midToolExitError
1254
1260
  ? detectSubagentError(errorMessages)
1255
1261
  : null;
1256
1262
  const terminalEmptyAfterUsefulWork = !validatedStructuredOutput
1257
1263
  && hasEmptyTerminalAssistantResponse(run.messages)
1258
1264
  && (run.toolCount > 0 || Boolean(run.finalOutput.trim()));
1259
- const emptyOutputError = run.exitCode === 0
1265
+ const emptyOutputError = completionDiagnosticsEligible && run.exitCode === 0
1260
1266
  && !run.error
1261
1267
  && !toolAvailabilityError
1262
1268
  && !structuredError
@@ -1271,7 +1277,7 @@ export async function runSingleStepInner(
1271
1277
  const mutationEvidence = collectTrackedMutationEvidence(mutationSnapshot, step.cwd ?? ctx.cwd);
1272
1278
  finalMutationEvidence = mutationEvidence;
1273
1279
  const completionMutationEvidence = ctx.trackedMutationEvidenceForCompletionGuard === false ? undefined : mutationEvidence;
1274
- const completionGuard = run.exitCode === 0 && !run.error && !structuredError && !hiddenError?.hasError && !midToolExitError && !emptyOutputError && completionGuardEnabled
1280
+ const completionGuard = completionDiagnosticsEligible && run.exitCode === 0 && !run.error && !structuredError && !hiddenError?.hasError && !midToolExitError && !emptyOutputError && completionGuardEnabled
1275
1281
  ? evaluateCompletionMutationGuard(omitUndefinedProperties({
1276
1282
  agent: step.agent,
1277
1283
  task: taskForCompletionGuard,
@@ -1313,7 +1319,7 @@ export async function runSingleStepInner(
1313
1319
  ? { kind: "structured" as const, path: effectiveStructuredOutput.outputPath, missing: !fs.existsSync(effectiveStructuredOutput.outputPath) }
1314
1320
  : undefined;
1315
1321
  finalRequiredOutputMissing = requiredOutput?.missing;
1316
- const missingRequiredOutputError = formatRequiredOutputError(requiredOutput);
1322
+ const missingRequiredOutputError = completionDiagnosticsEligible ? formatRequiredOutputError(requiredOutput) : undefined;
1317
1323
  const missingRequiredOutputAfterMutation = Boolean(missingRequiredOutputError) && (mutationAttemptObserved || Boolean(mutationEvidence.changedFiles.length));
1318
1324
  const effectiveExitCode = toolAvailabilityError || completionEvidence.legacyFailureError || midToolExitError || structuredError || emptyOutputError || missingRequiredOutputError
1319
1325
  ? 1
@@ -2911,7 +2917,10 @@ export async function runSubagent(
2911
2917
  const applySteerDelivery = (requestId: string, index: number, delivery: { state: "delivered" | "queued" | "failed"; message: string }): void => {
2912
2918
  const lifecycle = steeringStatus(statusPayload);
2913
2919
  const request = lifecycle.recent.find((candidate) => candidate.id === requestId);
2914
- if (!request || !request.targets.some((target) => target.index === index)) return;
2920
+ const target = request?.targets.find((candidate) => candidate.index === index);
2921
+ if (!request || !target) return;
2922
+ if (target.state === "delivered" || target.state === "late" || target.state === "failed") return;
2923
+ if (target.state === delivery.state) return;
2915
2924
  const late = fs.existsSync(steeringMarkerPath(requestId));
2916
2925
  const now = Date.now();
2917
2926
  if (delivery.state === "delivered") {
@@ -3686,10 +3695,12 @@ export async function runSubagent(
3686
3695
  nestedRoute: config.nestedRoute,
3687
3696
  capabilityCeiling: config.capabilityCeiling,
3688
3697
  runFanoutBudget: config.runFanoutBudget,
3698
+ hostAvailableBuiltins: config.hostAvailableBuiltins,
3689
3699
  registerInterrupt: (interrupt) => registerStepInterrupt(fi, interrupt),
3690
3700
  registerTimeout: (interrupt) => registerStepTimeout(fi, interrupt),
3691
3701
  registerStop: (stop) => registerStepStop(fi, stop),
3692
3702
  registerSteer: (steer) => registerStepSteer(fi, steer),
3703
+ onSteerOutcome: (request, delivery) => applySteerDelivery(request.id, fi, delivery),
3693
3704
  timeoutSignal: timeoutAbortController.signal,
3694
3705
  stopSignal: stopAbortController.signal,
3695
3706
  trackedMutationEvidenceForCompletionGuard: false,
@@ -4096,10 +4107,12 @@ export async function runSubagent(
4096
4107
  nestedRoute: config.nestedRoute,
4097
4108
  capabilityCeiling: config.capabilityCeiling,
4098
4109
  runFanoutBudget: config.runFanoutBudget,
4110
+ hostAvailableBuiltins: config.hostAvailableBuiltins,
4099
4111
  registerInterrupt: (interrupt) => registerStepInterrupt(fi, interrupt),
4100
4112
  registerTimeout: (interrupt) => registerStepTimeout(fi, interrupt),
4101
4113
  registerStop: (stop) => registerStepStop(fi, stop),
4102
4114
  registerSteer: (steer) => registerStepSteer(fi, steer),
4115
+ onSteerOutcome: (request, delivery) => applySteerDelivery(request.id, fi, delivery),
4103
4116
  timeoutSignal: timeoutAbortController.signal,
4104
4117
  stopSignal: stopAbortController.signal,
4105
4118
  trackedMutationEvidenceForCompletionGuard: Boolean(worktreeSetup),
@@ -4496,10 +4509,12 @@ export async function runSubagent(
4496
4509
  nestedRoute: config.nestedRoute,
4497
4510
  capabilityCeiling: config.capabilityCeiling,
4498
4511
  runFanoutBudget: config.runFanoutBudget,
4512
+ hostAvailableBuiltins: config.hostAvailableBuiltins,
4499
4513
  registerInterrupt: (interrupt) => registerStepInterrupt(flatIndex, interrupt),
4500
4514
  registerTimeout: (interrupt) => registerStepTimeout(flatIndex, interrupt),
4501
4515
  registerStop: (stop) => registerStepStop(flatIndex, stop),
4502
4516
  registerSteer: (steer) => registerStepSteer(flatIndex, steer),
4517
+ onSteerOutcome: (request, delivery) => applySteerDelivery(request.id, flatIndex, delivery),
4503
4518
  timeoutSignal: timeoutAbortController.signal,
4504
4519
  stopSignal: stopAbortController.signal,
4505
4520
  timeoutMessage,
@@ -4832,7 +4847,7 @@ export async function runSubagent(
4832
4847
  for (const target of request.targets) {
4833
4848
  if (target.state !== "scheduled" && target.state !== "routed" && target.state !== "queued") continue;
4834
4849
  changed = true;
4835
- const reason = target.state === "queued" ? "run ended before queued follow-up delivery" : "child terminated before steering delivery";
4850
+ const reason = target.state === "queued" ? unconsumedSteerReason() : "child terminated before steering delivery";
4836
4851
  updateSteeringLifecycleTarget(request.id, target.index, "failed", Date.now(), { reason });
4837
4852
  markSteeringAttention(target.index);
4838
4853
  emitSteeringEvent("subagent.steer.failed", { type: "steer", id: request.id, ts: request.requestedAt, message: reason }, target.index, { reason });
@@ -5080,7 +5095,8 @@ async function waitForStartupControl(
5080
5095
  throw new Error(`Timed out after ${timeoutMs}ms waiting for runner startup control '${action}'.`);
5081
5096
  }
5082
5097
 
5083
- async function runConfiguredSubagent(config: SubagentRunConfig): Promise<void> {
5098
+ // Both hosts enter here: startup authorization, revival leases and disposal stay shared.
5099
+ export async function runConfiguredSubagent(config: SubagentRunConfig, options?: DefaultChildSessionFactoryOptions): Promise<void> {
5084
5100
  let lease: ReturnType<typeof acquireSessionLease> | undefined;
5085
5101
  let startupCommitted = config.revivalLease === undefined && config.launchBarrierToken === undefined;
5086
5102
  const startupPath = path.join(config.asyncDir, "runner-startup.json");
@@ -5119,7 +5135,7 @@ async function runConfiguredSubagent(config: SubagentRunConfig): Promise<void> {
5119
5135
  }
5120
5136
  }
5121
5137
  }
5122
- const childSessions = await loadRunnerChildSessionFactory(config);
5138
+ const childSessions = await loadRunnerChildSessionFactory(config, options);
5123
5139
  try {
5124
5140
  await runSubagent(config, childSessions);
5125
5141
  } finally {
@@ -214,14 +214,13 @@ function matchesId(run: AsyncRunSummary, id: string): boolean {
214
214
  }
215
215
 
216
216
  function activeDetachedForegroundRuns(params: SubagentWaitParams, deps: SubagentWaitDeps): ForegroundResumeRun[] {
217
- if (!params.id || !deps.state.foregroundRuns) return [];
218
217
  const sessionId = deps.state.currentSessionId;
219
- if (!sessionId) return [];
220
- return [...deps.state.foregroundRuns.values()].filter((run) =>
221
- (run.runId === params.id || run.runId.startsWith(params.id!))
222
- && run.sessionId === sessionId
223
- && run.children.some((child) => child.status === "detached")
218
+ if (!sessionId || !deps.state.foregroundRuns) return [];
219
+ const runs = [...deps.state.foregroundRuns.values()].filter((run) =>
220
+ run.sessionId === sessionId && run.children.some((child) => child.status === "detached")
224
221
  );
222
+ if (params.id) return runs.filter((run) => run.runId === params.id || run.runId.startsWith(params.id!));
223
+ return params.all === true ? runs : [];
225
224
  }
226
225
 
227
226
  function summarizeForegroundChildren(run: ForegroundResumeRun, indices: Set<number>): string {
@@ -534,6 +533,36 @@ async function waitForDetachedForegroundRun(
534
533
  }
535
534
  }
536
535
 
536
+ async function waitForSessionDetachedForegroundRuns(
537
+ runs: ForegroundResumeRun[],
538
+ signal: AbortSignal | undefined,
539
+ deps: SubagentWaitDeps,
540
+ startedAt: number,
541
+ now: () => number,
542
+ pollIntervalMs: number,
543
+ timeoutMs: number,
544
+ ): Promise<AgentToolResult<Details>> {
545
+ const texts: string[] = [];
546
+ for (const run of runs) {
547
+ const one = await waitForDetachedForegroundRun(run, signal, deps, startedAt, now, pollIntervalMs, timeoutMs);
548
+ if (one.isError) return one;
549
+ if (one.details.wait?.reason === "window_elapsed") {
550
+ const activeRunIds = runs.filter((initial) => {
551
+ const current = deps.state.foregroundRuns?.get(initial.runId);
552
+ if (!current || current.sessionId !== initial.sessionId) return false;
553
+ return initial.children.some((child) => child.status === "detached"
554
+ && current.children.some((candidate) => candidate.index === child.index && candidate.status === "detached"));
555
+ }).map((activeRun) => activeRun.runId);
556
+ return windowElapsedResult(
557
+ one.content.map((part) => part.type === "text" ? part.text : "").join("\n"),
558
+ activeRunIds,
559
+ );
560
+ }
561
+ texts.push(one.content.map((part) => part.type === "text" ? part.text : "").join("\n").trim());
562
+ }
563
+ return result(texts.filter(Boolean).join("\n") || `Waited ${formatDuration(now() - startedAt)} for remembered detached foreground run(s); done.`);
564
+ }
565
+
537
566
  /**
538
567
  * Block until the targeted async or remembered detached foreground run finishes,
539
568
  * the timeout elapses, or the turn is aborted. Resolves with a short
@@ -570,7 +599,10 @@ export async function waitForSubagents(
570
599
  let providerSnapshot: BackgroundWorkSnapshot;
571
600
  try {
572
601
  active = activeRunsForSession(params, deps);
573
- foreground = activeDetachedForegroundRuns(params, deps);
602
+ foreground = activeDetachedForegroundRuns(params, deps).map((run) => ({
603
+ ...run,
604
+ children: run.children.map((child) => ({ ...child })),
605
+ }));
574
606
  providerSnapshot = params.id ? { providers: [], items: [] } : backgroundWorkForSession(deps, startedAt);
575
607
  } catch (error) {
576
608
  return result(error instanceof Error ? error.message : String(error), true);
@@ -606,6 +638,9 @@ export async function waitForSubagents(
606
638
 
607
639
  let providerActive = providerSnapshot.items;
608
640
  if (active.length === 0 && providerActive.length === 0) {
641
+ if (waitForAll && !params.id && foreground.length > 0) {
642
+ return waitForSessionDetachedForegroundRuns(foreground, signal, deps, startedAt, now, pollIntervalMs, timeoutMs);
643
+ }
609
644
  return result(params.id
610
645
  ? `No active run matched "${params.id}". Nothing to wait for.`
611
646
  : "No active async runs or registered provider work in this session. Nothing to wait for.");
@@ -697,6 +732,14 @@ export async function waitForSubagents(
697
732
  const recoveryNote = formatCompletionRecovery(completions);
698
733
 
699
734
  if (waitForAll) {
735
+ const foregroundResult = !params.id && foreground.length > 0 && relevantAttention.length === 0
736
+ ? await waitForSessionDetachedForegroundRuns(foreground, signal, deps, startedAt, now, pollIntervalMs, timeoutMs)
737
+ : undefined;
738
+ if (foregroundResult?.isError) return foregroundResult;
739
+ if (foregroundResult?.details.wait?.reason === "window_elapsed") return foregroundResult;
740
+ const foregroundNote = foregroundResult
741
+ ? `\n${foregroundResult.content.map((part) => part.type === "text" ? part.text : "").join("\n")}`
742
+ : "";
700
743
  const scope = params.id
701
744
  ? `run "${params.id}"`
702
745
  : initialProviderIds.size === 0
@@ -704,7 +747,7 @@ export async function waitForSubagents(
704
747
  : `${initialAsyncIds.size} async run(s) and ${initialProviderIds.size} provider item(s)`;
705
748
  const status = relevantAttention.length > 0 ? "attention required" : "done";
706
749
  return result(
707
- `Waited ${elapsed} for ${scope}; ${status}.${outcome}${recoveryNote}${resumeGuidance}${attentionNote} Completion/control events have been observed; inspect status if a notification is not visible yet.`,
750
+ `Waited ${elapsed} for ${scope}; ${status}.${outcome}${recoveryNote}${resumeGuidance}${attentionNote}${foregroundNote} Completion/control events have been observed; inspect status if a notification is not visible yet.`,
708
751
  (deps.failOnFailedRuns === true && failedAsyncCount > 0) || (deps.failOnAttention === true && relevantAttention.length > 0),
709
752
  completions,
710
753
  );
@@ -17,7 +17,7 @@ export function registerWaitTool(
17
17
  Ordinary async subagent runs already notify this session natively when they complete or need attention. In an interactive chat, return control instead of calling this merely to wait. Use this tool for provider jobs, remembered detached foreground runs, or other background work without a native notification path. Headless runs auto-drain current-session subagent work at agent_end; use this tool only when the current turn must receive non-notifying background work results.
18
18
 
19
19
  • { } — return when the first initially active async run or registered provider item finishes, or when a subagent needs attention.
20
- • { all: true } — wait for every async run and provider item that was active when the call began.
20
+ • { all: true } — wait for every async run, provider item, and remembered detached foreground descendant that was active when the call began.
21
21
  • { id: "..." } — wait for one async or remembered detached foreground subagent run (id or prefix).
22
22
  • { id: "...", nonBlocking: true } — resolve the prefix once, persist an exact-run wake subscription, and return immediately. Use this for detached work without native completion delivery; the originating interactive session wakes on completion, failure, attention, reconciliation failure, or timeout.
23
23
  • { stopOnAttention: false } — for blocking waits only, keep waiting through idle or long-thinking attention; supervisor/contact requests still stop the wait.
@@ -3,13 +3,18 @@ import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
4
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
5
5
  import { writeAtomicJson } from "../../shared/atomic-json.ts";
6
- import type { AsyncStatus, Details, SubagentState, ToolBudgetConfig } from "../../shared/types.ts";
6
+ import type { AsyncStatus, Details, SteerActionResult, SubagentState, ToolBudgetConfig } from "../../shared/types.ts";
7
7
  import { readStatus } from "../../shared/utils.ts";
8
8
  import { deliverInterruptRequest, queueRevivalBrief, requestAsyncSteer, type SteerDeliveryMode, type SteerRequest } from "../background/control-channel.ts";
9
9
  import { resolveAsyncResumeTarget } from "../background/async-resume.ts";
10
10
  import { reconcileAsyncRun } from "../background/stale-run-reconciler.ts";
11
11
  import { actionResultFromSteeringStatus, claimSteeringRecovery, createSteeringStatus, recordSteeringRequest, remainingSteeringRecoveryLimits, steeringReceipt, updateSteeringTarget, waitForSteeringAction } from "../background/steering.ts";
12
12
 
13
+ function steerTargetsAccepted(result: Pick<SteerActionResult, "targets"> | undefined): boolean {
14
+ const targets = result?.targets ?? [];
15
+ return targets.length > 0 && targets.every((target) => target.state === "queued" || target.state === "delivered");
16
+ }
17
+
13
18
  export function canQueueRetainedAsyncFollowUp(status: AsyncStatus, index?: number): boolean {
14
19
  const steps = status.steps ?? [];
15
20
  return status.state === "complete"
@@ -170,9 +175,11 @@ export async function steerAsyncRun(input: {
170
175
  }
171
176
  const finalStatus = readStatus(asyncDir);
172
177
  const finalResult = finalStatus?.steering ? actionResultFromSteeringStatus(finalStatus.steering, status.runId, requestId) : undefined;
173
- if (finalResult?.state === "delivered") {
174
- return { content: [{ type: "text", text: steeringReceipt(input.message, `Steering delivered for async run ${status.runId} (request ${requestId}).`) }], details: { mode: "management", results: [], steering: finalResult } };
175
- }
178
+ const acceptedReply = (accepted: SteerActionResult): AgentToolResult<Details> => {
179
+ const acceptedState = accepted.state === "delivered" ? "delivered" : "queued";
180
+ return { content: [{ type: "text", text: steeringReceipt(input.message, `Steering ${acceptedState} for async run ${status.runId} (request ${requestId}).`) }], details: { mode: "management", results: [], steering: accepted } };
181
+ };
182
+ if (finalResult && steerTargetsAccepted(finalResult)) return acceptedReply(finalResult);
176
183
  const running = (finalStatus?.steps ?? status.steps ?? []).filter((step) => step.status === "running");
177
184
  const recoveryAllowed = (input.mode ?? "steer") === "steer" && status.mode === "single" && status.isNested !== true && running.length === 1 && Boolean(finalStatus?.steering) && (input.index === undefined || input.index === 0);
178
185
  if (recoveryAllowed && finalResult?.state !== "scheduled" && input.recover) {
@@ -186,17 +193,21 @@ export async function steerAsyncRun(input: {
186
193
  try {
187
194
  const latest = readStatus(asyncDir);
188
195
  const latestResult = latest?.steering ? actionResultFromSteeringStatus(latest.steering, status.runId, requestId) : undefined;
189
- if (latestResult?.state === "delivered") return { content: [{ type: "text", text: steeringReceipt(input.message, `Steering delivered for async run ${status.runId} (request ${requestId}).`) }], details: { mode: "management", results: [], steering: latestResult } };
196
+ if (latestResult && steerTargetsAccepted(latestResult)) return acceptedReply(latestResult);
190
197
  const committedAt = Date.now();
191
198
  input.onBeforeRecoveryClaim?.(requestId, committedAt);
192
199
  const { claimPath, markerPath } = claimSteeringRecovery(asyncDir, { requestId, sourceRunId: status.runId, committedAt });
193
200
  input.onRecoveryCommitted?.(requestId, committedAt);
194
201
  const preCommitStatus = readStatus(asyncDir);
195
202
  const preCommitResult = preCommitStatus?.steering ? actionResultFromSteeringStatus(preCommitStatus.steering, status.runId, requestId) : undefined;
196
- if (preCommitResult?.state === "delivered" && preCommitResult.targets.every((target) => target.deliveredAt !== undefined && target.deliveredAt <= committedAt)) {
203
+ if (
204
+ preCommitResult
205
+ && preCommitResult.targets.length > 0
206
+ && preCommitResult.targets.every((target) => target.state === "queued" || (target.deliveredAt !== undefined && target.deliveredAt <= committedAt))
207
+ ) {
197
208
  fs.rmSync(markerPath, { force: true });
198
209
  fs.rmSync(claimPath, { force: true });
199
- return { content: [{ type: "text", text: steeringReceipt(input.message, `Steering delivered for async run ${status.runId} (request ${requestId}).`) }], details: { mode: "management", results: [], steering: preCommitResult } };
210
+ return acceptedReply(preCommitResult);
200
211
  }
201
212
  try {
202
213
  deliverInterruptRequest({ asyncDir, source: "steering-recovery" });
@@ -6,8 +6,8 @@ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
6
6
  import * as path from "node:path";
7
7
  import type { Message } from "@earendil-works/pi-ai";
8
8
  import { discoverAgents, formatUnknownAgentError, unknownAgentDiagnosticContext, type AgentConfig } from "../../agents/agents.ts";
9
- import { appendAgentRefinementOverlay } from "../../agents/agent-refinements.ts";
10
9
  import { alignForkedSessionCwd } from "../../shared/fork-session-cwd.ts";
10
+ import { buildEffectiveSystemPrompt } from "../shared/effective-system-prompt.ts";
11
11
  import {
12
12
  ensureArtifactsDir,
13
13
  formatOutputArtifactContent,
@@ -52,8 +52,7 @@ import {
52
52
  boundStreamedRecentOutput,
53
53
  boundStreamedToolCalls,
54
54
  } from "../../shared/utils.ts";
55
- import { buildSkillInjection, resolveSkillsWithFallback } from "../../agents/skills.ts";
56
- import { buildAgentMemoryInjection } from "../../agents/agent-memory.ts";
55
+ import { resolveSkillsWithFallback } from "../../agents/skills.ts";
57
56
  import { effectiveToolTimeoutMs, formatToolTimeoutMessage, resolveToolTimeoutMs, toolTimeoutCallKey, toolTimeoutFromEnv } from "../shared/tool-timeout.ts";
58
57
  import { evaluateCompletionMutationGuard, expectsImplementationMutation, hasMutationToolCapability, validateImplementationToolContract } from "../shared/completion-guard.ts";
59
58
  import { planCompletionEvidence } from "../shared/completion-evidence.ts";
@@ -75,7 +74,7 @@ import { MISSING_STRUCTURED_ACCEPTANCE_REPORT_ERROR, MISSING_STRUCTURED_OUTPUT_C
75
74
  import { formatMidToolExitError, isOrdinaryToolForMidToolExit } from "../shared/process-signal.ts";
76
75
  import { formatChildToolDiagnostic } from "../shared/tool-availability.ts";
77
76
  import { buildTimeoutRecoverySummary, collectTrackedMutationEvidence, snapshotTrackedMutations } from "../shared/mutation-evidence.ts";
78
- import { captureSingleOutputSnapshot, extractChildWrittenOutput, finalizeSingleOutput, formatSavedOutputReference, hasSingleOutputChangedSinceSnapshot, injectOutputPathSystemPrompt, resolveSingleOutput, validateFileOnlyOutputMode, type SingleOutputSnapshot } from "../shared/single-output.ts";
77
+ import { captureSingleOutputSnapshot, extractChildWrittenOutput, finalizeSingleOutput, formatSavedOutputReference, hasSingleOutputChangedSinceSnapshot, resolveSingleOutput, validateFileOnlyOutputMode, type SingleOutputSnapshot } from "../shared/single-output.ts";
79
78
  import {
80
79
  buildModelCandidates,
81
80
  formatSubagentModelVerificationError,
@@ -100,7 +99,7 @@ import { PROMPT_REDACTED } from "../../shared/utils.ts";
100
99
  import { attachContractProjections, isAgentContract } from "../shared/agent-contract.ts";
101
100
  import { initialToolBudgetState, toolBudgetState } from "../shared/tool-budget.ts";
102
101
  import { resolveWatchdogConfig } from "../../watchdog/settings.ts";
103
- import { agentDefinitionDigest, launchBindingDigest } from "../../shared/launch-contract.ts";
102
+ import { resolveLaunchBinding } from "../../shared/launch-contract.ts";
104
103
  import { consumeWorkflowChildPermit } from "../../shared/workflow-child-permit.ts";
105
104
  import { projectChildLifecycle, type ChildLifecycleAction, type ChildLifecycleState } from "../shared/child-lifecycle.ts";
106
105
  import {
@@ -113,7 +112,7 @@ import {
113
112
  type ChildWatchdogStatusEvent,
114
113
  } from "../../watchdog/child-status.ts";
115
114
  import { buildInProcessChildLaunch, createReportedChildSessionInput } from "../shared/child-launch.ts";
116
- import { childSessionFactory, projectChildSessionEventForJson, type ChildSession, type ChildSessionEvent } from "../shared/child-session.ts";
115
+ import { childSessionFactory, childSessionHasQueuedMessages, projectChildSessionEventForJson, type ChildSession, type ChildSessionEvent } from "../shared/child-session.ts";
117
116
 
118
117
  const artifactOutputByResult = new WeakMap<SingleResult, string>();
119
118
  const acceptanceOutputByResult = new WeakMap<SingleResult, string>();
@@ -399,6 +398,7 @@ async function runSingleAttempt(
399
398
  : undefined;
400
399
  let onWatchdogStatus: ((event: ChildWatchdogStatusEvent) => void) | undefined;
401
400
  const launch = buildInProcessChildLaunch({
401
+ extensionBindings: options.extensionBindings,
402
402
  sessionEnabled: shared.sessionEnabled,
403
403
  sessionDir: options.sessionDir,
404
404
  sessionFile: options.sessionFile,
@@ -442,6 +442,7 @@ async function runSingleAttempt(
442
442
  thinkingCeiling: options.thinkingCeiling,
443
443
  maxSubagentDepth: options.maxSubagentDepth,
444
444
  runtimeSnapshotHost: options.runtimeSnapshotHost,
445
+ hostAvailableBuiltins: options.hostAvailableBuiltins,
445
446
  inherited: options.childRuntime,
446
447
  host: "parent",
447
448
  });
@@ -482,24 +483,16 @@ async function runSingleAttempt(
482
483
  ...(toolPlan.capabilityAudit ? { capabilityAudit: toolPlan.capabilityAudit } : {}),
483
484
  };
484
485
  }
485
- const launchContractDigest = launchBindingDigest({
486
- definitionDigest: agentDefinitionDigest(agent),
486
+ const fast = options.fast ?? agent.fast;
487
+ const { launchContractDigest } = resolveLaunchBinding({
488
+ agent,
487
489
  task: shared.originalTask ?? task,
488
- ...(modelArg ? { model: modelArg } : {}),
489
- modelCandidates: shared.modelCandidates,
490
- ...((options.fast ?? agent.fast) !== undefined ? { fast: options.fast ?? agent.fast } : {}),
490
+ modelCandidates: shared.modelCandidates ?? [],
491
+ ...(fast !== undefined ? { fast } : {}),
491
492
  ...(resolvedThinking ? { thinking: resolvedThinking } : {}),
492
- ...(options.thinkingCeiling ? { thinkingCeiling: options.thinkingCeiling } : {}),
493
493
  systemPrompt: effectiveSystemPrompt,
494
- systemPromptMode: agent.systemPromptMode,
495
- inheritProjectContext: agent.inheritProjectContext,
496
- inheritGlobalContext: agent.inheritGlobalContext,
497
- inheritSkills: agent.inheritSkills,
498
494
  skills: shared.resolvedSkillNames ?? [],
499
- tools: toolPlan.effectiveToolAllowlist,
500
- ...(toolPlan.excludeTools.length > 0 ? { excludeTools: toolPlan.excludeTools } : {}),
501
- extensions: toolPlan.extensionArgs,
502
- mcpDirectTools: toolPlan.effectiveMcpTools,
495
+ toolPlan,
503
496
  ...(options.outputPath ? { outputPath: options.outputPath } : {}),
504
497
  outputMode: options.outputMode ?? "inline",
505
498
  ...(options.structuredOutput ? { structuredOutputSchema: options.structuredOutput.schema } : {}),
@@ -681,6 +674,7 @@ async function runSingleAttempt(
681
674
  let forcedTermination = false;
682
675
  let cleanTerminalAssistantStopReceived = false;
683
676
  let agentSettledReceived = false;
677
+ let queuedDrainHold = false;
684
678
  let compactionStartedReceived = false;
685
679
  let finalDrainTimer: NodeJS.Timeout | undefined;
686
680
  let finalHardFinishTimer: NodeJS.Timeout | undefined;
@@ -707,14 +701,28 @@ async function runSingleAttempt(
707
701
  finalHardFinishTimer = undefined;
708
702
  }
709
703
  };
704
+ const observeQueuedDrainHold = (): boolean => {
705
+ if (childSessionHasQueuedMessages(session)) queuedDrainHold = true;
706
+ return queuedDrainHold;
707
+ };
710
708
  const startFinalDrain = () => {
711
709
  if (childWatchdogIsActive(childWatchdogState)) {
712
710
  armWatchdogTail();
713
711
  return;
714
712
  }
713
+ if (sessionSettled || finalDrainTimer || lifecycleFinished) return;
714
+ if (observeQueuedDrainHold()) return;
715
+ armFinalDrainTimer();
716
+ };
717
+ const armFinalDrainTimer = () => {
715
718
  if (sessionSettled || finalDrainTimer || lifecycleFinished) return;
716
719
  finalDrainTimer = setTimeout(() => {
717
720
  if (lifecycleFinished || sessionSettled) return;
721
+ if (capture.finalDrainHeld() || observeQueuedDrainHold()) {
722
+ finalDrainTimer = undefined;
723
+ armFinalDrainTimer();
724
+ return;
725
+ }
718
726
  forcedTermination = true;
719
727
  if (!cleanTerminalAssistantStopReceived && !agentSettledReceived && !assistantError) {
720
728
  result.error = result.error ?? `Subagent session did not settle within ${FINAL_STOP_GRACE_MS}ms after its terminal event. Aborting it.`;
@@ -746,6 +754,8 @@ async function runSingleAttempt(
746
754
  }
747
755
  const applyChildLifecycle = (action: ChildLifecycleAction): void => {
748
756
  if (action === "cancel-drain") {
757
+ cleanTerminalAssistantStopReceived = false;
758
+ agentSettledReceived = false;
749
759
  clearFinalDrainTimers();
750
760
  clearWatchdogTailTimer();
751
761
  return;
@@ -992,6 +1002,9 @@ async function runSingleAttempt(
992
1002
  compactionStartedReceived = false;
993
1003
  afterCompactionSettlement = false;
994
1004
  }
1005
+ if (evt.type === "turn_start" || evt.type === "agent_start" || evt.type === "auto_retry_start") {
1006
+ queuedDrainHold = false;
1007
+ }
995
1008
  if (evt.type === "agent_start" || evt.type === "auto_retry_start") {
996
1009
  compactionStartedReceived = false;
997
1010
  afterCompactionSettlement = false;
@@ -1378,6 +1391,16 @@ async function runSingleAttempt(
1378
1391
  return;
1379
1392
  }
1380
1393
  session = created;
1394
+ const steer = created.steer.bind(created);
1395
+ const followUp = created.followUp.bind(created);
1396
+ created.steer = async (text) => {
1397
+ if (cleanTerminalAssistantStopReceived || agentSettledReceived) queuedDrainHold = true;
1398
+ return steer(text);
1399
+ };
1400
+ created.followUp = async (text) => {
1401
+ if (cleanTerminalAssistantStopReceived || agentSettledReceived) queuedDrainHold = true;
1402
+ return followUp(text);
1403
+ };
1381
1404
  created.detached = detached;
1382
1405
  unsubscribe = created.subscribe((event) => processEvent(event as Parameters<typeof processEvent>[0]));
1383
1406
  if (abortedBySignal || interruptedByControl || result.timedOut) {
@@ -1779,17 +1802,7 @@ async function runSyncCompletionInner(
1779
1802
  error: "Skills not found: pi-subagents",
1780
1803
  }, options.context));
1781
1804
  }
1782
- let systemPrompt = agent.systemPrompt?.trim() || "";
1783
- if (resolvedSkills.length > 0) {
1784
- const skillInjection = buildSkillInjection(resolvedSkills);
1785
- systemPrompt = systemPrompt ? `${systemPrompt}\n\n${skillInjection}` : skillInjection;
1786
- }
1787
- const memoryInjection = buildAgentMemoryInjection(agent, skillCwd);
1788
- if (memoryInjection) {
1789
- systemPrompt = systemPrompt ? `${systemPrompt}\n\n${memoryInjection}` : memoryInjection;
1790
- }
1791
- systemPrompt = appendAgentRefinementOverlay(systemPrompt, { cwd: skillCwd, agentName });
1792
- systemPrompt = injectOutputPathSystemPrompt(systemPrompt, options.outputPath, agent);
1805
+ const systemPrompt = buildEffectiveSystemPrompt({ agent, resolvedSkills, cwd: skillCwd, ...(options.outputPath ? { outputPath: options.outputPath } : {}) });
1793
1806
 
1794
1807
  const candidates = buildModelCandidates(
1795
1808
  options.modelOverride ?? agent.model,
@@ -2,6 +2,7 @@ import type { Agent, StreamFn, ThinkingLevel } from "@earendil-works/pi-agent-co
2
2
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
3
3
  import type { ProviderHeaders } from "@earendil-works/pi-ai";
4
4
  import { agentStreamOptions } from "../../shared/agent-stream-options.ts";
5
+ import { opencodeSessionHeaders } from "../../shared/opencode-session-headers.ts";
5
6
  import type { ForegroundRunControl } from "../../shared/types.ts";
6
7
  export type PromptAuditView = "authored" | "runtime" | "effective";
7
8
 
@@ -75,11 +76,12 @@ export async function rewritePromptWithGuidance(input: {
75
76
  const baseStreamFn = input.streamFn ?? (registeredProvider?.streamSimple && registeredProvider.api === model.api
76
77
  ? registeredProvider.streamSimple
77
78
  : streamSimple);
79
+ const sessionId = input.ctx.sessionManager.getSessionId();
78
80
  const streamFn: StreamFn = (nextModel, context, streamOptions) => baseStreamFn(nextModel, context, {
79
81
  ...streamOptions,
80
82
  ...(auth.apiKey ? { apiKey: auth.apiKey } : {}),
81
83
  env: auth.env || streamOptions?.env ? { ...(auth.env ?? {}), ...(streamOptions?.env ?? {}) } : undefined,
82
- headers: { ...(streamOptions?.headers ?? {}), ...(auth.headers ?? {}) },
84
+ headers: { ...opencodeSessionHeaders(nextModel, sessionId), ...(streamOptions?.headers ?? {}), ...(auth.headers ?? {}) },
83
85
  });
84
86
  const ctxThinking = (input.ctx as { getThinkingLevel?: () => ThinkingLevel }).getThinkingLevel?.();
85
87
  const agent = new Agent({