pi-subagents 0.65.0 → 0.66.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 (104) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/README.md +1 -1
  3. package/agents/researcher.md +23 -13
  4. package/docs/agents.md +17 -3
  5. package/docs/configuration.md +34 -0
  6. package/docs/extension-api.md +94 -0
  7. package/docs/models.md +58 -1
  8. package/docs/observability.md +42 -2
  9. package/docs/tool-reference.md +11 -5
  10. package/docs/workflows.md +22 -7
  11. package/package.json +4 -1
  12. package/runner-server-preload.mjs +13 -0
  13. package/skills/pi-subagents/SKILL.md +2 -1
  14. package/skills/pi-subagents/references/execution-controls.md +19 -2
  15. package/skills/pi-subagents/references/management-authoring-rpc.md +2 -1
  16. package/skills/pi-subagents/references/multi-lane-orchestration.md +2 -0
  17. package/src/agents/advertised-agent-prompt.ts +63 -0
  18. package/src/agents/agent-management.ts +14 -1
  19. package/src/agents/agent-serializer.ts +2 -0
  20. package/src/agents/agents.ts +8 -0
  21. package/src/api/preflight.ts +5 -1
  22. package/src/api/shared-types.ts +1 -1
  23. package/src/api/workflow-resources.ts +6 -0
  24. package/src/extension/config.ts +4 -2
  25. package/src/extension/index.ts +71 -4
  26. package/src/extension/public-execution.ts +0 -1
  27. package/src/extension/rpc.ts +4 -21
  28. package/src/extension/schemas.ts +9 -7
  29. package/src/extension/tool-description.ts +10 -5
  30. package/src/integrations/pi-web-session-liveness.ts +73 -0
  31. package/src/intercom/native-supervisor-channel.ts +102 -88
  32. package/src/intercom/supervisor-ui.ts +3 -2
  33. package/src/missions/workflow-state.ts +37 -16
  34. package/src/runs/background/active-async-capacity.ts +18 -18
  35. package/src/runs/background/async-execution.ts +8 -1
  36. package/src/runs/background/async-job-tracker.ts +35 -3
  37. package/src/runs/background/async-resume.ts +3 -1
  38. package/src/runs/background/async-retention.ts +9 -0
  39. package/src/runs/background/async-status-snapshot.ts +10 -12
  40. package/src/runs/background/async-status.ts +17 -9
  41. package/src/runs/background/auto-drain.ts +40 -29
  42. package/src/runs/background/chain-root-attachment.ts +8 -0
  43. package/src/runs/background/control-channel.ts +78 -44
  44. package/src/runs/background/notify.ts +88 -12
  45. package/src/runs/background/owned-process-tree.ts +6 -6
  46. package/src/runs/background/process-terminal.ts +23 -23
  47. package/src/runs/background/retained-nested-route-tracker.ts +96 -0
  48. package/src/runs/background/run-child-session.ts +62 -33
  49. package/src/runs/background/run-status.ts +75 -5
  50. package/src/runs/background/runner-aliases.ts +46 -8
  51. package/src/runs/background/runner-child-launch.ts +86 -0
  52. package/src/runs/background/stale-run-reconciler.ts +3 -1
  53. package/src/runs/background/subagent-runner.ts +430 -208
  54. package/src/runs/background/subagent-wait.ts +3 -0
  55. package/src/runs/background/wait-completions.ts +4 -0
  56. package/src/runs/foreground/async-steering-action.ts +19 -0
  57. package/src/runs/foreground/execution.ts +116 -26
  58. package/src/runs/foreground/foreground-history.ts +3 -1
  59. package/src/runs/foreground/prompt-audit.ts +9 -5
  60. package/src/runs/foreground/subagent-executor.ts +531 -227
  61. package/src/runs/foreground/workflow-detach-reconcile.ts +8 -5
  62. package/src/runs/foreground/workflow-foreground-steering.ts +56 -2
  63. package/src/runs/shared/acceptance.ts +16 -3
  64. package/src/runs/shared/agent-contract.ts +1 -1
  65. package/src/runs/shared/async-status-projection.ts +47 -47
  66. package/src/runs/shared/child-hooks.ts +151 -2
  67. package/src/runs/shared/child-launch.ts +18 -13
  68. package/src/runs/shared/child-session.ts +55 -24
  69. package/src/runs/shared/child-tool-plan.ts +2 -2
  70. package/src/runs/shared/completion-evidence.ts +2 -2
  71. package/src/runs/shared/completion-guard.ts +1 -0
  72. package/src/runs/shared/host-step-status.ts +11 -11
  73. package/src/runs/shared/llm-intent-arbiter.ts +30 -20
  74. package/src/runs/shared/model-exclusions.ts +2 -1
  75. package/src/runs/shared/model-fallback.ts +41 -8
  76. package/src/runs/shared/nested-events.ts +8 -8
  77. package/src/runs/shared/orca-progress-tabs.ts +6 -0
  78. package/src/runs/shared/parallel-handoff.ts +57 -12
  79. package/src/runs/shared/parallel-utils.ts +3 -2
  80. package/src/runs/shared/readonly-drain-observation.ts +42 -0
  81. package/src/runs/shared/readonly-model-continuation.ts +69 -0
  82. package/src/runs/shared/readonly-session-evidence.ts +307 -0
  83. package/src/runs/shared/run-fanout-budget.ts +8 -8
  84. package/src/runs/shared/runtime-acknowledged-extensions.ts +3 -3
  85. package/src/runs/shared/subagent-prompt-runtime.ts +13 -3
  86. package/src/runs/shared/worktree-cleanup-plan.ts +6 -3
  87. package/src/runs/shared/worktree-setup-command.ts +190 -0
  88. package/src/runs/shared/worktree.ts +403 -210
  89. package/src/shared/model-response-aliases.ts +13 -0
  90. package/src/shared/types.ts +89 -60
  91. package/src/shared/utils.ts +10 -2
  92. package/src/shared/watch-strategy.ts +2 -0
  93. package/src/shared/workflow-child-permit.ts +18 -13
  94. package/src/tui/fleet-status.ts +1 -1
  95. package/src/tui/fleet.ts +11 -5
  96. package/src/tui/render.ts +44 -15
  97. package/src/workflows/chat-progress.ts +3 -3
  98. package/src/workflows/scripted-workflow.ts +70 -16
  99. package/src/workflows/workflow-checklist.ts +15 -18
  100. package/src/workflows/workflow-child-summary.ts +57 -8
  101. package/src/workflows/workflow-preflight.ts +19 -19
  102. package/src/workflows/workflow-receipt.ts +3 -3
  103. package/src/workflows/workflow-resources.ts +96 -21
  104. package/src/workflows/workflow-settlement.ts +3 -0
