pi-long-task 0.6.0 → 0.7.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.
@@ -32,6 +32,24 @@ import {
32
32
  type PlanRevisionRelevantResult,
33
33
  } from "./plan_revision_generation.ts";
34
34
  import { taskSemanticFingerprint, type PlanTaskState } from "./plan_revision.ts";
35
+ import {
36
+ DEFAULT_PLANNER_THINKING_LEVEL,
37
+ DEFAULT_PLANNER_TIMEOUT_MS,
38
+ resolvePlannerBudget,
39
+ resolvePlannerGracefulShutdownMs,
40
+ resolvePlannerTimeoutMs,
41
+ type PlannerBudget,
42
+ } from "./planner_config.ts";
43
+ import {
44
+ createPlannerActiveProgress,
45
+ createPlannerGraceProgress,
46
+ createPlannerStartedProgress,
47
+ formatFriendlyDuration,
48
+ plannerProgressCheckpoints,
49
+ type PlannerProgressEvent,
50
+ type PlannerProgressHandler,
51
+ type PlannerProgressState,
52
+ } from "./planner_progress.ts";
35
53
  import {
36
54
  PersistentTodoPlanStore,
37
55
  planTaskReference,
@@ -53,6 +71,7 @@ import {
53
71
  import { buildTaskProgressModel, type TaskProgressModel, type TaskProgressStatus } from "./task_progress.ts";
54
72
  import {
55
73
  applyGoalInstructionsToTodoMarkdown,
74
+ applyWorkerCapabilityConstraintsToTodoMarkdown,
56
75
  buildTodoCreationPrompt,
57
76
  buildTodoRepairPrompt,
58
77
  extractAndValidateTodoMarkdown,
@@ -61,6 +80,7 @@ import {
61
80
  validateTodoMarkdown,
62
81
  } from "./todo_generator.ts";
63
82
  import { parseTasks, todoGlobalInstructions, type Task } from "./todo_parser.ts";
83
+ import { detectUnavailableWorkerCapabilities, type WorkerCapabilityWarning } from "./worker_capabilities.ts";
64
84
  import {
65
85
  buildWorkerSessionCreationFailureOutcome,
66
86
  createIsolatedWorkerSession,
@@ -84,11 +104,11 @@ export type { CoordinatorStatus } from "./types.ts";
84
104
  export const DEFAULT_COORDINATOR_OPTIONS = {
85
105
  maxAttemptsPerTask: 3,
86
106
  taskTimeoutMs: 900_000,
87
- todoTimeoutMs: 300_000,
107
+ todoTimeoutMs: DEFAULT_PLANNER_TIMEOUT_MS,
88
108
  todoGracefulShutdownMs: 15_000,
89
109
  maxBashTimeoutMs: 300_000,
90
110
  taskThinking: "high",
91
- todoThinking: "xhigh",
111
+ todoThinking: DEFAULT_PLANNER_THINKING_LEVEL,
92
112
  workerSessionReuse: DEFAULT_WORKER_SESSION_REUSE_ENABLED,
93
113
  workerSessionReuseContextThresholdPercent: DEFAULT_WORKER_SESSION_REUSE_CONTEXT_THRESHOLD_PERCENT,
94
114
  networkRecovery: DEFAULT_NETWORK_RECOVERY_CONFIG,
@@ -96,6 +116,7 @@ export const DEFAULT_COORDINATOR_OPTIONS = {
96
116
 
97
117
  export type WorkerRunner = (options: RunWorkerTaskOptions) => Promise<SessionOutcome>;
98
118
  export type CoordinatorProgressPhase =
119
+ | "capability_warning"
99
120
  | "planning"
100
121
  | "planned"
101
122
  | "task_start"
@@ -108,14 +129,30 @@ export type CoordinatorProgressPhase =
108
129
  | "task_obsolete"
109
130
  | "complete";
110
131
 
111
- export type PlannerDiagnosticKind = "timeout" | "abort" | "invalid_output" | "repair_attempt" | "failure";
132
+ export type PlannerDiagnosticKind =
133
+ | "timeout"
134
+ | "cancelled"
135
+ /** @deprecated Planner cancellation is now reported as `cancelled`. */
136
+ | "abort"
137
+ | "network_recovery"
138
+ | "network_failure"
139
+ | "invalid_output"
140
+ | "repair_attempt"
141
+ | "failure";
112
142
 
113
143
  export interface PlannerDiagnostic {
114
144
  kind: PlannerDiagnosticKind;
115
145
  message: string;
146
+ /** Present whenever output presence is known; partial content itself is deliberately omitted. */
147
+ partialOutputObserved?: boolean;
116
148
  diagnostics?: string[];
117
149
  sessionFile?: string;
118
150
  sessionId?: string;
151
+ /** Network lifecycle data is separate from timeout/cancellation classification. */
152
+ networkRecoveryEvent?: NetworkRecoveryEventType;
153
+ networkFailureReason?: string;
154
+ networkRetryCount?: number;
155
+ networkOutageElapsedMs?: number;
119
156
  }
120
157
 
121
158
  export type PlannerDiagnosticHandler = (diagnostic: PlannerDiagnostic) => void;
@@ -158,8 +195,18 @@ export interface CoordinatorProgressUpdate {
158
195
  taskProgress?: TaskProgressModel;
159
196
  plannerDiagnostic?: PlannerDiagnosticKind;
160
197
  plannerDiagnostics?: string[];
198
+ plannerPartialOutputObserved?: boolean;
161
199
  plannerSessionFile?: string;
162
200
  plannerSessionId?: string;
201
+ /** Deterministic deadline selection used by planner calls in this run. */
202
+ plannerBudget?: Readonly<PlannerBudget>;
203
+ /** Human-facing state with exact millisecond values retained for integrations. */
204
+ plannerProgressState?: PlannerProgressState;
205
+ plannerElapsedMs?: number;
206
+ plannerRemainingMs?: number;
207
+ plannerGracePeriodMs?: number;
208
+ plannerGraceRemainingMs?: number;
209
+ capabilityWarning?: Readonly<WorkerCapabilityWarning>;
163
210
  workerSessionEvent?: WorkerSessionDiagnostic["event"];
164
211
  workerSessionReason?: string;
165
212
  workerSessionContextUsagePercent?: number;
@@ -170,6 +217,10 @@ export interface CoordinatorProgressUpdate {
170
217
  networkNextRetryAtMs?: number;
171
218
  networkNextRetryInMs?: number;
172
219
  networkFailureReason?: string;
220
+ /** Identifies whether recovery belongs to planning or worker execution. */
221
+ networkOperation?: "planner" | "worker";
222
+ /** Planner recovery never mutates the configured per-attempt deadline. */
223
+ plannerDeadlinePolicy?: "per_attempt_excludes_network_wait";
173
224
  }
174
225
 
175
226
  export type CoordinatorProgressHandler = (update: CoordinatorProgressUpdate) => void;
@@ -210,20 +261,31 @@ export interface TodoPlannerOptions {
210
261
  inputText: string;
211
262
  cwd: string;
212
263
  runDir: string;
213
- thinkingLevel: string;
264
+ /** Defaults to the planner-only balanced level; explicit values are forwarded unchanged. */
265
+ thinkingLevel?: string;
214
266
  model?: unknown;
215
267
  abortSignal?: AbortSignal;
216
268
  timeoutMs?: number;
217
269
  gracefulShutdownMs?: number;
270
+ /** Structured record of explicit/default/adaptive deadline selection. */
271
+ plannerBudget?: Readonly<PlannerBudget>;
218
272
  sessionFactory?: WorkerSessionFactory;
219
273
  onDiagnostic?: PlannerDiagnosticHandler;
274
+ /** Shared human-readable timing events for CLI, TUI, and headless integrations. */
275
+ onProgress?: PlannerProgressHandler;
220
276
  goal?: string;
221
277
  /** Exact prompt for revision planners; bypasses the initial TODO-creation wrapper. */
222
278
  plannerPrompt?: string;
223
279
  /** Structured revision context supplied alongside plannerPrompt. */
224
280
  planRevision?: Readonly<PlanRevisionRequest>;
225
- /** Normalized coordinator recovery policy; network wait is excluded from operation timeouts. */
281
+ /**
282
+ * Normalized coordinator recovery policy. Recovery wait is accounted on its
283
+ * own outage clock; every replay receives the same configured per-attempt
284
+ * planner deadline, so recovery settings never replace that deadline.
285
+ */
226
286
  networkRecovery?: Readonly<NetworkRecoveryConfig>;
287
+ /** Run-level constraints derived from capabilities unavailable to isolated workers. */
288
+ capabilityConstraints?: readonly string[];
227
289
  }
228
290
 
229
291
  export interface TaskAttemptSummary {
@@ -276,6 +338,10 @@ export interface CoordinatorResult {
276
338
  workerUsageTotal?: WorkerUsageTotals;
277
339
  /** Additive lifecycle counters for adaptive worker-session reuse. */
278
340
  workerSessionMetrics?: WorkerSessionMetrics;
341
+ /** Deterministic deadline selection used by planner calls in this run. */
342
+ plannerBudget?: Readonly<PlannerBudget>;
343
+ /** Explicit, non-fatal warnings for requested capabilities unavailable to isolated workers. */
344
+ capabilityWarnings?: readonly WorkerCapabilityWarning[];
279
345
  commit: boolean;
280
346
  goal?: string;
281
347
  error?: string;
@@ -307,6 +373,7 @@ interface RuntimeOptions {
307
373
  networkRecovery: NetworkRecoveryConfig;
308
374
  todoTimeoutMs: number;
309
375
  todoGracefulShutdownMs: number;
376
+ plannerBudget: PlannerBudget;
310
377
  workerRunner: WorkerRunner;
311
378
  useRetainedWorkerLifecycle: boolean;
312
379
  todoPlanner: TodoPlanner;
@@ -320,6 +387,7 @@ interface RuntimeOptions {
320
387
  workerTextByWorker: Map<string, string>;
321
388
  workerTextPublishedLengthByWorker: Map<string, number>;
322
389
  plannerDiagnostics: PlannerDiagnostic[];
390
+ capabilityWarnings: WorkerCapabilityWarning[];
323
391
  workerSessionMetrics: WorkerSessionMetrics;
324
392
  steeringQueue?: SerializedSteeringQueue;
325
393
  onPlanRevisionAccepted?: (revision: GeneratedPlanRevision) => void | Promise<void>;
@@ -327,7 +395,12 @@ interface RuntimeOptions {
327
395
  lastProgress?: CoordinatorProgressUpdate;
328
396
  progressClosed: boolean;
329
397
  networkRecoverySequence: number;
330
- activeNetworkRecoveries: Map<number, NetworkRecoveryEvent>;
398
+ activeNetworkRecoveries: Map<number, ActiveNetworkRecovery>;
399
+ }
400
+
401
+ interface ActiveNetworkRecovery {
402
+ event: NetworkRecoveryEvent;
403
+ operation: "planner" | "worker";
331
404
  }
332
405
 
333
406
  type RetainedWorkerReuseScope = "sequential_task" | "partial_continuation";
@@ -956,7 +1029,7 @@ export async function runCoordinator(options: RunCoordinatorOptions): Promise<Co
956
1029
  const commits: CoordinatorCommitSummary[] = [];
957
1030
 
958
1031
  await mkdir(runtime.runDir, { recursive: true });
959
- await writeFile(runtime.taskResultPath, initialTaskResultMarkdown(runtime.runId), "utf8");
1032
+ await writeFile(runtime.taskResultPath, initialTaskResultMarkdown(runtime.runId, runtime.capabilityWarnings), "utf8");
960
1033
  let planningComplete = false;
961
1034
  let latestTodoMarkdown: string | undefined;
962
1035
  let latestTasks: Task[] = [];
@@ -972,7 +1045,14 @@ export async function runCoordinator(options: RunCoordinatorOptions): Promise<Co
972
1045
  const protectedDirtyPathsByTask = new Map<string, Set<string>>();
973
1046
 
974
1047
  try {
975
- emitProgress(runtime, "Creating TODO plan...", { phase: "planning" });
1048
+ for (const warning of runtime.capabilityWarnings) {
1049
+ emitProgress(runtime, warning.message, {
1050
+ phase: "capability_warning",
1051
+ status: "warning",
1052
+ capabilityWarning: warning,
1053
+ });
1054
+ }
1055
+ emitPlannerProgress(runtime, createPlannerStartedProgress(runtime.plannerBudget, runtime.todoGracefulShutdownMs));
976
1056
  let todoMarkdown = await generateOrNormalizeTodoMarkdown(inputText, runtime);
977
1057
  validateTodoMarkdown(todoMarkdown);
978
1058
  planningComplete = true;
@@ -1374,6 +1454,8 @@ export async function runCoordinator(options: RunCoordinatorOptions): Promise<Co
1374
1454
  workerCostTotal: runtime.workerCostState.total,
1375
1455
  workerUsageTotal: aggregateWorkerUsage(outcomes),
1376
1456
  workerSessionMetrics: snapshotWorkerSessionMetrics(runtime.workerSessionMetrics),
1457
+ plannerBudget: runtime.plannerBudget,
1458
+ capabilityWarnings: runtime.capabilityWarnings,
1377
1459
  commit: options.commit,
1378
1460
  goal: runtime.goal,
1379
1461
  error: failure,
@@ -1388,10 +1470,12 @@ export async function runCoordinator(options: RunCoordinatorOptions): Promise<Co
1388
1470
  return result;
1389
1471
  } catch (error) {
1390
1472
  const message = errorMessage(error);
1391
- if (!planningComplete) {
1473
+ if (!planningComplete && !hasTerminalPlannerDiagnostic(runtime.plannerDiagnostics)) {
1392
1474
  recordPlannerDiagnostic(runtime, {
1393
- kind: "failure",
1394
- message: `TODO planning failed: ${message}`,
1475
+ kind: runtime.abortSignal?.aborted ? "cancelled" : "failure",
1476
+ message: runtime.abortSignal?.aborted
1477
+ ? `TODO planning cancelled: ${abortSignalReason(runtime.abortSignal)}`
1478
+ : `TODO planning failed: ${message}`,
1395
1479
  });
1396
1480
  }
1397
1481
  const resultError = !planningComplete
@@ -1457,6 +1541,8 @@ export async function runCoordinator(options: RunCoordinatorOptions): Promise<Co
1457
1541
  workerCostTotal: runtime.workerCostState.total,
1458
1542
  workerUsageTotal: aggregateWorkerUsage(outcomes),
1459
1543
  workerSessionMetrics: snapshotWorkerSessionMetrics(runtime.workerSessionMetrics),
1544
+ plannerBudget: runtime.plannerBudget,
1545
+ capabilityWarnings: runtime.capabilityWarnings,
1460
1546
  commit: options.commit,
1461
1547
  goal: runtime.goal,
1462
1548
  error: resultError,
@@ -1478,9 +1564,10 @@ export async function runCoordinator(options: RunCoordinatorOptions): Promise<Co
1478
1564
  }
1479
1565
 
1480
1566
  async function generateOrNormalizeTodoMarkdown(inputText: string, runtime: RuntimeOptions): Promise<string> {
1567
+ const capabilityConstraints = runtime.capabilityWarnings.map((warning) => warning.planningConstraint);
1481
1568
  const local = todoMarkdownFromString(inputText, runtime.goal);
1482
1569
  if (local) {
1483
- return local;
1570
+ return applyWorkerCapabilityConstraintsToTodoMarkdown(local, capabilityConstraints);
1484
1571
  }
1485
1572
 
1486
1573
  const plannerText = await requestTodoPlan(inputText, runtime);
@@ -1489,6 +1576,7 @@ async function generateOrNormalizeTodoMarkdown(inputText: string, runtime: Runti
1489
1576
  plannerText,
1490
1577
  (repairPrompt) => requestTodoPlan(repairPrompt, runtime),
1491
1578
  runtime.goal,
1579
+ capabilityConstraints,
1492
1580
  {
1493
1581
  onInvalidOutput: (validationError) =>
1494
1582
  recordPlannerDiagnostic(runtime, {
@@ -1507,7 +1595,8 @@ async function generateOrNormalizeTodoMarkdown(inputText: string, runtime: Runti
1507
1595
  }),
1508
1596
  },
1509
1597
  );
1510
- return applyGoalInstructionsToTodoMarkdown(planned, runtime.goal);
1598
+ const withGoal = applyGoalInstructionsToTodoMarkdown(planned, runtime.goal);
1599
+ return applyWorkerCapabilityConstraintsToTodoMarkdown(withGoal, capabilityConstraints);
1511
1600
  }
1512
1601
 
1513
1602
  interface TodoExtractionRepairHooks {
@@ -1521,6 +1610,7 @@ async function extractTodoMarkdownWithOneRepair(
1521
1610
  plannerText: string,
1522
1611
  requestRepair: (repairPrompt: string) => Promise<string>,
1523
1612
  goal?: string,
1613
+ capabilityConstraints: readonly string[] = [],
1524
1614
  hooks: TodoExtractionRepairHooks = {},
1525
1615
  ): Promise<string> {
1526
1616
  try {
@@ -1529,7 +1619,9 @@ async function extractTodoMarkdownWithOneRepair(
1529
1619
  const validationError = errorMessage(error);
1530
1620
  hooks.onInvalidOutput?.(validationError);
1531
1621
  hooks.onRepairAttempt?.(validationError);
1532
- const repairText = await requestRepair(buildTodoRepairPrompt(inputText, plannerText, validationError, goal));
1622
+ const repairText = await requestRepair(
1623
+ buildTodoRepairPrompt(inputText, plannerText, validationError, goal, capabilityConstraints),
1624
+ );
1533
1625
  try {
1534
1626
  return extractAndValidateTodoMarkdown(repairText);
1535
1627
  } catch (repairError) {
@@ -1553,9 +1645,16 @@ async function requestTodoPlan(inputText: string, runtime: RuntimeOptions): Prom
1553
1645
  abortSignal: runtime.abortSignal,
1554
1646
  timeoutMs: runtime.todoTimeoutMs,
1555
1647
  gracefulShutdownMs: runtime.todoGracefulShutdownMs,
1648
+ plannerBudget: runtime.plannerBudget,
1556
1649
  sessionFactory: runtime.todoSessionFactory,
1557
1650
  networkRecovery: runtime.networkRecovery,
1651
+ capabilityConstraints: runtime.capabilityWarnings.map((warning) => warning.planningConstraint),
1558
1652
  onDiagnostic: (diagnostic) => recordPlannerDiagnostic(runtime, diagnostic),
1653
+ onProgress: (event) => {
1654
+ if (event.state !== "started") {
1655
+ emitPlannerProgress(runtime, event);
1656
+ }
1657
+ },
1559
1658
  goal: runtime.goal,
1560
1659
  },
1561
1660
  runtime,
@@ -1567,8 +1666,10 @@ async function requestTodoPlan(inputText: string, runtime: RuntimeOptions): Prom
1567
1666
  * failed. The default planner disables tools and disposes every session before
1568
1667
  * rejecting, so each retry rotates unsafe conversation state while replaying
1569
1668
  * only the complete immutable planning context. Recovery owns no planner
1570
- * repair/attempt counter, and each fresh call retains the planner timeout;
1571
- * backoff remains governed solely by the separate outage deadline.
1669
+ * repair/attempt counter, and each fresh call retains the exact configured
1670
+ * per-attempt planner timeout. Network wait is intentionally excluded and is
1671
+ * governed solely by the separate outage deadline; recovery cannot reset,
1672
+ * extend, or replace the timeout attached to any individual planner call.
1572
1673
  */
1573
1674
  async function runPlannerOperationWithNetworkRecovery(
1574
1675
  plannerOptions: TodoPlannerOptions,
@@ -1577,6 +1678,8 @@ async function runPlannerOperationWithNetworkRecovery(
1577
1678
  const run = (recoverySignal?: AbortSignal) =>
1578
1679
  runtime.todoPlanner({
1579
1680
  ...plannerOptions,
1681
+ // The recovery signal carries cancellation/outage expiry only. It does
1682
+ // not alter timeoutMs, which remains authoritative for every replay.
1580
1683
  abortSignal: combineAbortSignals(plannerOptions.abortSignal, recoverySignal),
1581
1684
  });
1582
1685
 
@@ -1584,17 +1687,73 @@ async function runPlannerOperationWithNetworkRecovery(
1584
1687
  return await run();
1585
1688
  } catch (initialFailure) {
1586
1689
  const classification = classifyNetworkFailure(initialFailure);
1690
+ if (plannerOptions.abortSignal?.aborted || classification.reason === "cancelled") {
1691
+ recordPlannerCancellation(runtime, plannerOptions.abortSignal, initialFailure);
1692
+ throw plannerCancellationError(plannerOptions.abortSignal, initialFailure);
1693
+ }
1587
1694
  if (!runtime.networkRecovery.enabled || !classification.recoverable) {
1588
1695
  throw initialFailure;
1589
1696
  }
1590
- const recovered = await recoverNetworkOperation({
1591
- initialFailure,
1592
- config: runtime.networkRecovery,
1593
- signal: plannerOptions.abortSignal,
1594
- onEvent: createNetworkRecoveryProgressHandler(runtime),
1595
- retry: ({ signal }) => run(signal),
1596
- });
1597
- return recovered.value;
1697
+
1698
+ const publishRecovery = createNetworkRecoveryProgressHandler(runtime, "planner");
1699
+ let recoveryStarted = false;
1700
+ let lastRecoveryEvent: NetworkRecoveryEvent | undefined;
1701
+ const onRecoveryEvent = (event: NetworkRecoveryEvent) => {
1702
+ if (event.type !== "cleanup") lastRecoveryEvent = event;
1703
+ if (event.type === "outage_started" && !recoveryStarted) {
1704
+ recoveryStarted = true;
1705
+ recordPlannerDiagnostic(
1706
+ runtime,
1707
+ plannerNetworkDiagnostic(
1708
+ event,
1709
+ plannerOptions.timeoutMs,
1710
+ latestPlannerPartialOutput(runtime.plannerDiagnostics),
1711
+ ),
1712
+ );
1713
+ }
1714
+ publishRecovery(event);
1715
+ if (event.type === "recovered") {
1716
+ recordPlannerDiagnostic(
1717
+ runtime,
1718
+ plannerNetworkDiagnostic(
1719
+ event,
1720
+ plannerOptions.timeoutMs,
1721
+ latestPlannerPartialOutput(runtime.plannerDiagnostics),
1722
+ ),
1723
+ );
1724
+ }
1725
+ };
1726
+
1727
+ try {
1728
+ const recovered = await recoverNetworkOperation({
1729
+ initialFailure,
1730
+ config: runtime.networkRecovery,
1731
+ signal: plannerOptions.abortSignal,
1732
+ onEvent: onRecoveryEvent,
1733
+ retry: ({ signal }) => run(signal),
1734
+ });
1735
+ return recovered.value;
1736
+ } catch (recoveryFailure) {
1737
+ if (plannerOptions.abortSignal?.aborted) {
1738
+ recordPlannerCancellation(runtime, plannerOptions.abortSignal, recoveryFailure);
1739
+ throw plannerCancellationError(plannerOptions.abortSignal, recoveryFailure);
1740
+ } else if (!hasTerminalPlannerDiagnosticAfterLatestRecovery(runtime.plannerDiagnostics)) {
1741
+ const finalClassification = classifyNetworkFailure(recoveryFailure);
1742
+ recordPlannerDiagnostic(runtime, {
1743
+ kind: "network_failure",
1744
+ message: `TODO planner network recovery ended before planning completed: ${errorMessage(recoveryFailure)}`,
1745
+ partialOutputObserved: latestPlannerPartialOutput(runtime.plannerDiagnostics),
1746
+ networkRecoveryEvent:
1747
+ lastRecoveryEvent?.type === "outage_expired" || lastRecoveryEvent?.type === "failed"
1748
+ ? lastRecoveryEvent.type
1749
+ : "failed",
1750
+ networkFailureReason: lastRecoveryEvent?.state.lastFailure.reason ?? finalClassification.reason,
1751
+ networkRetryCount: lastRecoveryEvent?.state.retryCount,
1752
+ networkOutageElapsedMs: lastRecoveryEvent?.state.elapsedMs,
1753
+ });
1754
+ }
1755
+ throw recoveryFailure;
1756
+ }
1598
1757
  }
1599
1758
  }
1600
1759
 
@@ -1640,9 +1799,12 @@ async function generateSteeringPlanRevision(options: {
1640
1799
  abortSignal: options.runtime.abortSignal,
1641
1800
  timeoutMs: options.runtime.todoTimeoutMs,
1642
1801
  gracefulShutdownMs: options.runtime.todoGracefulShutdownMs,
1802
+ plannerBudget: options.runtime.plannerBudget,
1643
1803
  sessionFactory: options.runtime.todoSessionFactory,
1644
1804
  networkRecovery: options.runtime.networkRecovery,
1805
+ capabilityConstraints: options.runtime.capabilityWarnings.map((warning) => warning.planningConstraint),
1645
1806
  onDiagnostic: (diagnostic) => recordPlannerDiagnostic(options.runtime, diagnostic),
1807
+ onProgress: (event) => emitPlannerProgress(options.runtime, event),
1646
1808
  goal: options.runtime.goal,
1647
1809
  },
1648
1810
  options.runtime,
@@ -1751,16 +1913,41 @@ function relevantPlanRevisionResults(
1751
1913
  // Planner/worker lifecycle differences are audited in docs/planner-worker-lifecycle-audit.md;
1752
1914
  // keep this function's public contract stable while moving shared prompt guarding into a helper.
1753
1915
  export async function runTodoPlanner(options: TodoPlannerOptions): Promise<string> {
1916
+ const capabilityConstraints =
1917
+ options.capabilityConstraints ??
1918
+ capabilityWarningsForRequest(options.inputText, options.goal).map((warning) => warning.planningConstraint);
1919
+ const plannerBudget =
1920
+ options.plannerBudget ??
1921
+ resolvePlannerBudget({
1922
+ inputText: options.inputText,
1923
+ explicitTimeoutMs: options.timeoutMs,
1924
+ defaultTimeoutMs: DEFAULT_COORDINATOR_OPTIONS.todoTimeoutMs,
1925
+ });
1926
+ const timeoutMs = resolvePlannerTimeoutMs(options.timeoutMs, plannerBudget.timeoutMs);
1927
+ const gracefulShutdownMs = resolvePlannerGracefulShutdownMs(
1928
+ options.gracefulShutdownMs,
1929
+ DEFAULT_COORDINATOR_OPTIONS.todoGracefulShutdownMs,
1930
+ );
1931
+ const effectivePlannerBudget: PlannerBudget =
1932
+ timeoutMs === plannerBudget.timeoutMs
1933
+ ? plannerBudget
1934
+ : {
1935
+ ...plannerBudget,
1936
+ timeoutMs,
1937
+ extensionApplied: false,
1938
+ extensionMs: 0,
1939
+ source: "explicit",
1940
+ trigger: undefined,
1941
+ };
1942
+ notifyPlannerProgress(options.onProgress, createPlannerStartedProgress(effectivePlannerBudget, gracefulShutdownMs));
1754
1943
  const sessionFactory = options.sessionFactory ?? createIsolatedWorkerSession;
1755
1944
  const result = await sessionFactory({
1756
1945
  cwd: options.cwd,
1757
1946
  tools: [],
1758
1947
  model: options.model,
1759
- thinkingLevel: options.thinkingLevel,
1948
+ thinkingLevel: options.thinkingLevel ?? DEFAULT_PLANNER_THINKING_LEVEL,
1760
1949
  });
1761
1950
  const session = result.session;
1762
- const timeoutMs = positiveMilliseconds(options.timeoutMs, DEFAULT_COORDINATOR_OPTIONS.todoTimeoutMs);
1763
- const gracefulShutdownMs = options.gracefulShutdownMs ?? DEFAULT_COORDINATOR_OPTIONS.todoGracefulShutdownMs;
1764
1951
 
1765
1952
  let plannerMarkdown: string | undefined;
1766
1953
  let plannerError: unknown;
@@ -1768,12 +1955,14 @@ export async function runTodoPlanner(options: TodoPlannerOptions): Promise<strin
1768
1955
  try {
1769
1956
  const plannerText = await runTodoPlannerPrompt({
1770
1957
  session,
1771
- prompt: options.plannerPrompt ?? buildTodoCreationPrompt(options.inputText, options.goal),
1958
+ prompt: options.plannerPrompt ?? buildTodoCreationPrompt(options.inputText, options.goal, capabilityConstraints),
1772
1959
  abortSignal: options.abortSignal,
1773
1960
  timeoutMs,
1774
1961
  gracefulShutdownMs,
1775
1962
  diagnostics: result.diagnostics,
1776
1963
  onDiagnostic: options.onDiagnostic,
1964
+ plannerBudget: effectivePlannerBudget,
1965
+ onProgress: options.onProgress,
1777
1966
  });
1778
1967
 
1779
1968
  plannerMarkdown = await extractTodoMarkdownWithOneRepair(
@@ -1788,8 +1977,11 @@ export async function runTodoPlanner(options: TodoPlannerOptions): Promise<strin
1788
1977
  gracefulShutdownMs,
1789
1978
  diagnostics: result.diagnostics,
1790
1979
  onDiagnostic: options.onDiagnostic,
1980
+ plannerBudget: effectivePlannerBudget,
1981
+ onProgress: options.onProgress,
1791
1982
  }),
1792
1983
  options.goal,
1984
+ capabilityConstraints,
1793
1985
  {
1794
1986
  onInvalidOutput: (validationError) =>
1795
1987
  options.onDiagnostic?.({
@@ -1833,7 +2025,8 @@ export async function runTodoPlanner(options: TodoPlannerOptions): Promise<strin
1833
2025
  if (!plannerMarkdown) {
1834
2026
  throw new TodoGenerationError("TODO planner did not return valid TODO markdown.");
1835
2027
  }
1836
- return applyGoalInstructionsToTodoMarkdown(plannerMarkdown, options.goal);
2028
+ const withGoal = applyGoalInstructionsToTodoMarkdown(plannerMarkdown, options.goal);
2029
+ return applyWorkerCapabilityConstraintsToTodoMarkdown(withGoal, capabilityConstraints);
1837
2030
  }
1838
2031
 
1839
2032
  async function runTodoPlannerPrompt(options: {
@@ -1844,6 +2037,8 @@ async function runTodoPlannerPrompt(options: {
1844
2037
  gracefulShutdownMs: number;
1845
2038
  diagnostics?: string[];
1846
2039
  onDiagnostic?: PlannerDiagnosticHandler;
2040
+ plannerBudget: Readonly<PlannerBudget>;
2041
+ onProgress?: PlannerProgressHandler;
1847
2042
  }): Promise<string> {
1848
2043
  const promptResult = await runGuardedSessionPrompt({
1849
2044
  session: options.session,
@@ -1853,17 +2048,47 @@ async function runTodoPlannerPrompt(options: {
1853
2048
  gracefulShutdownMs: options.gracefulShutdownMs,
1854
2049
  gracefulShutdownPrompt: buildTodoPlanningShutdownMessage(),
1855
2050
  diagnostics: options.diagnostics,
2051
+ progressCheckpointsMs: plannerProgressCheckpoints(options.timeoutMs),
2052
+ onProgressCheckpoint: (elapsedMs) =>
2053
+ notifyPlannerProgress(
2054
+ options.onProgress,
2055
+ createPlannerActiveProgress(options.plannerBudget, options.gracefulShutdownMs, elapsedMs),
2056
+ ),
2057
+ onGracePeriodStart: (gracePeriodMs) =>
2058
+ notifyPlannerProgress(options.onProgress, createPlannerGraceProgress(options.plannerBudget, gracePeriodMs)),
1856
2059
  dispose: false,
1857
2060
  });
1858
2061
 
2062
+ // Caller cancellation wins even when it arrives during grace. A hard abort
2063
+ // caused by grace expiry has cancelled=false and remains a timeout.
2064
+ if (promptResult.cancelled) {
2065
+ const outputState = promptResult.outputObserved
2066
+ ? "partial output observed; content omitted"
2067
+ : "no planner output observed";
2068
+ const message = `TODO planner cancelled (${outputState}): ${promptResult.error ?? "caller cancellation"}`;
2069
+ options.onDiagnostic?.(plannerPromptDiagnostic("cancelled", message, promptResult));
2070
+ throw new TodoGenerationError(message);
2071
+ }
1859
2072
  if (promptResult.timedOut) {
1860
- const message = `TODO planner timed out: ${promptResult.error ?? "time budget exceeded"}`;
2073
+ if (
2074
+ promptResult.completedDuringGrace &&
2075
+ promptResult.outputObserved &&
2076
+ !promptResult.error &&
2077
+ isSafeCompletedTodoPlannerOutput(promptResult.assistantText)
2078
+ ) {
2079
+ return promptResult.assistantText;
2080
+ }
2081
+
2082
+ const outputState = promptResult.outputObserved
2083
+ ? "partial output observed; content omitted"
2084
+ : "no planner output observed";
2085
+ const message = `TODO planner timed out (${outputState}): ${promptResult.error ?? "time budget exceeded"}`;
1861
2086
  options.onDiagnostic?.(plannerPromptDiagnostic("timeout", message, promptResult));
1862
2087
  throw new TodoGenerationError(message);
1863
2088
  }
1864
2089
  if (promptResult.aborted) {
1865
- const message = `TODO planner aborted: ${promptResult.error ?? "outer abort signal"}`;
1866
- options.onDiagnostic?.(plannerPromptDiagnostic("abort", message, promptResult));
2090
+ const message = `TODO planner stopped: ${promptResult.error ?? "session abort"}`;
2091
+ options.onDiagnostic?.(plannerPromptDiagnostic("failure", message, promptResult));
1867
2092
  throw new TodoGenerationError(message);
1868
2093
  }
1869
2094
  if (promptResult.error) {
@@ -1883,9 +2108,10 @@ async function runTodoPlannerPrompt(options: {
1883
2108
  }
1884
2109
 
1885
2110
  function plannerPromptDiagnostic(
1886
- kind: Extract<PlannerDiagnosticKind, "timeout" | "abort" | "failure">,
2111
+ kind: Extract<PlannerDiagnosticKind, "timeout" | "cancelled" | "abort" | "failure">,
1887
2112
  message: string,
1888
2113
  promptResult: {
2114
+ outputObserved: boolean;
1889
2115
  diagnostics: string[];
1890
2116
  sessionFile?: string;
1891
2117
  sessionId?: string;
@@ -1894,12 +2120,25 @@ function plannerPromptDiagnostic(
1894
2120
  return {
1895
2121
  kind,
1896
2122
  message,
2123
+ partialOutputObserved: promptResult.outputObserved,
1897
2124
  diagnostics: promptResult.diagnostics,
1898
2125
  sessionFile: promptResult.sessionFile,
1899
2126
  sessionId: promptResult.sessionId,
1900
2127
  };
1901
2128
  }
1902
2129
 
2130
+ function isSafeCompletedTodoPlannerOutput(text: string): boolean {
2131
+ if (!text.trim()) {
2132
+ return false;
2133
+ }
2134
+ try {
2135
+ extractAndValidateTodoMarkdown(text);
2136
+ return true;
2137
+ } catch {
2138
+ return false;
2139
+ }
2140
+ }
2141
+
1903
2142
  function buildTodoPlanningShutdownMessage(): string {
1904
2143
  return `Pi Long Task notice: TODO planning has reached its time budget.
1905
2144
  Return the best valid Pi Long Task TODO markdown you can produce now, or stop if that is not possible.`;
@@ -1912,8 +2151,13 @@ function buildRuntimeOptions(options: RunCoordinatorOptions): RuntimeOptions {
1912
2151
  const parsedWorkerConfig = parseWorkerRuntimeConfig(options.inputText ?? "");
1913
2152
  const configuredAttempts = options.maxAttemptsPerTask ?? parsedWorkerConfig.maxAttemptsPerTask;
1914
2153
  const configuredTaskTimeoutMs = options.taskTimeoutMs ?? parsedWorkerConfig.taskTimeoutMs;
1915
- const configuredTodoTimeoutMs = options.todoTimeoutMs;
1916
- const configuredTodoGracefulShutdownMs = options.todoGracefulShutdownMs;
2154
+ const configuredTodoTimeoutMs = options.todoTimeoutMs ?? parsedWorkerConfig.todoTimeoutMs;
2155
+ const configuredTodoGracefulShutdownMs = options.todoGracefulShutdownMs ?? parsedWorkerConfig.todoGracefulShutdownMs;
2156
+ const plannerBudget = resolvePlannerBudget({
2157
+ inputText: coordinatorInputText(options),
2158
+ explicitTimeoutMs: configuredTodoTimeoutMs,
2159
+ defaultTimeoutMs: DEFAULT_COORDINATOR_OPTIONS.todoTimeoutMs,
2160
+ });
1917
2161
  const configuredMaxBashTimeoutMs = options.maxBashTimeoutMs ?? parsedWorkerConfig.maxBashTimeoutMs;
1918
2162
  const workerModelName = options.workerModelName ?? parsedWorkerConfig.modelName;
1919
2163
  const workerModel = workerModelName ? undefined : options.workerModel;
@@ -1927,6 +2171,7 @@ function buildRuntimeOptions(options: RunCoordinatorOptions): RuntimeOptions {
1927
2171
  ...parsedWorkerConfig.networkRecovery,
1928
2172
  ...options.networkRecovery,
1929
2173
  });
2174
+ const capabilityWarnings = capabilityWarningsForRequest(options.inputText, options.goal);
1930
2175
 
1931
2176
  return {
1932
2177
  cwd,
@@ -1936,11 +2181,12 @@ function buildRuntimeOptions(options: RunCoordinatorOptions): RuntimeOptions {
1936
2181
  taskResultPath: path.join(runDir, "TASK_RESULT.md"),
1937
2182
  maxAttemptsPerTask: positiveInteger(configuredAttempts, DEFAULT_COORDINATOR_OPTIONS.maxAttemptsPerTask),
1938
2183
  taskTimeoutSeconds: positiveMilliseconds(configuredTaskTimeoutMs, DEFAULT_COORDINATOR_OPTIONS.taskTimeoutMs) / 1000,
1939
- todoTimeoutMs: positiveMilliseconds(configuredTodoTimeoutMs, DEFAULT_COORDINATOR_OPTIONS.todoTimeoutMs),
1940
- todoGracefulShutdownMs: positiveMilliseconds(
2184
+ todoTimeoutMs: plannerBudget.timeoutMs,
2185
+ todoGracefulShutdownMs: resolvePlannerGracefulShutdownMs(
1941
2186
  configuredTodoGracefulShutdownMs,
1942
2187
  DEFAULT_COORDINATOR_OPTIONS.todoGracefulShutdownMs,
1943
2188
  ),
2189
+ plannerBudget,
1944
2190
  maxBashTimeoutSeconds:
1945
2191
  positiveMilliseconds(configuredMaxBashTimeoutMs, DEFAULT_COORDINATOR_OPTIONS.maxBashTimeoutMs) / 1000,
1946
2192
  workerModel,
@@ -1964,6 +2210,7 @@ function buildRuntimeOptions(options: RunCoordinatorOptions): RuntimeOptions {
1964
2210
  workerTextByWorker: new Map(),
1965
2211
  workerTextPublishedLengthByWorker: new Map(),
1966
2212
  plannerDiagnostics: [],
2213
+ capabilityWarnings,
1967
2214
  workerSessionMetrics: createWorkerSessionMetrics(),
1968
2215
  steeringQueue: options.steeringQueue,
1969
2216
  onPlanRevisionAccepted: options.onPlanRevisionAccepted,
@@ -1974,6 +2221,23 @@ function buildRuntimeOptions(options: RunCoordinatorOptions): RuntimeOptions {
1974
2221
  };
1975
2222
  }
1976
2223
 
2224
+ function emitPlannerProgress(runtime: RuntimeOptions, event: Readonly<PlannerProgressEvent>): void {
2225
+ emitProgress(runtime, event.message, {
2226
+ phase: "planning",
2227
+ activeStatus: event.message,
2228
+ plannerBudget: event.budget,
2229
+ plannerProgressState: event.state,
2230
+ plannerElapsedMs: event.elapsedMs,
2231
+ plannerRemainingMs: event.remainingMs,
2232
+ plannerGracePeriodMs: event.gracePeriodMs,
2233
+ ...(event.graceRemainingMs === undefined ? {} : { plannerGraceRemainingMs: event.graceRemainingMs }),
2234
+ });
2235
+ }
2236
+
2237
+ function notifyPlannerProgress(handler: PlannerProgressHandler | undefined, event: PlannerProgressEvent): void {
2238
+ handler?.(event);
2239
+ }
2240
+
1977
2241
  function emitProgress(
1978
2242
  runtime: RuntimeOptions,
1979
2243
  message: string,
@@ -1992,7 +2256,7 @@ function emitProgress(
1992
2256
  runtime.lastProgress = progress;
1993
2257
  const activeRecovery = latestNetworkRecovery(runtime.activeNetworkRecoveries);
1994
2258
  if (activeRecovery) {
1995
- publishNetworkRecoveryProgress(runtime, activeRecovery);
2259
+ publishNetworkRecoveryProgress(runtime, activeRecovery.event, activeRecovery.operation);
1996
2260
  } else {
1997
2261
  runtime.onProgress?.(progress);
1998
2262
  }
@@ -2005,7 +2269,10 @@ function emitProgress(
2005
2269
  * status path. Operation IDs prevent an older concurrent recovery from
2006
2270
  * repainting a newer outage or completion.
2007
2271
  */
2008
- function createNetworkRecoveryProgressHandler(runtime: RuntimeOptions): (event: NetworkRecoveryEvent) => void {
2272
+ function createNetworkRecoveryProgressHandler(
2273
+ runtime: RuntimeOptions,
2274
+ operation: "planner" | "worker" = "worker",
2275
+ ): (event: NetworkRecoveryEvent) => void {
2009
2276
  const operationId = ++runtime.networkRecoverySequence;
2010
2277
  let cleaned = false;
2011
2278
 
@@ -2024,7 +2291,7 @@ function createNetworkRecoveryProgressHandler(runtime: RuntimeOptions): (event:
2024
2291
  if (event.type === "recovered") {
2025
2292
  const active = latestNetworkRecovery(runtime.activeNetworkRecoveries);
2026
2293
  if (active) {
2027
- publishNetworkRecoveryProgress(runtime, active);
2294
+ publishNetworkRecoveryProgress(runtime, active.event, active.operation);
2028
2295
  } else if (runtime.lastProgress) {
2029
2296
  runtime.onProgress?.({ ...runtime.lastProgress, workerCostTotal: runtime.workerCostState.total });
2030
2297
  }
@@ -2032,18 +2299,26 @@ function createNetworkRecoveryProgressHandler(runtime: RuntimeOptions): (event:
2032
2299
  return;
2033
2300
  }
2034
2301
 
2035
- runtime.activeNetworkRecoveries.set(operationId, event);
2302
+ runtime.activeNetworkRecoveries.set(operationId, { event, operation });
2036
2303
  if (operationId === latestNetworkRecoveryId(runtime.activeNetworkRecoveries)) {
2037
- publishNetworkRecoveryProgress(runtime, event);
2304
+ publishNetworkRecoveryProgress(runtime, event, operation);
2038
2305
  }
2039
2306
  };
2040
2307
  }
2041
2308
 
2042
- function publishNetworkRecoveryProgress(runtime: RuntimeOptions, event: NetworkRecoveryEvent): void {
2309
+ function publishNetworkRecoveryProgress(
2310
+ runtime: RuntimeOptions,
2311
+ event: NetworkRecoveryEvent,
2312
+ operation: "planner" | "worker" = "worker",
2313
+ ): void {
2043
2314
  if (runtime.progressClosed) return;
2044
2315
  const stable = runtime.lastProgress;
2045
2316
  const nowMs = event.state.outageStartedAtMs + event.state.elapsedMs;
2046
- const message = formatNetworkRecoveryStatus(event);
2317
+ const recoveryStatus = formatNetworkRecoveryStatus(event);
2318
+ const message =
2319
+ operation === "planner"
2320
+ ? `TODO planner network recovery: ${recoveryStatus} The ${formatFriendlyDuration(runtime.todoTimeoutMs)} planning deadline remains unchanged for each provider attempt.`
2321
+ : recoveryStatus;
2047
2322
  runtime.onProgress?.({
2048
2323
  message,
2049
2324
  phase: "network_wait",
@@ -2067,17 +2342,19 @@ function publishNetworkRecoveryProgress(runtime: RuntimeOptions, event: NetworkR
2067
2342
  networkNextRetryInMs:
2068
2343
  event.state.nextRetryAtMs === undefined ? undefined : Math.max(0, event.state.nextRetryAtMs - nowMs),
2069
2344
  networkFailureReason: event.state.lastFailure.reason,
2345
+ networkOperation: operation,
2346
+ ...(operation === "planner" ? { plannerDeadlinePolicy: "per_attempt_excludes_network_wait" as const } : {}),
2070
2347
  });
2071
2348
  }
2072
2349
 
2073
2350
  function latestNetworkRecovery(
2074
- recoveries: ReadonlyMap<number, NetworkRecoveryEvent>,
2075
- ): NetworkRecoveryEvent | undefined {
2351
+ recoveries: ReadonlyMap<number, ActiveNetworkRecovery>,
2352
+ ): ActiveNetworkRecovery | undefined {
2076
2353
  const id = latestNetworkRecoveryId(recoveries);
2077
2354
  return id === undefined ? undefined : recoveries.get(id);
2078
2355
  }
2079
2356
 
2080
- function latestNetworkRecoveryId(recoveries: ReadonlyMap<number, NetworkRecoveryEvent>): number | undefined {
2357
+ function latestNetworkRecoveryId(recoveries: ReadonlyMap<number, ActiveNetworkRecovery>): number | undefined {
2081
2358
  let latest: number | undefined;
2082
2359
  for (const id of recoveries.keys()) {
2083
2360
  if (latest === undefined || id > latest) latest = id;
@@ -2089,13 +2366,79 @@ function isTerminalNetworkRecoveryEvent(type: NetworkRecoveryEventType): boolean
2089
2366
  return type === "recovered" || type === "failed" || type === "cancelled" || type === "outage_expired";
2090
2367
  }
2091
2368
 
2369
+ function plannerNetworkDiagnostic(
2370
+ event: NetworkRecoveryEvent,
2371
+ timeoutMs: number | undefined,
2372
+ partialOutputObserved: boolean | undefined,
2373
+ ): PlannerDiagnostic {
2374
+ const recovered = event.type === "recovered";
2375
+ const budget = formatFriendlyDuration(timeoutMs ?? DEFAULT_COORDINATOR_OPTIONS.todoTimeoutMs);
2376
+ return {
2377
+ kind: "network_recovery",
2378
+ message: recovered
2379
+ ? `TODO planner network recovery succeeded after ${formatFriendlyDuration(event.state.elapsedMs)}; planning continues with its unchanged ${budget} per-attempt deadline.`
2380
+ : `TODO planner network recovery started (${event.state.lastFailure.reason}); its outage clock is separate and the ${budget} per-attempt planning deadline remains unchanged.`,
2381
+ partialOutputObserved,
2382
+ networkRecoveryEvent: event.type,
2383
+ networkFailureReason: event.state.lastFailure.reason,
2384
+ networkRetryCount: event.state.retryCount,
2385
+ networkOutageElapsedMs: event.state.elapsedMs,
2386
+ };
2387
+ }
2388
+
2389
+ function latestPlannerPartialOutput(diagnostics: readonly PlannerDiagnostic[]): boolean | undefined {
2390
+ return [...diagnostics].reverse().find((diagnostic) => diagnostic.partialOutputObserved !== undefined)
2391
+ ?.partialOutputObserved;
2392
+ }
2393
+
2394
+ function hasTerminalPlannerDiagnostic(diagnostics: readonly PlannerDiagnostic[]): boolean {
2395
+ const kind = diagnostics.at(-1)?.kind;
2396
+ return kind !== undefined && ["timeout", "cancelled", "abort", "network_failure", "failure"].includes(kind);
2397
+ }
2398
+
2399
+ function hasTerminalPlannerDiagnosticAfterLatestRecovery(diagnostics: readonly PlannerDiagnostic[]): boolean {
2400
+ let recoveryIndex = -1;
2401
+ for (let index = diagnostics.length - 1; index >= 0; index -= 1) {
2402
+ if (diagnostics[index]?.kind === "network_recovery") {
2403
+ recoveryIndex = index;
2404
+ break;
2405
+ }
2406
+ }
2407
+ return diagnostics
2408
+ .slice(recoveryIndex + 1)
2409
+ .some((diagnostic) => ["timeout", "cancelled", "abort", "network_failure"].includes(diagnostic.kind));
2410
+ }
2411
+
2412
+ function recordPlannerCancellation(runtime: RuntimeOptions, signal: AbortSignal | undefined, cause: unknown): void {
2413
+ if (runtime.plannerDiagnostics.some((diagnostic) => diagnostic.kind === "cancelled")) return;
2414
+ recordPlannerDiagnostic(runtime, {
2415
+ kind: "cancelled",
2416
+ message: `TODO planning cancelled: ${signal?.aborted ? abortSignalReason(signal) : errorMessage(cause)}`,
2417
+ partialOutputObserved: latestPlannerPartialOutput(runtime.plannerDiagnostics),
2418
+ });
2419
+ }
2420
+
2421
+ function abortSignalReason(signal: AbortSignal): string {
2422
+ return signal.reason === undefined ? "cancelled by caller" : errorMessage(signal.reason);
2423
+ }
2424
+
2425
+ function plannerCancellationError(signal: AbortSignal | undefined, cause: unknown): TodoGenerationError {
2426
+ const reason = signal?.aborted ? abortSignalReason(signal) : errorMessage(cause);
2427
+ return new TodoGenerationError(`TODO planning cancelled: ${reason}`, { cause });
2428
+ }
2429
+
2092
2430
  function recordPlannerDiagnostic(runtime: RuntimeOptions, diagnostic: PlannerDiagnostic): void {
2093
2431
  const normalized: PlannerDiagnostic = {
2094
2432
  kind: diagnostic.kind,
2095
2433
  message: diagnostic.message,
2434
+ partialOutputObserved: diagnostic.partialOutputObserved,
2096
2435
  diagnostics: diagnostic.diagnostics?.filter(Boolean),
2097
2436
  sessionFile: diagnostic.sessionFile,
2098
2437
  sessionId: diagnostic.sessionId,
2438
+ networkRecoveryEvent: diagnostic.networkRecoveryEvent,
2439
+ networkFailureReason: diagnostic.networkFailureReason,
2440
+ networkRetryCount: diagnostic.networkRetryCount,
2441
+ networkOutageElapsedMs: diagnostic.networkOutageElapsedMs,
2099
2442
  };
2100
2443
  const last = runtime.plannerDiagnostics.at(-1);
2101
2444
  if (last?.kind === normalized.kind && last.message === normalized.message) {
@@ -2105,11 +2448,22 @@ function recordPlannerDiagnostic(runtime: RuntimeOptions, diagnostic: PlannerDia
2105
2448
  emitProgress(runtime, normalized.message, {
2106
2449
  phase: "planning",
2107
2450
  status: normalized.kind,
2108
- isError: normalized.kind !== "repair_attempt",
2451
+ isError: !["repair_attempt", "network_recovery"].includes(normalized.kind),
2109
2452
  plannerDiagnostic: normalized.kind,
2110
2453
  plannerDiagnostics: normalized.diagnostics,
2454
+ plannerPartialOutputObserved: normalized.partialOutputObserved,
2111
2455
  plannerSessionFile: normalized.sessionFile,
2112
2456
  plannerSessionId: normalized.sessionId,
2457
+ networkRecoveryEvent: normalized.networkRecoveryEvent,
2458
+ networkRetryCount: normalized.networkRetryCount,
2459
+ networkOutageElapsedMs: normalized.networkOutageElapsedMs,
2460
+ networkFailureReason: normalized.networkFailureReason,
2461
+ networkOperation:
2462
+ normalized.kind === "network_recovery" || normalized.kind === "network_failure" ? "planner" : undefined,
2463
+ plannerDeadlinePolicy:
2464
+ normalized.kind === "network_recovery" || normalized.kind === "network_failure"
2465
+ ? "per_attempt_excludes_network_wait"
2466
+ : undefined,
2113
2467
  taskProgress: buildTaskProgressModel({ tasks: [] }),
2114
2468
  });
2115
2469
  }
@@ -2582,8 +2936,11 @@ function outcomeProgressItemStatus(
2582
2936
  return "failed";
2583
2937
  }
2584
2938
 
2585
- function initialTaskResultMarkdown(runId: string): string {
2586
- return `# Pi Long Task TASK_RESULT\n\nRun: ${runId}\n`;
2939
+ function initialTaskResultMarkdown(runId: string, capabilityWarnings: readonly WorkerCapabilityWarning[] = []): string {
2940
+ const warningBlock = capabilityWarnings.length
2941
+ ? `\n\n## Worker capability warnings\n\n${capabilityWarnings.map((warning) => `- ${warning.message}`).join("\n")}`
2942
+ : "";
2943
+ return `# Pi Long Task TASK_RESULT\n\nRun: ${runId}${warningBlock}\n`;
2587
2944
  }
2588
2945
 
2589
2946
  async function appendFailureNote(
@@ -2596,6 +2953,21 @@ async function appendFailureNote(
2596
2953
  lines.push("", "### Planner diagnostics");
2597
2954
  for (const diagnostic of plannerDiagnostics) {
2598
2955
  lines.push("", `- ${diagnostic.kind}: ${diagnostic.message}`);
2956
+ if (diagnostic.partialOutputObserved !== undefined) {
2957
+ lines.push(` - Partial output observed: ${diagnostic.partialOutputObserved ? "yes" : "no"}`);
2958
+ }
2959
+ if (diagnostic.networkRecoveryEvent) {
2960
+ lines.push(` - Network recovery event: ${diagnostic.networkRecoveryEvent}`);
2961
+ }
2962
+ if (diagnostic.networkFailureReason) {
2963
+ lines.push(` - Network failure reason: ${diagnostic.networkFailureReason}`);
2964
+ }
2965
+ if (diagnostic.networkRetryCount !== undefined) {
2966
+ lines.push(` - Network retry count: ${diagnostic.networkRetryCount}`);
2967
+ }
2968
+ if (diagnostic.networkOutageElapsedMs !== undefined) {
2969
+ lines.push(` - Network outage elapsed: ${formatFriendlyDuration(diagnostic.networkOutageElapsedMs)}`);
2970
+ }
2599
2971
  if (diagnostic.sessionId) {
2600
2972
  lines.push(` - Session ID: ${diagnostic.sessionId}`);
2601
2973
  }
@@ -2794,6 +3166,17 @@ function coordinatorInputText(options: RunCoordinatorOptions): string {
2794
3166
  return normalizeOptionalText(options.inputText) ?? normalizeOptionalText(options.goal) ?? "";
2795
3167
  }
2796
3168
 
3169
+ function capabilityWarningsForRequest(inputText?: string, goal?: string): WorkerCapabilityWarning[] {
3170
+ const requestText = [normalizeOptionalText(inputText), normalizeOptionalText(goal)]
3171
+ .filter((item): item is string => Boolean(item))
3172
+ .filter((item, index, all) => all.indexOf(item) === index)
3173
+ .join("\n");
3174
+ return detectUnavailableWorkerCapabilities(requestText, {
3175
+ tools: DEFAULT_WORKER_TOOLS,
3176
+ extensionsEnabled: false,
3177
+ });
3178
+ }
3179
+
2797
3180
  function positiveInteger(value: number | undefined, fallback: number): number {
2798
3181
  if (typeof value === "number" && Number.isFinite(value) && value > 0) {
2799
3182
  return Math.floor(value);