@oh-my-pi/pi-coding-agent 16.4.4 → 16.4.6

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 (109) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/dist/cli.js +3799 -3729
  3. package/dist/types/async/job-manager.d.ts +8 -0
  4. package/dist/types/cli/bench-cli.d.ts +1 -7
  5. package/dist/types/cli/usage-cli.d.ts +1 -0
  6. package/dist/types/commands/usage.d.ts +7 -0
  7. package/dist/types/config/settings-schema.d.ts +19 -9
  8. package/dist/types/config/settings.d.ts +3 -2
  9. package/dist/types/discovery/helpers.d.ts +2 -2
  10. package/dist/types/extensibility/extensions/types.d.ts +36 -0
  11. package/dist/types/irc/bus.d.ts +4 -0
  12. package/dist/types/modes/components/__tests__/pause-screen.test.d.ts +1 -0
  13. package/dist/types/modes/components/ask-dialog.d.ts +27 -0
  14. package/dist/types/modes/components/custom-editor.d.ts +3 -8
  15. package/dist/types/modes/components/index.d.ts +2 -1
  16. package/dist/types/modes/components/model-browser.d.ts +100 -0
  17. package/dist/types/modes/components/model-hub.d.ts +52 -0
  18. package/dist/types/modes/components/pause-screen.d.ts +43 -0
  19. package/dist/types/modes/components/session-selector.d.ts +13 -0
  20. package/dist/types/modes/components/tool-execution.d.ts +2 -0
  21. package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -1
  22. package/dist/types/modes/controllers/input-controller.d.ts +2 -0
  23. package/dist/types/modes/interactive-mode.d.ts +3 -0
  24. package/dist/types/modes/queue-input.d.ts +8 -0
  25. package/dist/types/modes/shared.d.ts +1 -1
  26. package/dist/types/modes/types.d.ts +4 -0
  27. package/dist/types/session/agent-session.d.ts +1 -1
  28. package/dist/types/session/agent-storage.d.ts +57 -0
  29. package/dist/types/session/session-context.d.ts +9 -0
  30. package/dist/types/task/executor.d.ts +26 -13
  31. package/dist/types/task/index.d.ts +12 -11
  32. package/dist/types/task/label.d.ts +4 -0
  33. package/dist/types/task/repair-args.d.ts +8 -8
  34. package/dist/types/task/types.d.ts +31 -56
  35. package/dist/types/tools/ask.d.ts +12 -0
  36. package/dist/types/tools/conflict-detect.d.ts +17 -1
  37. package/dist/types/tools/job.d.ts +16 -0
  38. package/package.json +12 -12
  39. package/scripts/build-binary.ts +0 -1
  40. package/scripts/compile-binary.ts +4 -3
  41. package/src/async/job-manager.ts +9 -0
  42. package/src/cli/bench-cli.ts +7 -26
  43. package/src/cli/usage-cli.ts +11 -0
  44. package/src/commands/usage.ts +13 -2
  45. package/src/commit/agentic/tools/analyze-file.ts +2 -3
  46. package/src/config/settings-schema.ts +18 -7
  47. package/src/config/settings.ts +13 -4
  48. package/src/discovery/helpers.ts +3 -4
  49. package/src/extensibility/custom-tools/loader.ts +70 -37
  50. package/src/extensibility/extensions/types.ts +46 -0
  51. package/src/irc/bus.ts +61 -20
  52. package/src/modes/components/__tests__/pause-screen.test.ts +143 -0
  53. package/src/modes/components/advisor-config.ts +32 -22
  54. package/src/modes/components/ask-dialog.ts +888 -0
  55. package/src/modes/components/custom-editor.test.ts +58 -1
  56. package/src/modes/components/custom-editor.ts +42 -11
  57. package/src/modes/components/index.ts +2 -1
  58. package/src/modes/components/model-browser.ts +769 -0
  59. package/src/modes/components/model-hub.ts +2002 -0
  60. package/src/modes/components/pause-screen.ts +208 -0
  61. package/src/modes/components/session-selector.ts +299 -42
  62. package/src/modes/components/tool-execution.ts +2 -0
  63. package/src/modes/components/usage-row.ts +5 -6
  64. package/src/modes/controllers/event-controller.ts +8 -2
  65. package/src/modes/controllers/extension-ui-controller.ts +252 -5
  66. package/src/modes/controllers/input-controller.ts +140 -6
  67. package/src/modes/controllers/selector-controller.ts +160 -97
  68. package/src/modes/controllers/tan-command-controller.ts +1 -1
  69. package/src/modes/controllers/todo-command-controller.ts +1 -2
  70. package/src/modes/interactive-mode.ts +8 -0
  71. package/src/modes/queue-input.ts +132 -0
  72. package/src/modes/shared.ts +1 -1
  73. package/src/modes/theme/theme.ts +3 -3
  74. package/src/modes/types.ts +4 -0
  75. package/src/modes/utils/ui-helpers.ts +50 -24
  76. package/src/prompts/agents/scout.md +0 -1
  77. package/src/prompts/agents/task.md +1 -1
  78. package/src/prompts/system/subagent-system-prompt.md +1 -5
  79. package/src/prompts/system/subagent-yield-reminder.md +10 -0
  80. package/src/prompts/system/task-label.md +23 -0
  81. package/src/prompts/tools/job.md +1 -1
  82. package/src/prompts/tools/task-summary.md +3 -0
  83. package/src/prompts/tools/task.md +17 -18
  84. package/src/session/agent-session.ts +186 -49
  85. package/src/session/agent-storage.ts +330 -3
  86. package/src/session/history-storage.ts +1 -34
  87. package/src/session/session-context.test.ts +73 -0
  88. package/src/session/session-context.ts +43 -26
  89. package/src/slash-commands/builtin-registry.ts +18 -0
  90. package/src/task/agents.ts +2 -0
  91. package/src/task/executor.ts +159 -46
  92. package/src/task/index.ts +377 -239
  93. package/src/task/label.ts +38 -0
  94. package/src/task/render.ts +74 -22
  95. package/src/task/repair-args.ts +20 -31
  96. package/src/task/spawn-policy.test.ts +4 -4
  97. package/src/task/types.ts +46 -66
  98. package/src/tools/ask.ts +233 -40
  99. package/src/tools/conflict-detect.ts +102 -5
  100. package/src/tools/index.ts +1 -0
  101. package/src/tools/irc.ts +20 -11
  102. package/src/tools/job.ts +158 -18
  103. package/src/tools/write.ts +70 -6
  104. package/src/vibe/runtime.ts +1 -1
  105. package/src/web/search/providers/browser-headers.ts +30 -13
  106. package/dist/types/modes/components/model-selector.d.ts +0 -37
  107. package/dist/types/tools/bash-command-fixup.d.ts +0 -3
  108. package/src/modes/components/model-selector.ts +0 -1291
  109. package/src/tools/bash-command-fixup.ts +0 -4
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import path from "node:path";
8
- import type { AgentEvent, AgentIdentity, AgentTelemetryConfig, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
8
+ import type { AgentEvent, AgentIdentity, AgentTelemetryConfig } from "@oh-my-pi/pi-agent-core";
9
9
  import { recordHandoff, resolveTelemetry } from "@oh-my-pi/pi-agent-core";
10
10
  import type { Api, Model, ServiceTierByFamily, Usage } from "@oh-my-pi/pi-ai";
11
11
  import { logger, popLoopPhase, prompt, pushLoopPhase, untilAborted } from "@oh-my-pi/pi-utils";
@@ -42,6 +42,7 @@ import type { AuthStorage } from "../session/auth-storage";
42
42
  import { SKILL_PROMPT_MESSAGE_TYPE, USER_INTERRUPT_LABEL } from "../session/messages";
43
43
  import { SessionManager } from "../session/session-manager";
44
44
  import { truncateTail } from "../session/streaming-output";
45
+ import type { ConfiguredThinkingLevel } from "../thinking";
45
46
  import type { ContextFileEntry, ToolSession } from "../tools";
46
47
  import { resolveEvalBackends } from "../tools/eval-backends";
47
48
  import { isIrcEnabled } from "../tools/irc";
@@ -56,15 +57,14 @@ import { ToolAbortError } from "../tools/tool-errors";
56
57
  import type { EventBus } from "../utils/event-bus";
57
58
  import { buildNamedToolChoice } from "../utils/tool-choice";
58
59
  import type { WorkspaceTree } from "../workspace-tree";
60
+ import { generateTaskLabel } from "./label";
59
61
  import { subprocessToolRegistry } from "./subprocess-tool-registry";
60
62
  import {
61
63
  type AgentDefinition,
62
64
  type AgentProgress,
63
65
  MAX_OUTPUT_BYTES,
64
66
  MAX_OUTPUT_LINES,
65
- oneLineLabel,
66
67
  type ReviewFinding,
67
- resolveSubagentDisplayName,
68
68
  type SingleResult,
69
69
  TASK_SUBAGENT_EVENT_CHANNEL,
70
70
  TASK_SUBAGENT_LIFECYCLE_CHANNEL,
@@ -79,23 +79,27 @@ export type { YieldItem } from "./types";
79
79
  const MCP_CALL_TIMEOUT_MS = 60_000;
80
80
 
81
81
  /**
82
- * Soft per-agent request budgets (assistant requests per run). When a subagent
83
- * crosses its budget it can receive an optional steering notice asking it to
84
- * wrap up; at 1.5x the budget the run is aborted gracefully so partial output is
85
- * salvaged. The `default` key applies to agents without an explicit entry and
86
- * can be overridden via the `task.softRequestBudget` setting (0 disables the
87
- * guard). The notice is off by default and controlled separately by
88
- * `task.softRequestBudgetNotice`.
82
+ * Soft per-agent request budgets (assistant requests per run). Crossing the
83
+ * budget injects a wrap-up steering notice (`task.softRequestBudgetNotice`,
84
+ * on by default). At 1.5x the budget the free-running turn is stopped and the
85
+ * agent is driven to one forced final `yield` so partial findings come back
86
+ * as a real report; only if it still refuses to yield within
87
+ * {@link BUDGET_STOP_GRACE_REQUESTS} more requests is the run hard-aborted.
88
+ * The `default` key applies to agents without an explicit entry and can be
89
+ * overridden via the `task.softRequestBudget` setting (0 disables the guard).
89
90
  */
90
91
  export const SOFT_REQUEST_BUDGET: Record<string, number> = {
91
- scout: 40,
92
- sonic: 40,
93
- default: 90,
92
+ scout: 100,
93
+ sonic: 100,
94
+ default: 200,
94
95
  };
95
96
 
96
- /** Optional steering notice injected when a subagent crosses its soft request budget. */
97
- export function buildBudgetNotice(requests: number): string {
98
- return `[budget notice] You have used ${requests} requests in this run. Wrap up now: finish the current step and yield your final report.`;
97
+ /** Extra requests allowed after a budget stop for the forced yield to land before the run is hard-aborted. */
98
+ export const BUDGET_STOP_GRACE_REQUESTS = 5;
99
+
100
+ /** Steering notice injected when a subagent crosses its soft request budget. */
101
+ export function buildBudgetNotice(requests: number, budget: number): string {
102
+ return `[budget notice] You have used ${requests} requests in this run (soft budget: ${budget}). Wrap up now: finish the current step and yield your final report. At ${Math.ceil(budget * 1.5)} requests the run is force-stopped and you will be asked to yield whatever you have.`;
99
103
  }
100
104
 
101
105
  /** Flatten whitespace and clip salvage text for the cancelled-child summary line. */
@@ -287,9 +291,8 @@ export interface ExecutorOptions {
287
291
  * the session did not start with a plan (or while plan mode is still active).
288
292
  */
289
293
  planReference?: { path: string; content: string };
294
+ /** Pre-set UI label (e.g. eval bridge label). When absent, a tiny-model label is generated from the assignment. */
290
295
  description?: string;
291
- /** Specialist role/expertise for this spawn; drives the system-prompt preamble, display name, and telemetry identity. */
292
- role?: string;
293
296
  index: number;
294
297
  id: string;
295
298
  parentToolCallId?: string;
@@ -306,7 +309,7 @@ export interface ExecutorOptions {
306
309
  * if the resolved subagent model has no working credentials. See #985.
307
310
  */
308
311
  parentActiveModelPattern?: string;
309
- thinkingLevel?: ThinkingLevel;
312
+ thinkingLevel?: ConfiguredThinkingLevel;
310
313
  outputSchema?: unknown;
311
314
  /**
312
315
  * Caller supplied a schema that supersedes the agent's native output prompt.
@@ -800,7 +803,7 @@ export function createSubagentSettings(
800
803
  });
801
804
  }
802
805
 
803
- type AbortReason = "signal" | "terminate" | "timeout" | "budget";
806
+ export type AbortReason = "signal" | "terminate" | "timeout" | "budget";
804
807
 
805
808
  /** Inputs for the run monitor driving one subagent assignment. */
806
809
  interface RunMonitorArgs {
@@ -810,6 +813,10 @@ interface RunMonitorArgs {
810
813
  task: string;
811
814
  assignment?: string;
812
815
  description?: string;
816
+ /** Parent model registry for tiny-model label generation; absent → skip labeling. */
817
+ modelRegistry?: ModelRegistry;
818
+ /** Parent settings for tiny-model label generation. */
819
+ settings?: Settings;
813
820
  modelOverride?: string | string[];
814
821
  signal?: AbortSignal;
815
822
  onProgress?: (progress: AgentProgress) => void;
@@ -838,6 +845,12 @@ interface SubagentRunMonitor {
838
845
  hasUsage(): boolean;
839
846
  yieldCalled(): boolean;
840
847
  runtimeLimitExceeded(): boolean;
848
+ /** True once the soft-budget stop fired: the free-running turn was aborted and the run is being driven to a forced final yield. */
849
+ budgetStopRequested(): boolean;
850
+ /** Resolves when the budget-stop session abort has settled (immediately when no stop fired). */
851
+ waitForBudgetStop(): Promise<void>;
852
+ /** The abort kind for this run, when an abort was requested. */
853
+ abortKind(): AbortReason | undefined;
841
854
  /** True when the abort carries a precise external reason (signal / wall-clock / budget). */
842
855
  hasExplicitAbortReason(): boolean;
843
856
  /** Whether the (attempted) abort counts as a cancelled run rather than an internal failure. */
@@ -927,6 +940,8 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
927
940
  let hasUsage = false;
928
941
  let budgetSteerSent = false;
929
942
  let budgetLimitExceeded = false;
943
+ let budgetStopRequested = false;
944
+ let budgetStopAbortPromise: Promise<void> | undefined;
930
945
  let lastAssistantSalvageText: string | undefined;
931
946
  let activeSessionAbortPromise: Promise<void> | undefined;
932
947
 
@@ -965,6 +980,24 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
965
980
  void abortActiveSession();
966
981
  };
967
982
 
983
+ // Soft-budget stop: cancel the free-running turn WITHOUT aborting the
984
+ // monitor, so driveSessionToYield can still drive one forced final yield.
985
+ // Deliberately not routed through abortActiveSession(): that memoizes its
986
+ // promise, and a later hard abort (grace exhausted) must be able to abort
987
+ // the session again.
988
+ const requestBudgetStop = () => {
989
+ if (budgetStopRequested || abortSent || resolved) return;
990
+ budgetStopRequested = true;
991
+ const session = activeSession;
992
+ budgetStopAbortPromise = session
993
+ ? session.abort().catch(error => {
994
+ logger.debug("Subagent budget-stop abort failed", {
995
+ error: error instanceof Error ? error.message : String(error),
996
+ });
997
+ })
998
+ : Promise.resolve();
999
+ };
1000
+
968
1001
  // Handle abort signal
969
1002
  if (signal) {
970
1003
  signal.addEventListener(
@@ -1010,6 +1043,9 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
1010
1043
  return `Subagent runtime limit exceeded (task.maxRuntimeMs=${maxRuntimeMs})`;
1011
1044
  }
1012
1045
  if (budgetLimitExceeded) {
1046
+ return `Soft request budget exceeded (${progress.requests} requests; budget ${softRequestBudget}) — agent did not yield when force-stopped`;
1047
+ }
1048
+ if (budgetStopRequested) {
1013
1049
  return `Soft request budget exceeded (${progress.requests} requests; budget ${softRequestBudget})`;
1014
1050
  }
1015
1051
  return resolveSignalAbortReason();
@@ -1066,6 +1102,27 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
1066
1102
  }, PROGRESS_COALESCE_MS - elapsed);
1067
1103
  };
1068
1104
 
1105
+ // The task wire schema carries no description: when the caller didn't pre-set
1106
+ // a UI label (e.g. the eval bridge's `label`), compress the assignment into a
1107
+ // tiny-model one-sentence label off the spawn's critical path. Best-effort —
1108
+ // a late label still lands via the finalize-time reads of `progress.description`;
1109
+ // failures just leave the label unset.
1110
+ const labelSource = assignment?.trim();
1111
+ if (!args.description && args.modelRegistry && args.settings && labelSource) {
1112
+ generateTaskLabel(labelSource, args.modelRegistry, args.settings, id)
1113
+ .then(label => {
1114
+ if (!label || abortSignal.aborted || progress.description) return;
1115
+ progress.description = label;
1116
+ if (!resolved) scheduleProgress();
1117
+ })
1118
+ .catch(err => {
1119
+ logger.debug("Subagent label generation failed", {
1120
+ id,
1121
+ error: err instanceof Error ? err.message : String(err),
1122
+ });
1123
+ });
1124
+ }
1125
+
1069
1126
  const getMessageContent = (message: unknown): unknown => {
1070
1127
  if (!isRecord(message) || !("content" in message)) {
1071
1128
  return undefined;
@@ -1323,14 +1380,26 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
1323
1380
  }
1324
1381
  }
1325
1382
  if (softRequestBudget > 0 && !abortSent && !yieldCallPending) {
1326
- if (progress.requests >= softRequestBudget * 1.5) {
1327
- requestAbort("budget");
1383
+ const stopThreshold = softRequestBudget * 1.5;
1384
+ if (budgetStopRequested) {
1385
+ // Grace window after the stop: the forced yield needs a
1386
+ // request or two; a child that keeps burning requests
1387
+ // instead of yielding is hard-aborted.
1388
+ if (progress.requests >= stopThreshold + BUDGET_STOP_GRACE_REQUESTS) {
1389
+ requestAbort("budget");
1390
+ }
1391
+ } else if (progress.requests >= stopThreshold) {
1392
+ requestBudgetStop();
1328
1393
  } else if (softRequestBudgetNotice && !budgetSteerSent && progress.requests >= softRequestBudget) {
1329
1394
  budgetSteerSent = true;
1330
1395
  const steerSession = activeSession;
1331
1396
  if (steerSession) {
1332
- void steerSession
1333
- .sendUserMessage(buildBudgetNotice(progress.requests), { deliverAs: "steer" })
1397
+ // Build the notice now (the count at crossing time), but send
1398
+ // behind an async boundary: a synchronously-throwing send must
1399
+ // never take down event processing (which escalates to terminate).
1400
+ const notice = buildBudgetNotice(progress.requests, softRequestBudget);
1401
+ void Promise.resolve()
1402
+ .then(() => steerSession.sendUserMessage(notice, { deliverAs: "steer" }))
1334
1403
  .catch(err => {
1335
1404
  logger.warn("Subagent budget steer failed", {
1336
1405
  error: err instanceof Error ? err.message : String(err),
@@ -1481,7 +1550,13 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
1481
1550
  hasUsage: () => hasUsage,
1482
1551
  yieldCalled: () => yieldCalled,
1483
1552
  runtimeLimitExceeded: () => runtimeLimitExceeded,
1484
- hasExplicitAbortReason: () => abortReason === "signal" || runtimeLimitExceeded || budgetLimitExceeded,
1553
+ hasExplicitAbortReason: () =>
1554
+ abortReason === "signal" || runtimeLimitExceeded || budgetLimitExceeded || budgetStopRequested,
1555
+ budgetStopRequested: () => budgetStopRequested,
1556
+ waitForBudgetStop: () => budgetStopAbortPromise ?? Promise.resolve(),
1557
+ // A soft stop that never escalated still identifies as a budget abort so
1558
+ // the lifecycle can park the agent as resumable instead of killing it.
1559
+ abortKind: () => abortReason ?? (budgetStopRequested ? "budget" : undefined),
1485
1560
  isAbortedRun: () =>
1486
1561
  abortReason === "signal" || runtimeLimitExceeded || budgetLimitExceeded || abortReason === undefined,
1487
1562
  requestAbort,
@@ -1529,7 +1604,9 @@ const MAX_YIELD_RETRIES = 3;
1529
1604
  /**
1530
1605
  * Drive one assignment through a live session: send the prompt, wait for idle,
1531
1606
  * remind the agent to `yield` (up to {@link MAX_YIELD_RETRIES} times), then
1532
- * classify the terminal assistant state.
1607
+ * classify the terminal assistant state. A soft-budget stop short-circuits the
1608
+ * reminder ladder into a single forced final yield so partial findings still
1609
+ * come back as a real report.
1533
1610
  */
1534
1611
  async function driveSessionToYield(
1535
1612
  session: AgentSession,
@@ -1570,13 +1647,30 @@ async function driveSessionToYield(
1570
1647
  };
1571
1648
 
1572
1649
  try {
1573
- await awaitAbortable(session.prompt(task, { attribution: "agent" }));
1574
- await awaitAbortable(session.waitForIdle());
1650
+ try {
1651
+ await awaitAbortable(session.prompt(task, { attribution: "agent" }));
1652
+ await awaitAbortable(session.waitForIdle());
1653
+ } catch (err) {
1654
+ // A budget stop cancels the free-running turn by aborting the
1655
+ // session, which can surface here as a rejected prompt. Swallow it
1656
+ // and drive the forced final yield below; real caller/timeout
1657
+ // aborts (monitor signal) and genuine failures keep the old path.
1658
+ if (!monitor.budgetStopRequested() || abortSignal.aborted) throw err;
1659
+ }
1575
1660
 
1576
1661
  const reminderToolChoice = buildNamedToolChoice("yield", session.model);
1577
1662
 
1578
1663
  let retryCount = 0;
1579
1664
  while (!monitor.yieldCalled() && retryCount < MAX_YIELD_RETRIES && !abortSignal.aborted) {
1665
+ // A budget stop collapses the reminder ladder to a single forced
1666
+ // final yield: wait for the stop's session abort to settle, then
1667
+ // prompt once with the wrap-up reminder + named tool choice.
1668
+ const budgetStop = monitor.budgetStopRequested();
1669
+ if (budgetStop) {
1670
+ retryCount = MAX_YIELD_RETRIES - 1;
1671
+ await monitor.waitForBudgetStop();
1672
+ if (monitor.yieldCalled() || abortSignal.aborted) break;
1673
+ }
1580
1674
  // Skip reminders when the model returned a terminal error (e.g.
1581
1675
  // rate-limit cap hit, auth failure). Re-prompting would just
1582
1676
  // hit the same wall, multiplying the failure noise without
@@ -1588,6 +1682,7 @@ async function driveSessionToYield(
1588
1682
  const reminder = prompt.render(submitReminderTemplate, {
1589
1683
  retryCount,
1590
1684
  maxRetries: MAX_YIELD_RETRIES,
1685
+ budgetStop,
1591
1686
  });
1592
1687
 
1593
1688
  const isFinalRetry = retryCount >= MAX_YIELD_RETRIES;
@@ -1642,6 +1737,14 @@ async function driveSessionToYield(
1642
1737
  error ??= lastAssistant.errorMessage || "Subagent failed";
1643
1738
  }
1644
1739
  }
1740
+
1741
+ // A budget-stopped run that still produced no yield is a budget abort:
1742
+ // surface the precise reason instead of a generic missing-yield failure.
1743
+ if (!monitor.yieldCalled() && monitor.budgetStopRequested() && !aborted) {
1744
+ aborted = true;
1745
+ abortReasonText ??= monitor.resolveAbortReasonText();
1746
+ exitCode = 1;
1747
+ }
1645
1748
  } catch (err) {
1646
1749
  if (abortSignal.aborted && monitor.yieldCalled() && !monitor.runtimeLimitExceeded()) {
1647
1750
  exitCode = 0;
@@ -1672,7 +1775,6 @@ interface FinalizeRunArgs {
1672
1775
  agent: AgentDefinition;
1673
1776
  task: string;
1674
1777
  assignment?: string;
1675
- description?: string;
1676
1778
  modelOverride?: string | string[];
1677
1779
  outputSchema?: unknown;
1678
1780
  signal?: AbortSignal;
@@ -1789,7 +1891,7 @@ async function finalizeRunResult(args: FinalizeRunArgs): Promise<SingleResult> {
1789
1891
  parentToolCallId: args.parentToolCallId,
1790
1892
  detached: args.detached,
1791
1893
  agentSource: agent.source,
1792
- description: args.description,
1894
+ description: progress.description,
1793
1895
  status: progress.status as "completed" | "failed" | "aborted",
1794
1896
  sessionFile: args.sessionFile,
1795
1897
  index,
@@ -1803,7 +1905,7 @@ async function finalizeRunResult(args: FinalizeRunArgs): Promise<SingleResult> {
1803
1905
  agentSource: agent.source,
1804
1906
  task,
1805
1907
  assignment,
1806
- description: args.description,
1908
+ description: progress.description,
1807
1909
  lastIntent: progress.lastIntent,
1808
1910
  exitCode,
1809
1911
  output: truncatedOutput,
@@ -1827,10 +1929,19 @@ async function finalizeRunResult(args: FinalizeRunArgs): Promise<SingleResult> {
1827
1929
  };
1828
1930
  }
1829
1931
 
1932
+ /**
1933
+ * Settle a subagent's registry lifecycle after a run: terminal teardown for
1934
+ * hard aborts, unregister for one-shot helpers, park for isolated runs, and
1935
+ * idle + lifecycle adoption for kept-alive agents. A soft-budget abort on a
1936
+ * kept-alive, revivable agent is treated as a self-inflicted stop rather than
1937
+ * a kill — the agent stays interrogable and resumable (irc wake / revival).
1938
+ */
1830
1939
  export async function finalizeSubagentLifecycle(args: {
1831
1940
  id: string;
1832
1941
  session: AgentSession;
1833
1942
  aborted: boolean;
1943
+ /** Which watchdog (if any) requested the abort; decides revivability. */
1944
+ abortKind?: AbortReason;
1834
1945
  keepAlive: boolean;
1835
1946
  isolated: boolean;
1836
1947
  agentIdleTtlMs: number;
@@ -1845,8 +1956,12 @@ export async function finalizeSubagentLifecycle(args: {
1845
1956
  }
1846
1957
  };
1847
1958
 
1848
- if (args.aborted) {
1849
- // Hard abort (caller signal / wall-clock / budget): terminal teardown.
1959
+ // A budget abort leaves a consistent session with its transcript on disk;
1960
+ // caller signals, wall-clock timeouts (possible stream hang), and internal
1961
+ // terminations are genuine kills and stay terminal.
1962
+ const resumableAbort =
1963
+ args.abortKind === "budget" && args.keepAlive && !args.isolated && args.reviveSession !== null;
1964
+ if (args.aborted && !resumableAbort) {
1850
1965
  registry.setStatus(args.id, "aborted");
1851
1966
  await disposeSession();
1852
1967
  return;
@@ -1974,7 +2089,6 @@ export async function runSubagentFollowUpTurn(options: FollowUpTurnOptions): Pro
1974
2089
  id,
1975
2090
  agent,
1976
2091
  task: message,
1977
- description: options.description,
1978
2092
  signal,
1979
2093
  artifactsDir: options.artifactsDir,
1980
2094
  eventBus: options.eventBus,
@@ -2046,12 +2160,6 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
2046
2160
  options.parentServiceTier,
2047
2161
  );
2048
2162
  const maxRecursionDepth = settings.get("task.maxRecursionDepth") ?? 2;
2049
- // Tailored specialist identity for this spawn. `subagentRole` is the full
2050
- // (trimmed) role text fed to the system-prompt preamble; `subagentDisplayName`
2051
- // is the label-normalized form the registry/roster show, falling back to the
2052
- // agent type name when no role was given.
2053
- const subagentRole = options.role?.trim() || undefined;
2054
- const subagentDisplayName = resolveSubagentDisplayName(options.role, agent.name);
2055
2163
  const maxRuntimeMs = Math.max(
2056
2164
  0,
2057
2165
  Math.trunc(Number(options.maxRuntimeMs ?? settings.get("task.maxRuntimeMs") ?? 0) || 0),
@@ -2117,6 +2225,8 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
2117
2225
  task,
2118
2226
  assignment,
2119
2227
  description: options.description,
2228
+ modelRegistry: options.modelRegistry,
2229
+ settings,
2120
2230
  modelOverride,
2121
2231
  signal,
2122
2232
  onProgress,
@@ -2252,7 +2362,11 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
2252
2362
  ? formatModelSelectorValue(formatModelStringWithRouting(model), resolvedThinkingLevel)
2253
2363
  : formatModelStringWithRouting(model);
2254
2364
  }
2255
- const effectiveThinkingLevel = thinkingLevel ?? resolvedThinkingLevel;
2365
+ // Precedence: explicit `:level` suffix on the resolved model pattern >
2366
+ // agent-definition default (e.g. task's `auto`) > pattern-derived level.
2367
+ const effectiveThinkingLevel = explicitThinkingLevel
2368
+ ? resolvedThinkingLevel
2369
+ : (thinkingLevel ?? resolvedThinkingLevel);
2256
2370
  resolvedAt = performance.now();
2257
2371
 
2258
2372
  const effectiveCwd = worktree ?? cwd;
@@ -2280,8 +2394,8 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
2280
2394
  const subagentAgentIdentity: AgentIdentity | undefined = options.parentTelemetry
2281
2395
  ? {
2282
2396
  id,
2283
- name: subagentDisplayName,
2284
- description: subagentRole ? oneLineLabel(subagentRole) : agent.description,
2397
+ name: agent.name,
2398
+ description: agent.description,
2285
2399
  }
2286
2400
  : undefined;
2287
2401
  const subagentTelemetry: AgentTelemetryConfig | undefined =
@@ -2337,7 +2451,6 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
2337
2451
  systemPrompt: defaultPrompt => {
2338
2452
  const subagentPrompt = prompt.render(subagentSystemPromptTemplate, {
2339
2453
  agent: agent.systemPrompt,
2340
- role: subagentRole ? oneLineLabel(subagentRole) : "",
2341
2454
  context: options.context?.trim() ?? "",
2342
2455
  planReference: options.planReference?.content ?? "",
2343
2456
  planReferencePath: options.planReference?.path ?? "",
@@ -2360,7 +2473,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
2360
2473
  parentTaskPrefix: id,
2361
2474
  parentAgentId: options.parentAgentId,
2362
2475
  agentId: id,
2363
- agentDisplayName: subagentDisplayName,
2476
+ agentDisplayName: agent.name,
2364
2477
  enableLsp: lspEnabled,
2365
2478
  skipPythonPreflight,
2366
2479
  enableMCP,
@@ -2571,6 +2684,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
2571
2684
  id,
2572
2685
  session,
2573
2686
  aborted,
2687
+ abortKind: monitor.abortKind(),
2574
2688
  keepAlive: options.keepAlive !== false,
2575
2689
  isolated: worktree !== undefined,
2576
2690
  agentIdleTtlMs,
@@ -2629,7 +2743,6 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
2629
2743
  agent,
2630
2744
  task,
2631
2745
  assignment,
2632
- description: options.description,
2633
2746
  modelOverride,
2634
2747
  outputSchema,
2635
2748
  signal,