@@ -337,6 +337,9 @@ function completionUsage(completions: WaitCompletion[] | undefined): Usage | und
337
337
 
338
338
  function result(text: string, isError = false, completions?: WaitCompletion[]): AgentToolResult<Details> {
339
339
  const usage = completionUsage(completions);
340
+ for (const completion of completions ?? []) {
341
+ if (completion.workflowReceiptPath) text += `\nWorkflow receipt [${completion.runId}]: ${completion.workflowReceiptPath}`;
342
+ }
340
343
  return {
341
344
  content: [{ type: "text", text }],
342
345
  ...(isError ? { isError: true } : {}),
@@ -96,6 +96,9 @@ export function toWaitCompletion(data: Record<string, unknown>, runId: string):
96
96
  })
97
97
  : undefined;
98
98
  const agent = asNonEmptyString(data.agent);
99
+ const receipt = data.workflowReceipt;
100
+ const workflowReceiptPath = receipt && typeof receipt === "object" && !Array.isArray(receipt)
101
+ ? asNonEmptyString((receipt as Record<string, unknown>).path) : undefined;
99
102
  const mode = asNonEmptyString(data.mode);
100
103
  const state = asNonEmptyString(data.state);
101
104
  const workflowChildren = parseWorkflowChildSummary(data.workflowChildren);
@@ -104,6 +107,7 @@ export function toWaitCompletion(data: Record<string, unknown>, runId: string):
104
107
  runId,
105
108
  ...(agent ? { agent } : {}),
106
109
  ...(mode ? { mode } : {}),
110
+ ...(workflowReceiptPath ? { workflowReceiptPath } : {}),
107
111
  ...(state ? { state } : {}),
108
112
  ...(typeof data.success === "boolean" ? { success: data.success } : {}),
109
113
  ...(results && results.length > 0 ? { results } : {}),
