pi-subagents 0.65.0 → 0.65.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 (49) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/docs/agents.md +1 -1
  3. package/docs/configuration.md +16 -0
  4. package/docs/extension-api.md +3 -0
  5. package/docs/tool-reference.md +8 -2
  6. package/docs/workflows.md +8 -0
  7. package/package.json +3 -1
  8. package/runner-server-preload.mjs +13 -0
  9. package/skills/pi-subagents/SKILL.md +2 -1
  10. package/skills/pi-subagents/references/execution-controls.md +5 -1
  11. package/skills/pi-subagents/references/multi-lane-orchestration.md +2 -0
  12. package/src/api/preflight.ts +5 -1
  13. package/src/extension/config.ts +4 -2
  14. package/src/extension/index.ts +31 -2
  15. package/src/extension/schemas.ts +1 -1
  16. package/src/extension/tool-description.ts +5 -1
  17. package/src/integrations/pi-web-session-liveness.ts +73 -0
  18. package/src/intercom/native-supervisor-channel.ts +22 -36
  19. package/src/intercom/supervisor-ui.ts +3 -2
  20. package/src/missions/workflow-state.ts +37 -16
  21. package/src/runs/background/async-execution.ts +8 -1
  22. package/src/runs/background/async-resume.ts +3 -1
  23. package/src/runs/background/async-retention.ts +9 -0
  24. package/src/runs/background/notify.ts +2 -0
  25. package/src/runs/background/retained-nested-route-tracker.ts +96 -0
  26. package/src/runs/background/run-child-session.ts +2 -1
  27. package/src/runs/background/runner-aliases.ts +32 -5
  28. package/src/runs/background/subagent-runner.ts +19 -0
  29. package/src/runs/foreground/execution.ts +15 -1
  30. package/src/runs/foreground/foreground-history.ts +3 -1
  31. package/src/runs/foreground/prompt-audit.ts +9 -5
  32. package/src/runs/foreground/subagent-executor.ts +61 -34
  33. package/src/runs/shared/acceptance.ts +14 -1
  34. package/src/runs/shared/child-session.ts +13 -18
  35. package/src/runs/shared/llm-intent-arbiter.ts +20 -11
  36. package/src/runs/shared/model-exclusions.ts +2 -1
  37. package/src/runs/shared/model-fallback.ts +31 -2
  38. package/src/runs/shared/nested-events.ts +3 -3
  39. package/src/runs/shared/parallel-utils.ts +1 -0
  40. package/src/runs/shared/worktree-cleanup-plan.ts +6 -3
  41. package/src/runs/shared/worktree.ts +75 -10
  42. package/src/shared/model-response-aliases.ts +13 -0
  43. package/src/shared/types.ts +8 -0
  44. package/src/shared/utils.ts +3 -0
  45. package/src/shared/watch-strategy.ts +2 -0
  46. package/src/tui/fleet-status.ts +1 -1
  47. package/src/tui/render.ts +21 -10
  48. package/src/workflows/scripted-workflow.ts +32 -6
  49. package/src/workflows/workflow-checklist.ts +4 -3
@@ -49,6 +49,7 @@ import { updateActiveRunIndex } from "../background/active-run-index.ts";
49
49
  import { steeringReceipt } from "../background/steering.ts";
50
50
  import { acquireActiveAsyncCapacity, ActiveAsyncCapacityError, getActiveAsyncCapacitySnapshot, resolveAbandonedSlotReleaseAfterMs, resolveMaxActiveAsyncRunsPerSession, transferActiveAsyncCapacity, type ActiveAsyncCapacityHandle } from "../background/active-async-capacity.ts";
51
51
  import { isScheduledRunAction } from "../background/scheduled-runs.ts";
52
+ import { encodeIndexSegment } from "../background/index-segment.ts";
52
53
  import { enqueueChainAppendRequest, readPendingChainAppendRequests, runnerStepOutputNames } from "../background/chain-append.ts";
53
54
  import { ChainOutputValidationError, validateChainOutputBindingsWithContext } from "../shared/chain-outputs.ts";
54
55
  import { normalizeGateAcceptance, resolveAcceptanceReportMode, validateExecutionAcceptance } from "../shared/acceptance.ts";