@@ -26,6 +26,7 @@ export async function steerAsyncRun(input: {
26
26
  message: string;
27
27
  mode?: SteerDeliveryMode;
28
28
  index?: number;
29
+ findPendingAsks?: (target: { runId: string; agent: string; childIndex: number }) => string[];
29
30
  kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean;
30
31
  location: { asyncDir: string | null };
31
32
  signal?: AbortSignal;
@@ -44,6 +45,24 @@ export async function steerAsyncRun(input: {
44
45
  };
45
46
  }
46
47
  const asyncDir = input.location.asyncDir;
48
+ const snapshot = input.findPendingAsks ? readStatus(asyncDir) : null;
49
+ if (snapshot?.mode === "single"
50
+ && (!input.state.currentSessionId || snapshot.sessionId === input.state.currentSessionId)
51
+ && (snapshot.state === "running" || snapshot.state === "queued")
52
+ && snapshot.steps?.length === 1 && (input.index === undefined || input.index === 0)
53
+ && (snapshot.steps[0]?.status === "running" || snapshot.steps[0]?.status === "pending")) {
54
+ const asks = input.findPendingAsks?.({ runId: snapshot.runId, agent: snapshot.steps[0].agent, childIndex: 0 }) ?? [];
55
+ if (asks.length > 0) {
56
+ const replies = asks.map(replyTo => `subagent_supervisor(${JSON.stringify({ action: "reply", replyTo, message: "<explicit answer>" })})`).join("\n");
57
+ return {
58
+ content: [{ type: "text", text: `Steering not delivered or queued: async run ${snapshot.runId} is blocked on ${asks.length === 1 ? "a pending supervisor ask" : "ambiguous pending supervisor asks"}. No reply or recovery was attempted. Reply explicitly${asks.length > 1 ? " to the intended request ID" : ""}:\n${replies}` }],
59
+ isError: true,
60
+ details: { mode: "management", results: [] },
61
+ };
62
+ }
63
+ }
64
+ // The read-only snapshot precedes even stale-run reconciliation. An ask arriving
65
+ // later does not make a runner acknowledgement proof of consumption or unblocking.
47
66
  const status = reconcileAsyncRun(asyncDir, { kill: input.kill }).status;
48
67
  if (input.state.currentSessionId && status?.sessionId !== input.state.currentSessionId) {
49
68
  return {
@@ -58,6 +58,9 @@ import { effectiveToolTimeoutMs, formatToolTimeoutMessage, resolveToolTimeoutMs,
58
58
  import { evaluateCompletionMutationGuard, expectsImplementationMutation, hasMutationToolCapability, validateImplementationToolContract } from "../shared/completion-guard.ts";
59
59
  import { planCompletionEvidence } from "../shared/completion-evidence.ts";
60
60
  import { planAbortRecovery } from "../shared/abort-recovery.ts";
61
+ import { planReadonlyModelContinuation, type LogicalRecoveryState } from "../shared/readonly-model-continuation.ts";
62
+ import { getReadonlySessionEvidence, requestReadonlySessionEvidence, type SettledReadonlyEvidence } from "../shared/readonly-session-evidence.ts";
63
+ import { getReadonlyChildModels } from "../shared/child-session.ts";
61
64
  import { arbitrateCompletionGuardRescue } from "../shared/llm-intent-arbiter.ts";
62
65
  import { preflightLaunchCwd } from "../shared/launch-cwd.ts";
63
66
  import { createJsonlWriter } from "../../shared/jsonl-writer.ts";
@@ -94,7 +97,7 @@ import {
94
97
  } from "../shared/long-running-guard.ts";
95
98
  import { acceptanceFailureMessage, buildSkippedAcceptanceLedger, evaluateAcceptance, formatAcceptancePrompt, resolveEffectiveAcceptance, stripAcceptanceReport, validateAcceptanceInput } from "../shared/acceptance.ts";
96
99
  import { PROMPT_REDACTED } from "../../shared/utils.ts";
97
- import { attachContractProjections, isAgentContractV1 } from "../shared/agent-contract.ts";
100
+ import { attachContractProjections, isAgentContract } from "../shared/agent-contract.ts";
98
101
  import { initialToolBudgetState, toolBudgetState } from "../shared/tool-budget.ts";
99
102
  import { resolveWatchdogConfig } from "../../watchdog/settings.ts";
100
103
  import { agentDefinitionDigest, launchBindingDigest } from "../../shared/launch-contract.ts";
@@ -109,7 +112,7 @@ import {
109
112
  type ChildWatchdogStateSnapshot,
110
113
  type ChildWatchdogStatusEvent,
111
114
  } from "../../watchdog/child-status.ts";
112
- import { buildInProcessChildLaunch } from "../shared/child-launch.ts";
115
+ import { buildInProcessChildLaunch, createReportedChildSessionInput } from "../shared/child-launch.ts";
113
116
  import { childSessionFactory, projectChildSessionEventForJson, type ChildSession, type ChildSessionEvent } from "../shared/child-session.ts";
114
117
 
115
118
  const artifactOutputByResult = new WeakMap<SingleResult, string>();
@@ -340,6 +343,7 @@ function structuredDelegationProgressChanged(
340
343
 
341
344
  const AFTER_COMPACTION_SETTLEMENT = Symbol("afterCompactionSettlement");
342
345
  type AbortRecoverySingleResult = SingleResult & { [AFTER_COMPACTION_SETTLEMENT]?: true };
346
+ const settledReadonlySource = new WeakMap<SingleResult, ChildSession>();
343
347
 
344
348
  const STOPPED_BEFORE_COMPLETION_ERROR = "Subagent stopped before completion.";
345
349
 
@@ -353,6 +357,7 @@ async function runSingleAttempt(
353
357
  shared: {
354
358
  sessionEnabled: boolean;
355
359
  systemPrompt: string;
360
+ acceptancePrompt: string;
356
361
  resolvedSkillNames?: string[];
357
362
  modelCandidates?: string[];
358
363
  skillsWarning?: string;
@@ -365,6 +370,9 @@ async function runSingleAttempt(
365
370
  orcaProgressTab?: OrcaProgressTab;
366
371
  launchWarnings: { emitted: boolean };
367
372
  verifyModel: boolean;
373
+ readonlyExpected?: SettledReadonlyEvidence;
374
+ readonlyModel?: string;
375
+ readonlyHandoffAllowed?: () => boolean;
368
376
  },
369
377
  ): Promise<SingleResult> {
370
378
  const effectiveThinking = options.thinkingOverride ?? agent.thinking;
@@ -405,7 +413,8 @@ async function runSingleAttempt(
405
413
  allowNestedSubagents: agent.allowNestedSubagents,
406
414
  extensions: agent.extensions,
407
415
  subagentOnlyExtensions: agent.subagentOnlyExtensions,
408
- systemPrompt: shared.systemPrompt,
416
+ // Exact terminal protocol belongs to session resources, not compactable history.
417
+ systemPrompt: shared.acceptancePrompt ? `${shared.systemPrompt}\n${shared.acceptancePrompt}` : shared.systemPrompt,
409
418
  mcpDirectTools: agent.mcpDirectTools,
410
419
  cwd: options.cwd ?? runtimeCwd,
411
420
  intercomSessionName: options.intercomSessionName,
@@ -425,7 +434,8 @@ async function runSingleAttempt(
425
434
  permissionRules,
426
435
  permissionAuditPath,
427
436
  childWatchdog,
428
- watchdogStatus: (event) => onWatchdogStatus?.(event),
437
+ // registerChildWatchdog returns before reading the sink when no watchdog exists.
438
+ watchdogStatus: childWatchdog ? (event) => onWatchdogStatus?.(event) : undefined,
429
439
  waitToolEnabled: options.waitToolEnabled,
430
440
  waitToolDefaultTimeoutMs: options.waitToolDefaultTimeoutMs,
431
441
  capabilityCeiling: options.capabilityCeiling,
@@ -780,7 +790,10 @@ async function runSingleAttempt(
780
790
  // JSONL artifact flush is best effort.
781
791
  });
782
792
  // Report the run only after the child's extensions have shut down.
783
- void Promise.resolve().then(() => session?.dispose()).catch(() => undefined).then(() => resolve(code));
793
+ void Promise.resolve().then(() => session?.dispose()).catch(() => undefined).then(() => {
794
+ if (session && getReadonlySessionEvidence(session)) settledReadonlySource.set(result, session);
795
+ resolve(code);
796
+ });
784
797
  };
785
798
 
786
799
  const drainPendingControlEvents = (): ControlEvent[] | undefined => {
@@ -983,6 +996,20 @@ async function runSingleAttempt(
983
996
  compactionStartedReceived = false;
984
997
  afterCompactionSettlement = false;
985
998
  }
999
+ if (evt.type === "agent_start") {
1000
+ const diagnostic = capture.toolDiagnostic();
1001
+ if (diagnostic) {
1002
+ const message = formatChildToolDiagnostic(diagnostic, { host: "parent" });
1003
+ toolAvailabilityError = message;
1004
+ result.error = message;
1005
+ result.finalOutput = message;
1006
+ progress.status = "failed";
1007
+ progress.error = message;
1008
+ fireUpdate();
1009
+ abortChild();
1010
+ return;
1011
+ }
1012
+ }
986
1013
  const lifecycleAction = projectChildLifecycle(evt, false, childLifecycleState);
987
1014
  if (evt.type === "agent_settled" && lifecycleAction === "start-drain") {
988
1015
  agentSettledReceived = true;
@@ -1086,11 +1113,15 @@ async function runSingleAttempt(
1086
1113
  progress.model = evt.message.model;
1087
1114
  if (!result.model) result.model = evt.message.model;
1088
1115
  if (expectedModelForVerification && !hasToolCall) {
1089
- const modelVerificationError = formatSubagentModelVerificationError(expectedModelForVerification, evt.message.model, options.availableModels);
1116
+ const modelVerificationError = formatSubagentModelVerificationError(expectedModelForVerification, evt.message.model, options.availableModels, options.modelResponseAliases);
1090
1117
  if (modelVerificationError && !result.error) result.error = modelVerificationError;
1091
1118
  }
1092
1119
  }
1093
1120
  if (evt.message.errorMessage) assistantError = evt.message.errorMessage;
1121
+ else if (hasToolCall && evt.message.stopReason === "toolUse") {
1122
+ // A recovered request can finish via a terminating tool, without a text stop.
1123
+ assistantError = undefined;
1124
+ }
1094
1125
  const assistantText = extractTextFromContent(evt.message.content);
1095
1126
  appendRecentOutput(progress, assistantText.split("\n").slice(-10));
1096
1127
  // Final assistant message: start the settle drain window.
@@ -1338,12 +1369,10 @@ async function runSingleAttempt(
1338
1369
 
1339
1370
  void (async () => {
1340
1371
  try {
1341
- const created = await childSessions.create({
1342
- ...launch.session,
1343
- onExtensionError: (error) => {
1344
- shared.transcriptWriter?.writeStderrLine(`Extension error (${error.extensionPath}, ${error.event}): ${error.error instanceof Error ? error.error.message : String(error.error)}`);
1345
- },
1346
- });
1372
+ const input = createReportedChildSessionInput(launch, shared.transcriptWriter);
1373
+ requestReadonlySessionEvidence(input, shared.readonlyExpected);
1374
+ if (shared.readonlyHandoffAllowed && !shared.readonlyHandoffAllowed()) throw new Error("Read-only continuation handoff vetoed.");
1375
+ const created = await childSessions.create(input);
1347
1376
  if (lifecycleFinished) {
1348
1377
  void created.dispose();
1349
1378
  return;
@@ -1355,6 +1384,10 @@ async function runSingleAttempt(
1355
1384
  abortChild();
1356
1385
  }
1357
1386
  options.onChildSession?.({ steer: (text) => created.steer(text), followUp: (text) => created.followUp(text) });
1387
+ const actualReadonlyModel = shared.readonlyExpected && getReadonlyChildModels(created)?.current;
1388
+ if (shared.readonlyExpected && (!actualReadonlyModel || actualReadonlyModel.fullId !== shared.readonlyModel
1389
+ || actualReadonlyModel.api !== shared.readonlyExpected.api || created.modelId !== shared.readonlyModel || abortedBySignal || interruptedByControl || result.timedOut
1390
+ || !shared.readonlyHandoffAllowed?.())) throw new Error("Read-only continuation handoff vetoed.");
1358
1391
  await created.prompt(`Task: ${task}`);
1359
1392
  settle(undefined);
1360
1393
  } catch (error) {
@@ -1483,7 +1516,7 @@ async function runSingleAttempt(
1483
1516
  ? `${timeoutMessage}\n\n${result.timeoutRecovery.message}\n\nPartial output before timeout:\n${fullOutput}`
1484
1517
  : `${timeoutMessage}\n\n${result.timeoutRecovery.message}`;
1485
1518
  }
1486
- const completionGuardEnabled = isAgentContractV1(options.agentContract) ? agent.completionGuard === true : agent.completionGuard !== false;
1519
+ const completionGuardEnabled = isAgentContract(options.agentContract) ? agent.completionGuard === true : agent.completionGuard !== false;
1487
1520
  const completionGuard = ((result.exitCode === 0 && !result.error) || toolAvailabilityError) && completionGuardEnabled
1488
1521
  ? evaluateCompletionMutationGuard({
1489
1522
  agent: agent.name,
@@ -1523,7 +1556,7 @@ async function runSingleAttempt(
1523
1556
  mutationAttemptObserved,
1524
1557
  mutationEvidence,
1525
1558
  arbiterRescued,
1526
- agentContractV1: isAgentContractV1(options.agentContract),
1559
+ agentContractEnabled: isAgentContract(options.agentContract),
1527
1560
  });
1528
1561
  if (completionEvidence.fileMutation) {
1529
1562
  result.effects = {
@@ -1719,7 +1752,7 @@ async function runSyncCompletionInner(
1719
1752
  dynamicGroup: options.acceptanceContext?.dynamicGroup,
1720
1753
  agentContract: options.agentContract,
1721
1754
  });
1722
- const acceptancePrompt = formatAcceptancePrompt(effectiveAcceptance, { reportOptional: isAgentContractV1(options.agentContract), structuredOutput: Boolean(options.structuredOutput?.acceptanceReportPath) });
1755
+ const acceptancePrompt = formatAcceptancePrompt(effectiveAcceptance, { reportOptional: isAgentContract(options.agentContract), structuredOutput: Boolean(options.structuredOutput?.acceptanceReportPath) });
1723
1756
  const taskWithAcceptance = acceptancePrompt ? `${task}\n${acceptancePrompt}` : task;
1724
1757
  options.onEffectivePrompt?.(taskWithAcceptance);
1725
1758
  const sessionEnabled = Boolean(options.sessionFile || options.sessionDir) || shareEnabled;
@@ -1870,19 +1903,33 @@ async function runSyncCompletionInner(
1870
1903
  };
1871
1904
  let lastResult: SingleResult | undefined;
1872
1905
  const modelsToTry = candidates.length > 0 ? candidates : [undefined];
1873
- let abortRecoveryAttempted = false;
1874
- let nextAttemptTask = taskWithAcceptance;
1906
+ let recoveryState: LogicalRecoveryState = "unused";
1907
+ let readonlyExpected: SettledReadonlyEvidence | undefined;
1908
+ let readonlyModel: string | undefined;
1909
+ let readonlySource: ChildSession | undefined;
1910
+ // Ordinary startup retries retain their per-attempt timeout. Only retained
1911
+ // continuation uses the original logical deadline, never a renewed allowance.
1912
+ const continuationDeadline = options.deadlineAt ?? (options.timeoutMs === undefined ? undefined : Date.now() + options.timeoutMs);
1913
+ const readonlyHandoffAllowed = () => !options.signal?.aborted && !options.interruptSignal?.aborted
1914
+ && !intercomDetached && !detachedReason && !options.workflowChildPermitLaunch
1915
+ && options.usageBudget === undefined && options.toolBudget === undefined
1916
+ && (continuationDeadline === undefined || Date.now() < continuationDeadline)
1917
+ && (!readonlySource || getReadonlySessionEvidence(readonlySource) === readonlyExpected)
1918
+ && !readonlySource?.detached && !readonlySource?.shutDown;
1919
+ let nextAttemptTask = task;
1875
1920
  modelAttemptsLoop: for (let modelIndex = 0; modelIndex < modelsToTry.length; modelIndex++) {
1876
1921
  const candidate = modelsToTry[modelIndex];
1877
1922
  // The inner loop re-runs the same candidate at most once, for abort recovery.
1878
1923
  for (;;) {
1879
- const recoveringAbort = abortRecoveryAttempted;
1924
+ const recoveringAbort = recoveryState === "abort-recovery";
1880
1925
  const attemptTask = nextAttemptTask;
1881
1926
  const verifyModel = Boolean(candidate) && !(options.modelOverrideFromParent && modelIndex === 0);
1882
1927
  const outputSnapshot = captureSingleOutputSnapshot(options.outputPath);
1928
+ if (recoveryState === "readonly-continuation") attemptOptions.deadlineAt = continuationDeadline;
1883
1929
  const result = await runSingleAttempt(runtimeCwd, agent, attemptTask, candidate, attemptOptions, {
1884
1930
  sessionEnabled,
1885
1931
  systemPrompt,
1932
+ acceptancePrompt,
1886
1933
  resolvedSkillNames: resolvedSkills.length > 0 ? resolvedSkills.map((skill) => skill.name) : undefined,
1887
1934
  skillsWarning: missingSkills.length > 0 ? `Skills not found: ${missingSkills.join(", ")}` : undefined,
1888
1935
  jsonlPath,
@@ -1897,6 +1944,9 @@ async function runSyncCompletionInner(
1897
1944
  orcaProgressTab,
1898
1945
  launchWarnings,
1899
1946
  verifyModel,
1947
+ readonlyExpected,
1948
+ readonlyModel,
1949
+ readonlyHandoffAllowed: readonlyExpected ? readonlyHandoffAllowed : undefined,
1900
1950
  });
1901
1951
  lastResult = result;
1902
1952
  if (!recoveringAbort) {
@@ -1915,6 +1965,46 @@ async function runSyncCompletionInner(
1915
1965
  usage: { ...result.usage },
1916
1966
  };
1917
1967
  modelAttempts.push(attempt);
1968
+ // A consumed retained continuation is terminal even on a startup error or abort.
1969
+ if (recoveryState === "readonly-continuation") break modelAttemptsLoop;
1970
+ const source = settledReadonlySource.get(result);
1971
+ const evidence = source && getReadonlySessionEvidence(source);
1972
+ const models = source && getReadonlyChildModels(source);
1973
+ // Deny non-text retained inputs, including unknown blocks. Count bytes once,
1974
+ // not restored usage (the latter belongs to earlier attempts/runs).
1975
+ const textOnly = evidence && (JSON.parse(evidence.contextJson) as Array<{ role: string; content: unknown }>).every((message) =>
1976
+ typeof message.content === "string" || Array.isArray(message.content) && message.content.every((block) =>
1977
+ block?.type === "text" || message.role === "assistant" && block?.type === "toolCall"));
1978
+ const retainedBytes = evidence && models ? Buffer.byteLength(evidence.contextJson) + models.requestBytes : Infinity;
1979
+ const resolvedCandidates = modelsToTry.map((reference, index) => index === modelIndex ? models?.current : reference ? models?.resolve(reference) : undefined);
1980
+ const continuation = planReadonlyModelContinuation({
1981
+ source, recoveryState, currentIndex: modelIndex,
1982
+ candidates: resolvedCandidates.map((resolved, index) => {
1983
+ // A conservative byte ceiling includes serialized history, actual system
1984
+ // prompt/tools, framing/continuation headroom and full output allowance.
1985
+ const required = resolved?.maxTokens ? retainedBytes + 4096 + resolved.maxTokens : Infinity;
1986
+ return {
1987
+ resolved: resolved?.api ? { provider: resolved.provider, model: resolved.id, api: resolved.api } : undefined,
1988
+ tried: index <= modelIndex,
1989
+ compatibility: textOnly && resolved?.input?.includes("text") && (resolved.contextWindow ?? 0) >= required ? "compatible" as const : "unknown" as const,
1990
+ };
1991
+ }),
1992
+ lifecycleAllowsContinuation: !attemptSucceeded && readonlyHandoffAllowed() && !result.stopped && !result.detached && !result.interrupted && !result.timedOut,
1993
+ effectsAllowContinuation: !result.structuredOutputFailed && !result.toolBudgetBlocked && !result.progress?.currentTool
1994
+ && !result.outputSaveError && (!result.effects?.fileMutation || result.effects.fileMutation.status === "not-applicable"),
1995
+ budget: options.toolBudget ? "tool-budget-configured" : options.usageBudget ? "unknown" : "unconfigured",
1996
+ knownContextOverflow: Boolean(result.contextOverflow || isContextOverflow(result.error)),
1997
+ });
1998
+ if (continuation.kind === "continue") {
1999
+ recoveryState = continuation.recoveryState; // consume BEFORE any sibling creation
2000
+ readonlyExpected = continuation.expected;
2001
+ readonlySource = source;
2002
+ readonlyModel = resolvedCandidates[continuation.candidateIndex]?.fullId;
2003
+ nextAttemptTask = continuation.prompt;
2004
+ attemptNotes.push(`[readonly-continuation] ${attempt.model} failed with HTTP 429 after read-only progress; continuing retained session once with ${readonlyModel}.`);
2005
+ modelIndex = continuation.candidateIndex - 1;
2006
+ continue modelAttemptsLoop;
2007
+ }
1918
2008
  if (!attemptSucceeded) {
1919
2009
  const afterCompactionSettlement = (result as AbortRecoverySingleResult)[AFTER_COMPACTION_SETTLEMENT];
1920
2010
  const abortRecovery = planAbortRecovery({
@@ -1922,7 +2012,7 @@ async function runSyncCompletionInner(
1922
2012
  error: result.error,
1923
2013
  processSignal: result.processSignal,
1924
2014
  sessionAvailable: Boolean(options.sessionFile && existsSync(options.sessionFile)),
1925
- alreadyResumed: abortRecoveryAttempted,
2015
+ alreadyResumed: recoveryState !== "unused",
1926
2016
  stopped: result.stopped || result.detached || options.signal?.aborted,
1927
2017
  interrupted: result.interrupted || intercomDetached || options.interruptSignal?.aborted,
1928
2018
  timedOut: result.timedOut,
@@ -1934,7 +2024,7 @@ async function runSyncCompletionInner(
1934
2024
  afterCompactionSettlement,
1935
2025
  });
1936
2026
  if (abortRecovery.action === "resume") {
1937
- abortRecoveryAttempted = true;
2027
+ recoveryState = "abort-recovery";
1938
2028
  nextAttemptTask = abortRecovery.prompt;
1939
2029
  attemptNotes.push("[abort-recovery] provider/transport abort after useful progress; resuming the retained child session once.");
1940
2030
  continue;
@@ -2045,7 +2135,7 @@ async function runSyncCompletionInner(
2045
2135
  ? { content: childWrittenOutput, path: options.outputPath, authoritative: options.outputMode === "file-only", durable: result.savedOutputPath !== undefined }
2046
2136
  : undefined,
2047
2137
  cwd: options.cwd ?? runtimeCwd,
2048
- reportOptional: isAgentContractV1(options.agentContract),
2138
+ reportOptional: isAgentContract(options.agentContract),
2049
2139
  artifactsDir: options.artifactsDir,
2050
2140
  runId: options.runId,
2051
2141
  watchdog: result.watchdog,
@@ -2057,7 +2147,7 @@ async function runSyncCompletionInner(
2057
2147
  }
2058
2148
  const acceptanceFailure = acceptanceFailureMessage(result.acceptance);
2059
2149
  stripAcceptanceReportsFromMessages(result.messages);
2060
- if (acceptanceFailure && result.acceptance.explicit && result.exitCode === 0 && !result.interrupted && !result.timedOut && !isAgentContractV1(options.agentContract)) {
2150
+ if (acceptanceFailure && result.acceptance.explicit && result.exitCode === 0 && !result.interrupted && !result.timedOut && !isAgentContract(options.agentContract)) {
2061
2151
  result.exitCode = 1;
2062
2152
  if (result.savedOutputPath) {
2063
2153
  result.finalOutput = finalizeSingleOutput({
@@ -2090,7 +2180,7 @@ async function runSyncCompletionInner(
2090
2180
  result.progress.error = result.error;
2091
2181
  }
2092
2182
  }
2093
- if (isAgentContractV1(options.agentContract)) attachContractProjections(result);
2183
+ if (isAgentContract(options.agentContract)) attachContractProjections(result);
2094
2184
  redactResultPrompt(result);
2095
2185
  try {
2096
2186
  persistResultMetadata(result);
@@ -2135,7 +2225,7 @@ export async function runSync(
2135
2225
  ): Promise<SingleResult> {
2136
2226
  // Capture the strict contract before consumer-owned objects can be mutated
2137
2227
  // after a detached receipt is published.
2138
- const strictContract = isAgentContractV1(options.agentContract);
2228
+ const strictContract = isAgentContract(options.agentContract);
2139
2229
  let detachedReason: string | undefined;
2140
2230
  let publishedReceipt: SingleResult | undefined;
2141
2231
  let activeDetachAttempt: ((reason?: string) => boolean) | undefined;
@@ -2233,13 +2323,13 @@ export async function runSync(
2233
2323
  });
2234
2324
 
2235
2325
  let terminalCallbackInvoked = false;
2236
- void authoritativeCompletion.then((terminalResult) => {
2326
+ void authoritativeCompletion.then(async (terminalResult) => {
2237
2327
  if (!detachedReason || terminalCallbackInvoked) return;
2238
2328
  terminalCallbackInvoked = true;
2239
2329
  terminalResult.detached = undefined;
2240
2330
  terminalResult.detachedReason = detachedReason;
2241
2331
  try {
2242
- options.onDetachedExit?.(terminalResult);
2332
+ await options.onDetachedExit?.(terminalResult);
2243
2333
  } catch {
2244
2334
  // The authoritative result has settled. Consumer callback failures are
2245
2335
  // contained here; each consumer owns cleanup through its own finally block.
@@ -5,6 +5,7 @@ import { DIRS } from "../../shared/types.ts";
5
5
  import { writePrivateAtomicJson } from "../../shared/atomic-json.ts";
6
6
  import { utf8Tail } from "../../shared/utf8.ts";
7
7
  import { validateAcceptanceInput } from "../shared/acceptance.ts";
8
+ import { validateModelResponseAliases } from "../../shared/model-response-aliases.ts";
8
9
 
9
10
  export const MAX_REMEMBERED_FOREGROUND_RUNS = 50;
10
11
  const HISTORY_VERSION = 1;
@@ -71,11 +72,12 @@ function isRestorableResumeContract(value: unknown): boolean {
71
72
  if (!value || typeof value !== "object" || Array.isArray(value)) return false;
72
73
  try {
73
74
  if (Buffer.byteLength(JSON.stringify(value), "utf8") > MAX_RESUME_CONTRACT_BYTES) return false;
75
+ validateModelResponseAliases((value as ForegroundResumeChild["resumeContract"])?.modelResponseAliases);
74
76
  } catch {
75
77
  return false;
76
78
  }
77
79
  const contract = value as NonNullable<ForegroundResumeChild["resumeContract"]>;
78
- if (Object.keys(contract).some((key) => !["outputSchema", "agentContract", "acceptance", "output", "outputMode"].includes(key))) return false;
80
+ if (Object.keys(contract).some((key) => !["modelResponseAliases", "outputSchema", "agentContract", "acceptance", "output", "outputMode"].includes(key))) return false;
79
81
  if (contract.outputSchema !== undefined && (!contract.outputSchema || typeof contract.outputSchema !== "object" || Array.isArray(contract.outputSchema))) return false;
80
82
  if (contract.agentContract !== undefined && (!contract.agentContract || typeof contract.agentContract !== "object" || Array.isArray(contract.agentContract) || contract.agentContract.version !== 1)) return false;
81
83
  if (validateAcceptanceInput(contract.acceptance).length > 0) return false;
@@ -1,7 +1,6 @@
1
- import { Agent, type StreamFn, type ThinkingLevel } from "@earendil-works/pi-agent-core";
2
- import { convertToLlm, type ExtensionContext } from "@earendil-works/pi-coding-agent";
3
- import { streamSimple } from "@earendil-works/pi-ai/compat";
4
- import type { Model, ProviderHeaders } from "@earendil-works/pi-ai";
1
+ import type { Agent, StreamFn, ThinkingLevel } from "@earendil-works/pi-agent-core";
2
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
3
+ import type { ProviderHeaders } from "@earendil-works/pi-ai";
5
4
  import { agentStreamOptions } from "../../shared/agent-stream-options.ts";
6
5
  import type { ForegroundRunControl } from "../../shared/types.ts";
7
6
  export type PromptAuditView = "authored" | "runtime" | "effective";
@@ -21,7 +20,7 @@ export interface LivePromptAudit {
21
20
 
22
21
  const livePrompts = new WeakMap<ForegroundRunControl, Map<number, LivePromptAudit>>();
23
22
 
24
- type RegistryModel = Model<any>;
23
+ type RegistryModel = NonNullable<ExtensionContext["model"]>;
25
24
 
26
25
  function fullModelId(model: Pick<RegistryModel, "provider" | "id">): string {
27
26
  return `${model.provider}/${model.id}`;
@@ -64,6 +63,11 @@ export async function rewritePromptWithGuidance(input: {
64
63
  }): Promise<string> {
65
64
  const model = input.ctx.model;
66
65
  if (!model) throw new Error("Prompt redo needs the current session model to rewrite the authored task.");
66
+ const [{ Agent }, { convertToLlm }, { streamSimple }] = await Promise.all([
67
+ import("@earendil-works/pi-agent-core"),
68
+ import("@earendil-works/pi-coding-agent"),
69
+ import("@earendil-works/pi-ai/compat"),
70
+ ]);
67
71
  const auth = await resolveRewriteAuth(input.ctx, model);
68
72
  const registeredProvider = (input.ctx.modelRegistry as {
69
73
  getRegisteredProviderConfig?: (provider: string) => { api?: string; streamSimple?: StreamFn } | undefined;