@@ -60,6 +61,7 @@ import { applyIntercomBridgeToAgent, INTERCOM_BRIDGE_MARKER, resolveIntercomBrid
60
61
  import { formatControlIntercomMessage, formatControlNoticeMessage, resolveControlConfig, shouldNotifyControlEvent } from "../shared/subagent-control.ts";
61
62
  import { formatSpawnBudget, getSpawnBudgetSnapshot, grantSpawnBudget, preflightSpawnBudget, preflightSpawnBudgetGrant, reserveSpawnBudget } from "../shared/spawn-budget.ts";
62
63
  import { claimRunFanoutBatch, claimRunFanoutBatchWithCommit, createRunFanoutBudget, formatRunFanoutBudget, getRunFanoutBudgetSnapshot, readRunFanoutBudgetDescriptor, RunFanoutLimitError, writeRunFanoutBudgetDescriptor } from "../shared/run-fanout-budget.ts";
64
+ import { retainLiveForegroundNestedRoute } from "../../integrations/pi-web-session-liveness.ts";
63
65
  import { validateToolBudgetConfig } from "../shared/tool-budget.ts";
64
66
  import { usageBudgetExceededMessage, usageBudgetState, validateUsageBudgetConfig } from "../shared/usage-budget.ts";
65
67
  import { intersectSubagentCapabilityCeilings, resolveCurrentSubagentCapabilityCeiling, type ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
@@ -438,6 +440,7 @@ interface ExecutorDeps {
438
440
  allowMutatingManagementActions?: boolean;
439
441
  activateSupervisorTransport?: () => void;
440
442
  refreshResultDelivery?: () => void;
443
+ trackRetainedNestedRoute?: (rootRunId: string) => void;
441
444
  kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean;
442
445
  /** Set when this executor runs inside a child session; carries the runtime settings the host passes instead of environment variables. */
443
446
  childRuntime?: ChildRuntimeConfig;
@@ -526,9 +529,16 @@ function loadWorkflowScriptPath(params: SubagentParamsLike, runtimeCwd: string):
526
529
  return { params: { ...rest, workflowScript } };
527
530
  }
528
531
 
529
- function removeForegroundControlIfIdle(state: SubagentState, runId: string): boolean {
532
+ export function removeForegroundControlIfIdle(state: SubagentState, runId: string, trackRetainedNestedRoute?: (rootRunId: string) => void): boolean {
530
533
  const control = state.foregroundControls.get(runId);
531
534
  if (control && (!foregroundSchedulingSettled(control) || (control.activeChildren?.size ?? 0) > 0)) return false;
535
+ if (control?.nestedRoute && trackRetainedNestedRoute) {
536
+ try {
537
+ if (retainLiveForegroundNestedRoute(state, control.nestedRoute)) trackRetainedNestedRoute(runId);
538
+ } catch (error) {
539
+ console.error(`Failed to retain live nested descendants for foreground run '${runId}':`, error);
540
+ }
541
+ }
532
542
  state.foregroundControls.delete(runId);
533
543
  if (state.lastForegroundControlId === runId) state.lastForegroundControlId = null;
534
544
  return true;
@@ -723,7 +733,7 @@ function foregroundChildActivityFromProgress(progress: SingleResult["progress"]
723
733
  };
724
734
  }
725
735
 
726
- function rememberForegroundRun(state: SubagentState, input: { runId: string; mode: "single" | "parallel" | "chain"; cwd: string; sessionId: string | null; results: SingleResult[]; params: SubagentParamsLike; effectiveOutput?: string | boolean; effectiveOutputMode: OutputMode; extensionBindings?: ExtensionBindings }): void {
736
+ function rememberForegroundRun(state: SubagentState, input: { modelResponseAliases?: Record<string, string[]>; runId: string; mode: "single" | "parallel" | "chain"; cwd: string; sessionId: string | null; results: SingleResult[]; params: SubagentParamsLike; effectiveOutput?: string | boolean; effectiveOutputMode: OutputMode; extensionBindings?: ExtensionBindings }): void {
727
737
  state.foregroundRuns ??= new Map();
728
738
  const previous = state.foregroundRuns.get(input.runId);
729
739
  const updatedAt = Date.now();
@@ -735,6 +745,7 @@ function rememberForegroundRun(state: SubagentState, input: { runId: string; mod
735
745
  updatedAt,
736
746
  children: input.results.map((result, index) => {
737
747
  const resumeContract = omitUndefinedProperties({
748
+ modelResponseAliases: input.modelResponseAliases,
738
749
  outputSchema: input.params.outputSchema,
739
750
  agentContract: input.params.agentContract,
740
751
  acceptance: input.params.acceptance,
@@ -1313,6 +1324,7 @@ function appendStepToAsyncChain(input: {
1313
1324
  currentModelProvider: parentModel?.provider,
1314
1325
  currentModel: parentModel,
1315
1326
  modelScope: discoveredForAppend.modelScope,
1327
+ modelResponseAliases: input.deps.config.modelResponseAliases,
1316
1328
  interactive: input.ctx.hasUI,
1317
1329
  permissions: input.deps.config.permissions,
1318
1330
  childRuntime: input.deps.childRuntime,
@@ -1694,6 +1706,7 @@ async function resumeExternalJobFollowUp(input: {
1694
1706
  currentModelProvider: parentModel?.provider,
1695
1707
  currentModel: parentModel,
1696
1708
  modelScope: input.modelScope,
1709
+ modelResponseAliases: input.deps.config.modelResponseAliases,
1697
1710
  interactive: input.ctx.hasUI,
1698
1711
  permissions: input.deps.config.permissions,
1699
1712
  childRuntime: input.deps.childRuntime,
@@ -1947,6 +1960,7 @@ async function resumeAsyncRun(input: {
1947
1960
  currentModelProvider: parentModel?.provider,
1948
1961
  currentModel: parentModel,
1949
1962
  modelScope,
1963
+ modelResponseAliases: input.deps.config.modelResponseAliases,
1950
1964
  interactive: input.ctx.hasUI,
1951
1965
  permissions: input.deps.config.permissions,
1952
1966
  childRuntime: input.deps.childRuntime,
@@ -2055,6 +2069,8 @@ async function resumeAsyncRun(input: {
2055
2069
  currentModelProvider: parentModel?.provider,
2056
2070
  currentModel: parentModel,
2057
2071
  modelScope,
2072
+ // Absence in the retained contract is meaningful; never acquire current aliases.
2073
+ modelResponseAliases: recoveryDescriptor ? recoveryDescriptor.modelResponseAliases : foregroundContract?.modelResponseAliases,
2058
2074
  interactive: input.ctx.hasUI,
2059
2075
  permissions: input.deps.config.permissions,
2060
2076
  childRuntime: input.deps.childRuntime,
@@ -3210,6 +3226,7 @@ async function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
3210
3226
  currentModelProvider: parentModel?.provider,
3211
3227
  currentModel: parentModel,
3212
3228
  modelScope: data.modelScope,
3229
+ modelResponseAliases: deps.config.modelResponseAliases,
3213
3230
  interactive: ctx.hasUI,
3214
3231
  permissions: deps.config.permissions,
3215
3232
  childRuntime: deps.childRuntime,
@@ -3808,6 +3825,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3808
3825
  }));
3809
3826
  }
3810
3827
 
3828
+ const modelResponseAliases = deps.config.modelResponseAliases === undefined ? undefined : structuredClone(deps.config.modelResponseAliases);
3811
3829
  const forwardSingleUpdate = onUpdate
3812
3830
  ? (update: AgentToolResult<Details>) => {
3813
3831
  if (foregroundControl) updateForegroundChild(foregroundControl, 0, update.details?.progress?.[0]);
@@ -3867,6 +3885,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3867
3885
  thinkingCeiling: agentConfig.maxThinking,
3868
3886
  extensionBindings: params.extensionBindings,
3869
3887
  availableModels,
3888
+ modelResponseAliases,
3870
3889
  preferredModelProvider: currentProvider,
3871
3890
  modelScope: modelScopes,
3872
3891
  skills: effectiveSkills,
@@ -3911,7 +3930,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3911
3930
  try {
3912
3931
  if (foregroundControl) finishForegroundChild(foregroundControl, 0);
3913
3932
  } finally {
3914
- removeForegroundControlIfIdle(deps.state, runId);
3933
+ removeForegroundControlIfIdle(deps.state, runId, deps.trackRetainedNestedRoute);
3915
3934
  }
3916
3935
  }
3917
3936
  }
@@ -3984,7 +4003,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3984
4003
  usageBudget: usageBudgetState(data.usageBudget, totalCost),
3985
4004
  ...(worktreeHandoff?.reference ? { parallelHandoff: worktreeHandoff.reference } : {}),
3986
4005
  }));
3987
- rememberForegroundRun(deps.state, { runId, mode: "single", cwd: singleCwd, sessionId: data.parentSessionId, results: details.results, params, effectiveOutput, effectiveOutputMode, extensionBindings: params.extensionBindings });
4006
+ rememberForegroundRun(deps.state, { modelResponseAliases, runId, mode: "single", cwd: singleCwd, sessionId: data.parentSessionId, results: details.results, params, effectiveOutput, effectiveOutputMode, extensionBindings: params.extensionBindings });
3988
4007
 
3989
4008
  const suppressRoutineResultIntercom = shouldSuppressRoutineResultIntercom({ suppressRoutineResultIntercom: params.suppressRoutineResultIntercom, results: [r] });
3990
4009
  if (!r.detached && !r.interrupted && !suppressRoutineResultIntercom) {
@@ -4140,9 +4159,13 @@ function workflowChildResult(
4140
4159
  ? result.details.results[0].finalOutput
4141
4160
  : receiptOutput;
4142
4161
  const childError = result.details.results.map((child) => child.error).find((error): error is string => Boolean(error));
4143
- const failureError = childError && receiptOutput && receiptOutput !== childError
4144
- ? `${childError}\n\n${receiptOutput}`
4162
+ const failureErrorBase = childError && receiptOutput
4163
+ ? receiptOutput.includes(childError) ? receiptOutput : `${childError}\n\n${receiptOutput}`
4145
4164
  : childError || receiptOutput || output || "Child run failed.";
4165
+ const savedOutputEvidence = [...new Set(result.details.results.map((child) => child.savedOutputPath).filter((value): value is string => Boolean(value)))]
4166
+ .filter((savedOutputPath) => !failureErrorBase.includes(savedOutputPath))
4167
+ .map((savedOutputPath) => `Saved output: ${savedOutputPath}`);
4168
+ const failureError = [failureErrorBase, ...savedOutputEvidence].join("\n");
4146
4169
  const detached = result.details.results.some((child) => child.detached);
4147
4170
  const interrupted = result.details.results.some((child) => child.interrupted);
4148
4171
  const stopped = result.details.results.some((child) => child.stopped);
@@ -4514,12 +4537,13 @@ export function prepareWorkflowLaunchParams(
4514
4537
  };
4515
4538
  }
4516
4539
  const control = mergeWorkflowControlOverrides(workflowDefaults.control, childParams.control as ControlConfig | undefined);
4540
+ const asyncOmitted = childParams.async === undefined && workflowDefaults.async === undefined;
4517
4541
  const launchParams = {
4518
4542
  ...workflowDefaults,
4519
- async: options.externalAsyncRequired === true && childParams.async === undefined && workflowDefaults.async === undefined ? true : false,
4543
+ ...(options.externalAsyncRequired === true && asyncOmitted ? { async: true } : {}),
4520
4544
  ...childParams,
4521
4545
  ...(control !== undefined ? { control } : {}),
4522
- ...(options.externalAsyncRequired === true && childParams.async === undefined && workflowDefaults.async === undefined ? { workflowAwaitAsync: true } : {}),
4546
+ ...(asyncOmitted ? { workflowAwaitAsync: true } : {}),
4523
4547
  ...(options.missionDetached ? { mission: false } : {}),
4524
4548
  workflowParentRunId: parentWorkflowRunId,
4525
4549
  workflowKey,
@@ -4815,8 +4839,9 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4815
4839
  workflowResource = { permit: workflowResourcePermit, ...consumed };
4816
4840
  }
4817
4841
  if (requestParams.workflowScript !== undefined && normalizedAction === undefined) {
4842
+ const foregroundWorkflowRunId = encodeIndexSegment(_id);
4818
4843
  if (delegatedWorkflowPermit) {
4819
- const permitError = validateWorkflowChildPermitRoot(delegatedWorkflowPermit, _id);
4844
+ const permitError = validateWorkflowChildPermitRoot(delegatedWorkflowPermit, foregroundWorkflowRunId);
4820
4845
  if (permitError) return buildRequestedModeError(requestParams, permitError);
4821
4846
  if (requestParams.async !== false) return buildRequestedModeError(requestParams, "Workflow child permit supports foreground workflow roots only; set async:false.");
4822
4847
  }
@@ -5359,7 +5384,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5359
5384
  if (child.runId) workflowChildRunIds.set(key, child.runId);
5360
5385
  const step = status.steps?.find((candidate) => candidate.workflowKey === key);
5361
5386
  if (step) {
5362
- step.async = Boolean(result.details.asyncId || result.details.asyncDir);
5387
+ step.async = step.async === true || Boolean(result.details.asyncId || result.details.asyncDir);
5363
5388
  if (child.runId) step.runId = child.runId;
5364
5389
  if (child.lane) step.lane = child.lane;
5365
5390
  }
@@ -5468,7 +5493,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5468
5493
  const { workflowScript: _workflowScript, action: _action, agent: _agent, task: _task, resume: _resume, tasks: _tasks, chain: _chain, concurrency: _concurrency, async: _async, foregroundOnly: _foregroundOnly, clarify: _clarify, timeoutMs: _timeoutMs, maxRuntimeMs: _maxRuntimeMs, usageBudget: _usageBudget, chatProgress: _chatProgress, missionId: _missionId, mission: _mission, preflight: _preflight, globalConcurrencyLimit: _globalConcurrencyLimit, maxSubagentSpawnsPerRun: _maxSubagentSpawnsPerRun, ...workflowChildDefaults } = requestParams;
5469
5494
  const workflowOutput = typeof workflowChildDefaults.output === "string" || typeof workflowChildDefaults.output === "boolean" ? workflowChildDefaults.output : undefined;
5470
5495
  const configuredOutputBaseDir = resolveConfiguredSingleRunOutputBaseDir(deps);
5471
- const workflowAggregateOutputPath = resolveWorkflowAggregateOutputPath(workflowOutput, ctx.cwd, workflowCwd, resolveSingleRunOutputBaseDir(deps, workflowArtifactsDir, _id));
5496
+ const workflowAggregateOutputPath = resolveWorkflowAggregateOutputPath(workflowOutput, ctx.cwd, workflowCwd, resolveSingleRunOutputBaseDir(deps, workflowArtifactsDir, foregroundWorkflowRunId));
5472
5497
  const claimedOutputPaths = new Map<string, string>();
5473
5498
  const childOutputOverrides = new Map<string, string>();
5474
5499
  const childOutputClaimPaths = new Map<string, string>();
@@ -5478,7 +5503,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5478
5503
  const runHostCommand = workflowHostCommandRunner({
5479
5504
  workflowCwd,
5480
5505
  artifactsDir: workflowArtifactsDir,
5481
- workflowRunId: _id,
5506
+ workflowRunId: foregroundWorkflowRunId,
5482
5507
  claimedOutputPaths,
5483
5508
  producedOutputPaths: producedChildOutputPaths,
5484
5509
  ...(workflowResource ? { authorize: (key, params) => authorizeWorkflowResourceHost(workflowResource!.permit, key, params.command) } : {}),
@@ -5490,17 +5515,17 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5490
5515
  : publicExecution ? undefined : runHostCommand;
5491
5516
  const workflowHostSteps = new Map<string, HostStepNodeV1>();
5492
5517
  let liveWorkflow: NonNullable<Details["workflow"]> = { trace: [], emits: [], console: [], ...(workflowResource ? { resource: workflowResource.provenance } : {}) };
5493
- let liveWorkflowChildren = workflowChildSummary({ parentToolCallId: _id, workflowRunId: _id, workflowState: "running", inventoryComplete: false });
5518
+ let liveWorkflowChildren = workflowChildSummary({ parentToolCallId: _id, workflowRunId: foregroundWorkflowRunId, workflowState: "running", inventoryComplete: false });
5494
5519
  const workflowDeadlineAt = timeout === undefined ? undefined : Date.now() + timeout;
5495
5520
  const workflowCapabilityCeiling = intersectSubagentCapabilityCeilings(requestParams.capabilityCeiling, resolveCurrentSubagentCapabilityCeiling(resolveCurrentSessionId(ctx.sessionManager)));
5496
5521
  const sendWorkflowProgress = () => {
5497
- const update = workflowChatProgressUpdate(_id, chatProgress, liveWorkflow, liveWorkflowChildren, workflowPreflight);
5522
+ const update = workflowChatProgressUpdate(foregroundWorkflowRunId, chatProgress, liveWorkflow, liveWorkflowChildren, workflowPreflight);
5498
5523
  if (update) onUpdate?.(update);
5499
5524
  };
5500
5525
  try {
5501
5526
  const workflow = await runWorkflowScript({
5502
5527
  script: requestParams.workflowScript,
5503
- ...(delegatedWorkflowPermit ? { oneUsePermit: { claim: (key: string) => claimWorkflowChildPermit(delegatedWorkflowPermit, _id, key) } } : {}),
5528
+ ...(delegatedWorkflowPermit ? { oneUsePermit: { claim: (key: string) => claimWorkflowChildPermit(delegatedWorkflowPermit, foregroundWorkflowRunId, key) } } : {}),
5504
5529
  globalConcurrencyLimit: requestParams.globalConcurrencyLimit ?? deps.config.globalConcurrencyLimit,
5505
5530
  timeoutMs: timeout,
5506
5531
  signal,
@@ -5513,11 +5538,11 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5513
5538
  const projectedTrace = annotateWorkflowPreflightTrace(trace, workflowPreflight);
5514
5539
  const preflightWarnings = workflowPreflightWarnings(workflowPreflight, trace);
5515
5540
  liveWorkflow = { ...liveWorkflow, trace: projectedTrace, ...(preflightWarnings.length ? { preflightWarnings } : {}) };
5516
- liveWorkflowChildren = workflowChildSummary({ parentToolCallId: _id, workflowRunId: _id, workflowState: "running", inventoryComplete: false, trace });
5541
+ liveWorkflowChildren = workflowChildSummary({ parentToolCallId: _id, workflowRunId: foregroundWorkflowRunId, workflowState: "running", inventoryComplete: false, trace });
5517
5542
  sendWorkflowProgress();
5518
5543
  },
5519
5544
  admit: (calls) => {
5520
- const outputClaims = workflowChildOutputClaims({ ctxCwd: ctx.cwd, workflowCwd, artifactsDir: workflowArtifactsDir, workflowRunId: _id, aggregateOutputPath: workflowAggregateOutputPath, configuredOutputBaseDir, discoverAgents: discoverWorkflowAgents, agents: workflowAgents, workflowAgentScope: workflowChildDefaults.agentScope, state: deps.state, claimedOutputPaths, entries: calls });
5545
+ const outputClaims = workflowChildOutputClaims({ ctxCwd: ctx.cwd, workflowCwd, artifactsDir: workflowArtifactsDir, workflowRunId: foregroundWorkflowRunId, aggregateOutputPath: workflowAggregateOutputPath, configuredOutputBaseDir, discoverAgents: discoverWorkflowAgents, agents: workflowAgents, workflowAgentScope: workflowChildDefaults.agentScope, state: deps.state, claimedOutputPaths, entries: calls });
5521
5546
  if (outputClaims.error) throw new Error(outputClaims.error);
5522
5547
  claimRunFanoutBatch(workflowFanoutBudget, calls.map(({ key }) => `workflow[${key}]`));
5523
5548
  if (outputClaims.claims) applyWorkflowChildOutputClaims(claimedOutputPaths, outputClaims.claims);
@@ -5537,7 +5562,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5537
5562
  if (budgetState?.exhausted) return workflowChildResult(key, buildRequestedModeError(childParams as SubagentParamsLike, usageBudgetExceededMessage(budgetState)), childParams, deps.state, { state: "partial", reason: "budget_exhausted" });
5538
5563
  const childPhase = typeof childParams.phase === "string" && childParams.phase.trim() ? childParams.phase.trim() : undefined;
5539
5564
  const childLabel = typeof childParams.label === "string" && childParams.label.trim() ? childParams.label.trim() : undefined;
5540
- recordMissionWorkflowChild(missionBinding, _id, key, {
5565
+ recordMissionWorkflowChild(missionBinding, foregroundWorkflowRunId, key, {
5541
5566
  status: "running",
5542
5567
  ...(typeof childParams.agent === "string" && childParams.agent.trim() ? { agent: childParams.agent.trim() } : {}),
5543
5568
  ...(childLabel ? { label: childLabel } : {}),
@@ -5545,9 +5570,9 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5545
5570
  heartbeat: { status: "running", ...(childPhase ? { phase: childPhase } : {}) },
5546
5571
  });
5547
5572
  let preparedChildParams: SubagentParamsLike | undefined;
5548
- const result = await runMissionWorkflowChild(missionBinding, _id, key, childPhase, () => {
5573
+ const result = await runMissionWorkflowChild(missionBinding, foregroundWorkflowRunId, key, childPhase, () => {
5549
5574
  const childRequest = bindMissionWorkflowChildAsyncLaunch(
5550
- { ...prepareWorkflowChildLaunchParams({ workflowDefaults: workflowChildDefaults, childParams, parentWorkflowRunId: _id, workflowKey: key, ctxCwd: ctx.cwd, workflowCwd, artifactsDir: workflowArtifactsDir, aggregateOutputPath: workflowAggregateOutputPath, configuredOutputBaseDir, discoverAgents: discoverWorkflowAgents, agents: workflowAgents, workflowAgentScope: workflowChildDefaults.agentScope, outputOverride: childOutputOverrides.get(key), outputClaimPath: childOutputClaimPaths.get(key), options: { missionDetached: detachWorkflowChildMissions, suppressRoutineResultIntercom: chatProgress.mode === "live-card", runFanoutBudget: workflowFanoutBudget, parentDeadlineAt: workflowDeadlineAt, capabilityCeiling: workflowCapabilityCeiling } }), runFanoutAdmitted: admission.admitted },
5575
+ { ...prepareWorkflowChildLaunchParams({ workflowDefaults: workflowChildDefaults, childParams: delegatedWorkflowPermit ? { ...childParams, async: false } : childParams, parentWorkflowRunId: foregroundWorkflowRunId, workflowKey: key, ctxCwd: ctx.cwd, workflowCwd, artifactsDir: workflowArtifactsDir, aggregateOutputPath: workflowAggregateOutputPath, configuredOutputBaseDir, discoverAgents: discoverWorkflowAgents, agents: workflowAgents, workflowAgentScope: workflowChildDefaults.agentScope, outputOverride: childOutputOverrides.get(key), outputClaimPath: childOutputClaimPaths.get(key), options: { missionDetached: detachWorkflowChildMissions, suppressRoutineResultIntercom: chatProgress.mode === "live-card", runFanoutBudget: workflowFanoutBudget, parentDeadlineAt: workflowDeadlineAt, capabilityCeiling: workflowCapabilityCeiling } }), runFanoutAdmitted: admission.admitted },
5551
5576
  missionBinding,
5552
5577
  deps.asyncByDefault,
5553
5578
  );
@@ -5559,9 +5584,9 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5559
5584
  ? resolveAgentName(childRequest.agent, discoverWorkflowAgents(childCwd, resolveExecutionAgentScope(childRequest.agentScope)).agents).agent
5560
5585
  : undefined;
5561
5586
  if (childAgent?.runner?.type === "external-cli" || childAgent?.runner?.type === "external-job") throw new Error("Workflow child permit supports native Pi children only.");
5562
- workflowPermitContexts.set(childRequest, { child: { permit: delegatedWorkflowPermit, workflowRunId: _id, childKey: key } });
5587
+ workflowPermitContexts.set(childRequest, { child: { permit: delegatedWorkflowPermit, workflowRunId: foregroundWorkflowRunId, childKey: key } });
5563
5588
  }
5564
- workflowLaunchObservers.set(childRequest, (launch) => recordMissionWorkflowChild(missionBinding, _id, key, {
5589
+ workflowLaunchObservers.set(childRequest, (launch) => recordMissionWorkflowChild(missionBinding, foregroundWorkflowRunId, key, {
5565
5590
  status: "running",
5566
5591
  agent: launch.agent,
5567
5592
  ...(launch.sessionFile ? { sessionPath: launch.sessionFile } : {}),
@@ -5570,7 +5595,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5570
5595
  const progress = update.details.progress?.[0];
5571
5596
  if (!progress) return;
5572
5597
  const progressStatus = progress.status === "completed" ? "completed" : progress.status === "failed" ? "failed" : "running";
5573
- recordMissionWorkflowChild(missionBinding, _id, key, {
5598
+ recordMissionWorkflowChild(missionBinding, foregroundWorkflowRunId, key, {
5574
5599
  status: progressStatus,
5575
5600
  heartbeat: { status: progressStatus, ...(childPhase ? { phase: childPhase } : {}) },
5576
5601
  });
@@ -5584,7 +5609,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5584
5609
  const child = workflowChildResult(key, result, preparedChildParams ?? childParams, deps.state);
5585
5610
  if (child.runId) workflowChildRunIds.set(key, child.runId);
5586
5611
  const childStatus = missionWorkflowChildStatus(result);
5587
- recordMissionWorkflowChild(missionBinding, _id, key, {
5612
+ recordMissionWorkflowChild(missionBinding, foregroundWorkflowRunId, key, {
5588
5613
  status: childStatus,
5589
5614
  ...(child.runId ? { runId: child.runId } : {}),
5590
5615
  ...(result.details.results[0]?.agent ? { agent: result.details.results[0].agent } : {}),
@@ -5597,12 +5622,12 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5597
5622
  },
5598
5623
  status: async (keyOrRunId, workflowSignal) => workflowChildResult(keyOrRunId, await execute(randomUUID(), { action: "status", id: keyOrRunId }, workflowSignal, undefined, ctx, preserveActiveSession, workflowParentModel)),
5599
5624
  resolveResume: (reference) => resolveKeyedWorkflowResume(reference, deps.state),
5600
- steer: (key, message, options, workflowSignal) => steerWorkflowChildByKey({ state: deps.state, workflowRunId: _id, key, message, options, signal: workflowSignal, resolveRunId: () => workflowChildRunIds.get(key) }),
5625
+ steer: (key, message, options, workflowSignal) => steerWorkflowChildByKey({ state: deps.state, workflowRunId: foregroundWorkflowRunId, key, message, options, signal: workflowSignal, resolveRunId: () => workflowChildRunIds.get(key) }),
5601
5626
  });
5602
5627
  const finalPreflightWarnings = workflowPreflightWarnings(workflowPreflight, workflow.trace, { settled: true });
5603
5628
  const finalPreflightTrace = annotateWorkflowPreflightTrace(workflow.trace, workflowPreflight);
5604
- const workflowChildren = workflowChildSummary({ parentToolCallId: _id, workflowRunId: _id, workflowState: "completed", inventoryComplete: true, trace: workflow.trace, children: workflow.children });
5605
- const receipt = terminalWorkflowReceipt(_id, "complete", workflow.children, workflowChildren, undefined, [...workflowHostSteps.values()], workflowResource?.provenance);
5629
+ const workflowChildren = workflowChildSummary({ parentToolCallId: _id, workflowRunId: foregroundWorkflowRunId, workflowState: "completed", inventoryComplete: true, trace: workflow.trace, children: workflow.children });
5630
+ const receipt = terminalWorkflowReceipt(foregroundWorkflowRunId, "complete", workflow.children, workflowChildren, undefined, [...workflowHostSteps.values()], workflowResource?.provenance);
5606
5631
  const traceLines = finalPreflightTrace.map((entry) => `- ${entry.operation} ${entry.key}: ${entry.state}${entry.runId ? ` (${entry.runId})` : ""}${entry.durationMs !== undefined ? ` in ${entry.durationMs}ms` : ""}${entry.warning ? ` — ${entry.warning}` : ""}${entry.error ? ` — ${entry.error}` : ""}`);
5607
5632
  const sections = [
5608
5633
  ...(workflowPreflight ? [formatWorkflowPreflight(workflowPreflight)] : []),
@@ -5620,11 +5645,11 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5620
5645
  const displayText = appendWorkflowOutputWarning(workflowText, outputWarning);
5621
5646
  return attachWorkflowMission(withRunFanoutBudget({
5622
5647
  content: [{ type: "text", text: displayText }],
5623
- details: compactOptional<Details>({ mode: "workflow", runId: _id, results: workflowDetailsResults(workflow.children), ...(workflowPreflight ? { preflight: workflowPreflight } : {}), workflowChildren, totalChildUsage: sumResultsUsage(workflowResults), totalCost: sumResultsCost(workflowResults), usageBudget: usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults)), workflow: { value: workflow.value, trace: finalPreflightTrace, emits: workflow.emits, console: workflow.console, ...(workflowResource ? { resource: workflowResource.provenance } : {}), ...(finalPreflightWarnings.length ? { preflightWarnings: finalPreflightWarnings } : {}), receipt }, chatProgress }),
5648
+ details: compactOptional<Details>({ mode: "workflow", runId: foregroundWorkflowRunId, results: workflowDetailsResults(workflow.children), ...(workflowPreflight ? { preflight: workflowPreflight } : {}), workflowChildren, totalChildUsage: sumResultsUsage(workflowResults), totalCost: sumResultsCost(workflowResults), usageBudget: usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults)), workflow: { value: workflow.value, trace: finalPreflightTrace, emits: workflow.emits, console: workflow.console, ...(workflowResource ? { resource: workflowResource.provenance } : {}), ...(finalPreflightWarnings.length ? { preflightWarnings: finalPreflightWarnings } : {}), receipt }, chatProgress }),
5624
5649
  }, workflowFanoutBudget));
5625
5650
  } catch (error) {
5626
5651
  const partial = error instanceof WorkflowScriptError ? error.partial : { trace: [], emits: [], console: [], children: [] };
5627
- const text = workflowFailureMessage(error, _id, partial.children);
5652
+ const text = workflowFailureMessage(error, foregroundWorkflowRunId, partial.children);
5628
5653
  const finalPreflightWarnings = workflowPreflightWarnings(workflowPreflight, partial.trace, { settled: true });
5629
5654
  const finalPreflightTrace = annotateWorkflowPreflightTrace(partial.trace, workflowPreflight);
5630
5655
  const traceLines = finalPreflightTrace.map((entry) => `- ${entry.operation} ${entry.key}: ${entry.state}${entry.runId ? ` (${entry.runId})` : ""}${entry.warning ? ` — ${entry.warning}` : ""}${entry.error ? ` — ${entry.error}` : ""}`);
@@ -5641,13 +5666,13 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5641
5666
  const workflowText = sections.join("\n\n");
5642
5667
  const outputWarning = writeWorkflowAggregateOutput(workflowAggregateOutputPath, workflowText, producedChildOutputPaths);
5643
5668
  const displayText = appendWorkflowOutputWarning(workflowText, outputWarning);
5644
- const workflowChildren = workflowChildSummary({ parentToolCallId: _id, workflowRunId: _id, workflowState: "failed", inventoryComplete: true, trace: partial.trace, children: partial.children });
5669
+ const workflowChildren = workflowChildSummary({ parentToolCallId: _id, workflowRunId: foregroundWorkflowRunId, workflowState: "failed", inventoryComplete: true, trace: partial.trace, children: partial.children });
5645
5670
  const terminalOutcome = workflowFailureTerminalOutcome(error, partial.children, usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults)));
5646
- const receipt = terminalWorkflowReceipt(_id, "failed", partial.children, workflowChildren, terminalOutcome, [...workflowHostSteps.values()], workflowResource?.provenance);
5671
+ const receipt = terminalWorkflowReceipt(foregroundWorkflowRunId, "failed", partial.children, workflowChildren, terminalOutcome, [...workflowHostSteps.values()], workflowResource?.provenance);
5647
5672
  return attachWorkflowMission(withRunFanoutBudget({
5648
5673
  content: [{ type: "text", text: displayText }],
5649
5674
  isError: true,
5650
- details: compactOptional<Details>({ mode: "workflow", runId: _id, results: workflowDetailsResults(partial.children), ...(workflowPreflight ? { preflight: workflowPreflight } : {}), workflowChildren, totalChildUsage: sumResultsUsage(workflowResults), totalCost: sumResultsCost(workflowResults), usageBudget: usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults)), workflow: { trace: finalPreflightTrace, emits: partial.emits, console: partial.console, ...(workflowResource ? { resource: workflowResource.provenance } : {}), ...(finalPreflightWarnings.length ? { preflightWarnings: finalPreflightWarnings } : {}), receipt }, chatProgress }),
5675
+ details: compactOptional<Details>({ mode: "workflow", runId: foregroundWorkflowRunId, results: workflowDetailsResults(partial.children), ...(workflowPreflight ? { preflight: workflowPreflight } : {}), workflowChildren, totalChildUsage: sumResultsUsage(workflowResults), totalCost: sumResultsCost(workflowResults), usageBudget: usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults)), workflow: { trace: finalPreflightTrace, emits: partial.emits, console: partial.console, ...(workflowResource ? { resource: workflowResource.provenance } : {}), ...(finalPreflightWarnings.length ? { preflightWarnings: finalPreflightWarnings } : {}), receipt }, chatProgress }),
5651
5676
  }, workflowFanoutBudget));
5652
5677
  }
5653
5678
  }
@@ -6551,7 +6576,9 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
6551
6576
 
6552
6577
  let sessionRoot: string;
6553
6578
  if (effectiveParams.sessionDir) {
6554
- sessionRoot = path.resolve(deps.expandTilde(effectiveParams.sessionDir));
6579
+ // An explicit sessionDir is a root keyed by this launch's run id so
6580
+ // concurrent children resolve distinct per-child session files.
6581
+ sessionRoot = path.join(path.resolve(deps.expandTilde(effectiveParams.sessionDir)), runId);
6555
6582
  } else {
6556
6583
  const baseSessionRoot = deps.config.defaultSessionDir
6557
6584
  ? path.resolve(deps.expandTilde(deps.config.defaultSessionDir))
@@ -6902,7 +6929,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
6902
6929
  if (activeAsyncCapacity && !activeAsyncCapacity.owner.runnerStartedAt) activeAsyncCapacity.rollback();
6903
6930
  if (foregroundControl) {
6904
6931
  settleForegroundSchedulingOwner(foregroundControl);
6905
- removeForegroundControlIfIdle(deps.state, runId);
6932
+ removeForegroundControlIfIdle(deps.state, runId, deps.trackRetainedNestedRoute);
6906
6933
  }
6907
6934
  }
6908
6935
 
@@ -202,10 +202,23 @@ export function normalizeGateAcceptance(gate: unknown, acceptance: AcceptanceInp
202
202
  return normalized.error ? { ok: false, error: normalized.error } : { ok: true, acceptance: normalized.value as AcceptanceInput };
203
203
  }
204
204
  if (typeof gate !== "string" || !gate.trim()) return { ok: false, error: "gate must be a non-empty command string." };
205
- if (acceptance !== undefined) return { ok: false, error: "gate cannot be combined with acceptance; use one gate command or acceptance.verify." };
205
+ if (acceptance !== undefined && acceptance !== false) return { ok: false, error: "gate cannot be combined with acceptance; use one gate command or acceptance.verify." + describeGateAcceptanceConflict(gate, acceptance) };
206
206
  return { ok: true, acceptance: { level: "verified", verify: [{ id: "gate", command: gate.trim() }] } };
207
207
  }
208
208
 
209
+ export function describeGateAcceptanceConflict(gate: unknown, acceptance: unknown): string {
210
+ const render = (value: unknown): string => {
211
+ let encoded: string;
212
+ try {
213
+ encoded = JSON.stringify(value) ?? String(value);
214
+ } catch {
215
+ encoded = String(value);
216
+ }
217
+ return encoded.length > 120 ? `${encoded.slice(0, 120)}...` : encoded;
218
+ };
219
+ return ` Both fields were present: gate=${render(gate)} acceptance=${render(acceptance)}.`;
220
+ }
221
+
209
222
  function explicitAcceptanceCanDisable(explicit: AcceptanceConfig): boolean {
210
223
  return explicit.level === "none" && typeof explicit.reason === "string" && explicit.reason.trim().length > 0;
211
224
  }
@@ -115,17 +115,6 @@ export interface DefaultChildSessionFactoryOptions {
115
115
  }
116
116
 
117
117
  type ModelRuntimeInstance = Awaited<ReturnType<PiCodingAgentModule["ModelRuntime"]["create"]>>;
118
- type QueuedProviderRegistration = { name: string; config: Parameters<ModelRuntimeInstance["registerProvider"]>[1]; extensionPath: string };
119
- type QueuedNativeProviderRegistration = { provider: Parameters<ModelRuntimeInstance["registerNativeProvider"]>[0]; extensionPath: string };
120
-
121
- interface LoaderWithExtensions {
122
- getExtensions(): {
123
- runtime: {
124
- pendingProviderRegistrations: QueuedProviderRegistration[];
125
- pendingNativeProviderRegistrations: QueuedNativeProviderRegistration[];
126
- };
127
- };
128
- }
129
118
 
130
119
  /** One launch at a time from env application through `session_start`, so parallel launches never observe each other's `processEnv` while their extensions load and start. */
131
120
  let loading: Promise<unknown> = Promise.resolve();
@@ -151,25 +140,29 @@ function applyProcessEnv(values: Record<string, string | undefined> | undefined)
151
140
  }
152
141
  }
153
142
 
154
- function flushQueuedProviderRegistrations(loader: object, modelRuntime: ModelRuntimeInstance, onError: ((error: ChildSessionExtensionError) => void) | undefined): void {
143
+ async function flushQueuedProviderRegistrations(loader: InstanceType<PiCodingAgentModule["DefaultResourceLoader"]>, modelRuntime: ModelRuntimeInstance, onError: ((error: ChildSessionExtensionError) => void) | undefined): Promise<void> {
155
144
  if (!("getExtensions" in loader) || typeof loader.getExtensions !== "function") return;
156
- const { runtime } = (loader as LoaderWithExtensions).getExtensions();
157
- for (const { name, config, extensionPath } of runtime.pendingProviderRegistrations) {
145
+ const { runtime } = loader.getExtensions();
146
+ let registered = false;
147
+ for (const { name, config, extensionPath } of runtime.pendingProviderRegistrations ?? []) {
158
148
  try {
159
149
  modelRuntime.registerProvider(name, config);
150
+ registered = true;
160
151
  } catch (error) {
161
152
  onError?.({ extensionPath, event: "register_provider", error });
162
153
  }
163
154
  }
164
- runtime.pendingProviderRegistrations = [];
165
- for (const { provider, extensionPath } of runtime.pendingNativeProviderRegistrations) {
155
+ if (Array.isArray(runtime.pendingProviderRegistrations)) runtime.pendingProviderRegistrations = [];
156
+ for (const { provider, extensionPath } of runtime.pendingNativeProviderRegistrations ?? []) {
166
157
  try {
167
158
  modelRuntime.registerNativeProvider(provider);
159
+ registered = true;
168
160
  } catch (error) {
169
161
  onError?.({ extensionPath, event: "register_provider", error });
170
162
  }
171
163
  }
172
- runtime.pendingNativeProviderRegistrations = [];
164
+ if (Array.isArray(runtime.pendingNativeProviderRegistrations)) runtime.pendingNativeProviderRegistrations = [];
165
+ if (registered) await modelRuntime.refresh({ allowNetwork: false });
173
166
  }
174
167
 
175
168
  /**
@@ -196,6 +189,8 @@ export function createDefaultChildSessionFactory(options: DefaultChildSessionFac
196
189
  const modelRuntime = await sharedRuntime(pi);
197
190
  const agentDir = getAgentDir();
198
191
  const settingsManager = pi.SettingsManager.create(launch.cwd, agentDir);
192
+ // Headless sessions skip Pi's CLI theme setup; extensions still need ctx.ui.theme.
193
+ if (typeof pi.initTheme === "function") pi.initTheme(settingsManager.getTheme());
199
194
  const loader = new pi.DefaultResourceLoader({
200
195
  cwd: launch.cwd,
201
196
  agentDir,
@@ -214,7 +209,7 @@ export function createDefaultChildSessionFactory(options: DefaultChildSessionFac
214
209
  applyProcessEnv(launch.processEnv);
215
210
  if (!resetExtensionCacheOnReload(loader) && (launch.ambientExtensions || launch.extensionPaths.length)) launch.onExtensionError?.({ extensionPath: "<loader>", event: "load", error: new Error("pi's extension cache reset is unavailable; extensions loaded into this child share module state with other sessions in this process.") });
216
211
  await loader.reload();
217
- flushQueuedProviderRegistrations(loader, modelRuntime, launch.onExtensionError);
212
+ await flushQueuedProviderRegistrations(loader, modelRuntime, launch.onExtensionError);
218
213
  const sessionManager = launch.storage.kind === "file"
219
214
  ? pi.SessionManager.open(launch.storage.sessionFile, undefined, launch.cwd)
220
215
  : launch.storage.kind === "dir"
@@ -1,7 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
- import { Agent, type AgentTool, type StreamFn } from "@earendil-works/pi-agent-core";
3
- import { convertToLlm, type ExtensionContext } from "@earendil-works/pi-coding-agent";
4
- import { streamSimple } from "@earendil-works/pi-ai/compat";
2
+ import type { Agent, AgentTool, StreamFn } from "@earendil-works/pi-agent-core";
3
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
5
4
  import type { ProviderHeaders } from "@earendil-works/pi-ai";
6
5
  import { Type, type Static } from "typebox";
7
6
  import { agentStreamOptions } from "../../shared/agent-stream-options.ts";
@@ -52,7 +51,11 @@ export function mapArbiterDecision(
52
51
 
53
52
  interface ArbiterRuntime {
54
53
  model: NonNullable<RegistryModel>;
55
- baseStreamFn: StreamFn;
54
+ /** Explicit override; it always wins over a registered provider stream. */
55
+ explicitStreamFn?: StreamFn;
56
+ /** Registered provider stream, usable only when its api matches the model. */
57
+ registeredStreamFn?: StreamFn;
58
+ registeredApi?: string;
56
59
  timeoutMs: number;
57
60
  }
58
61
 
@@ -103,15 +106,12 @@ function resolveArbiterRuntime(
103
106
  const registry = ctx.modelRegistry as {
104
107
  getRegisteredProviderConfig?: (provider: string) => { api?: string; streamSimple?: StreamFn } | undefined;
105
108
  };
106
- const modelApi = (model as { api?: string }).api;
107
109
  const registered = registry.getRegisteredProviderConfig?.(model.provider);
108
- const baseStreamFn = options?.streamFn
109
- ?? (registered?.streamSimple && registered.api === modelApi
110
- ? registered.streamSimple
111
- : streamSimple);
112
110
  return {
113
111
  model,
114
- baseStreamFn,
112
+ explicitStreamFn: options?.streamFn,
113
+ registeredStreamFn: registered?.streamSimple,
114
+ registeredApi: registered?.api,
115
115
  timeoutMs: options?.timeoutMs ?? DEFAULT_ARBITER_TIMEOUT_MS,
116
116
  };
117
117
  }
@@ -164,6 +164,15 @@ async function runArbitration(
164
164
  auth: ArbiterAuth,
165
165
  task: string,
166
166
  ): Promise<TaskMutationVerdict> {
167
+ // Keep optional Pi peers out of the detached runner's static import graph.
168
+ const [{ Agent }, { convertToLlm }, { streamSimple }] = await Promise.all([
169
+ import("@earendil-works/pi-agent-core"),
170
+ import("@earendil-works/pi-coding-agent"),
171
+ import("@earendil-works/pi-ai/compat"),
172
+ ]);
173
+ const streamFn: StreamFn = runtime.explicitStreamFn
174
+ ?? (runtime.registeredApi !== undefined && runtime.registeredApi === runtime.model.api ? runtime.registeredStreamFn : undefined)
175
+ ?? streamSimple;
167
176
  let decision: DecisionParams | undefined;
168
177
  const tool: AgentTool<typeof DecisionParams, { recorded: boolean }> = {
169
178
  name: "task_mutation_decision",
@@ -190,7 +199,7 @@ async function runArbitration(
190
199
  tools: [tool],
191
200
  },
192
201
  convertToLlm,
193
- ...agentStreamOptions(authWrappedStreamFn(runtime.baseStreamFn, auth)),
202
+ ...agentStreamOptions(authWrappedStreamFn(streamFn, auth)),
194
203
  getApiKey: (providerName) =>
195
204
  providerName === runtime.model.provider ? auth.apiKey : undefined,
196
205
  beforeToolCall: async ({ toolCall }) =>
@@ -317,6 +317,7 @@ export function parseModelKey(fullId: string): { provider?: string; modelId: str
317
317
  export function filterFallbackCandidates(candidates: string[], opts?: {
318
318
  now?: number;
319
319
  onExcluded?: (candidate: string, exclusion: Readonly<ModelExclusion>) => void;
320
+ ignoreExclusion?: (candidate: string, exclusion: Readonly<ModelExclusion>) => boolean;
320
321
  }): string[] {
321
322
  ensureLoaded();
322
323
  invalidateAuthExclusions();
@@ -326,7 +327,7 @@ export function filterFallbackCandidates(candidates: string[], opts?: {
326
327
  for (const raw of candidates) {
327
328
  if (!raw || seen.has(raw)) continue;
328
329
  const { provider: candidateProvider, modelId: candidateModelId } = parseModelKey(raw);
329
- const exclusion = exclusions.find((entry) => entryMatches(entry, candidateModelId, candidateProvider, timestamp));
330
+ const exclusion = exclusions.find((entry) => entryMatches(entry, candidateModelId, candidateProvider, timestamp) && opts?.ignoreExclusion?.(raw, entry) !== true);
330
331
  if (exclusion) {
331
332
  opts?.onExcluded?.(raw, exclusion);
332
333
  continue;
@@ -18,9 +18,17 @@ export function splitThinkingSuffix(model: string): { baseModel: string; thinkin
18
18
  return splitKnownThinkingSuffix(model);
19
19
  }
20
20
 
21
- export function formatSubagentModelVerificationError(expectedModel: string, observedModel: string, availableModels: AvailableModelInfo[] | undefined): string | undefined {
21
+ /** Aliases apply only to the resolved launch candidate (without its thinking suffix) and the exact raw response ID. */
22
+ export function formatSubagentModelVerificationError(
23
+ expectedModel: string,
24
+ observedModel: string,
25
+ availableModels: AvailableModelInfo[] | undefined,
26
+ modelResponseAliases?: Record<string, string[]>,
27
+ ): string | undefined {
22
28
  if (!availableModels || availableModels.length === 0) return undefined;
23
29
  const expectedBase = splitThinkingSuffix(expectedModel).baseModel;
30
+ if (modelResponseAliases && Object.hasOwn(modelResponseAliases, expectedBase)
31
+ && modelResponseAliases[expectedBase]?.includes(observedModel)) return undefined;
24
32
  const observedBase = splitThinkingSuffix(observedModel).baseModel;
25
33
  if (expectedBase === observedBase) return undefined;
26
34
  const expectedEntry = availableModels.find((entry) => entry.fullId === expectedBase);
@@ -309,6 +317,24 @@ function formatExcludedCandidateEvidence(candidate: string, exclusion: NonNullab
309
317
  return `${displayCandidate} — model: ${displayModel}; provider: ${displayProvider}; reason: ${reason}; expires: ${formatModelExclusionExpiry(exclusion.expiresAt)}`;
310
318
  }
311
319
 
320
+ const MODEL_UNAVAILABLE_EXCLUSION_PATTERNS = [
321
+ /model.*not found/i,
322
+ /unknown model/i,
323
+ /model.*unavailable/i,
324
+ /model.*disabled/i,
325
+ ];
326
+
327
+ function isCurrentRegistryModel(candidate: string, availableModels: AvailableModelInfo[] | undefined): boolean {
328
+ if (!availableModels || availableModels.length === 0) return false;
329
+ const { baseModel } = splitThinkingSuffix(candidate);
330
+ return availableModels.some((entry) => entry.fullId === baseModel);
331
+ }
332
+
333
+ function ignoreStaleModelUnavailableExclusion(candidate: string, exclusion: NonNullable<ReturnType<typeof findModelExclusion>>, availableModels: AvailableModelInfo[] | undefined): boolean {
334
+ const reason = exclusion.reason ?? "";
335
+ return MODEL_UNAVAILABLE_EXCLUSION_PATTERNS.some((pattern) => pattern.test(reason)) && isCurrentRegistryModel(candidate, availableModels);
336
+ }
337
+
312
338
  function throwForExplicitModelExclusion(model: string): void {
313
339
  const exclusion = findModelExclusion(model);
314
340
  if (!exclusion) return;
@@ -487,7 +513,10 @@ export function buildModelCandidates(
487
513
  seen.add(normalized);
488
514
  candidates.push(normalized);
489
515
  }
490
- const resolved = filterFallbackCandidates(candidates, { onExcluded: warnCachedExclusion });
516
+ const resolved = filterFallbackCandidates(candidates, {
517
+ onExcluded: warnCachedExclusion,
518
+ ignoreExclusion: (candidate, exclusion) => ignoreStaleModelUnavailableExclusion(candidate, exclusion, availableModels),
519
+ });
491
520
  if (resolved.length === 0) {
492
521
  if (skippedPrimary) resolveRequiredSubagentModelCandidate(skippedPrimary, availableModels, preferredProvider);
493
522
  if (candidates.length === 0 && skippedFallback) resolveRequiredSubagentModelCandidate(skippedFallback, availableModels, preferredProvider);
@@ -286,7 +286,7 @@ function sanitizeTurnBudget(value: unknown): TurnBudgetState | undefined {
286
286
  }
287
287
 
288
288
  function sanitizeState(value: unknown, fallback: NestedRunState): NestedRunState {
289
- return value === "queued" || value === "running" || value === "complete" || value === "failed" || value === "partial" || value === "paused" || value === "stopped"
289
+ return value === "queued" || value === "running" || value === "complete" || value === "failed" || value === "partial" || value === "paused" || value === "stopped" || value === "rejected"
290
290
  ? value
291
291
  : fallback;
292
292
  }
@@ -296,7 +296,7 @@ function sanitizeStep(input: unknown, depth: number): NestedStepSummary | undefi
296
296
  const raw = input as Record<string, unknown>;
297
297
  const agent = stringValue(raw.agent, 128);
298
298
  if (!agent) return undefined;
299
- const status = raw.status === "pending" || raw.status === "running" || raw.status === "complete" || raw.status === "completed" || raw.status === "failed" || raw.status === "paused" || raw.status === "stopped"
299
+ const status = raw.status === "pending" || raw.status === "running" || raw.status === "complete" || raw.status === "completed" || raw.status === "failed" || raw.status === "partial" || raw.status === "paused" || raw.status === "stopped" || raw.status === "rejected"
300
300
  ? raw.status
301
301
  : "pending";
302
302
  const model = stringValue(raw.model);
@@ -438,7 +438,7 @@ export function parseNestedEventRecords(content: string, route: NestedRoute): Ne
438
438
  }
439
439
 
440
440
  function terminal(state: NestedRunState): boolean {
441
- return state === "complete" || state === "failed" || state === "partial" || state === "paused" || state === "stopped";
441
+ return state === "complete" || state === "failed" || state === "partial" || state === "paused" || state === "rejected" || state === "stopped";
442
442
  }
443
443
 
444
444
  function mergeBoundedChildren(existing: NestedRunSummary[] | undefined, incoming: NestedRunSummary[] | undefined): NestedRunSummary[] | undefined {
@@ -41,6 +41,7 @@ export interface RunnerSubagentStep {
41
41
  /** The primary model is inherited from the parent session and should not be verified against the child-reported active registry model. */
42
42
  skipPrimaryModelVerification?: boolean;
43
43
  modelVerificationRegistry?: Array<{ provider: string; id: string; fullId: string; contextWindow?: number }>;
44
+ modelResponseAliases?: Record<string, string[]>;
44
45
  tools?: string[];
45
46
  excludeTools?: string[];
46
47
  allowNestedSubagents?: boolean;