@ordewell/core 0.10.4 → 0.10.5

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 (46) hide show
  1. package/dist/{IFileSystem-_effm8-b.d.ts → IFileSystem-ClBdp59f.d.ts} +1 -1
  2. package/dist/{IFileSystem-vyqMdL-x.d.mts → IFileSystem-Db34maxC.d.mts} +1 -1
  3. package/dist/{IRunner-D_UkkMbJ.d.ts → IRunner-Crkt3tHD.d.ts} +64 -3
  4. package/dist/{IRunner-NtTHQgLR.d.mts → IRunner-lMQIa5Wc.d.mts} +64 -3
  5. package/dist/{ModeResolver-wnTFsDnF.d.mts → ModeResolver-B4hhABNf.d.mts} +1 -1
  6. package/dist/{ModeResolver-Dy3Ef6p-.d.ts → ModeResolver-CUR5jCSU.d.ts} +1 -1
  7. package/dist/{Task-DZs9eKgg.d.mts → Task-D4QHjdGv.d.mts} +92 -10
  8. package/dist/{Task-DZs9eKgg.d.ts → Task-D4QHjdGv.d.ts} +92 -10
  9. package/dist/{chunk-A57ITQE7.mjs → chunk-4QLFM73A.mjs} +34 -5
  10. package/dist/chunk-4QLFM73A.mjs.map +1 -0
  11. package/dist/{chunk-FH6M5QZG.mjs → chunk-6LEA6WAM.mjs} +88 -7
  12. package/dist/chunk-6LEA6WAM.mjs.map +1 -0
  13. package/dist/{chunk-645FALZX.mjs → chunk-F3KIX7WR.mjs} +3 -3
  14. package/dist/{chunk-OYUSADFM.mjs → chunk-NJGID6BG.mjs} +3 -1
  15. package/dist/chunk-NJGID6BG.mjs.map +1 -0
  16. package/dist/{chunk-SM6BV6XS.mjs → chunk-RT4NQ2BZ.mjs} +3 -1
  17. package/dist/chunk-RT4NQ2BZ.mjs.map +1 -0
  18. package/dist/index.d.mts +296 -64
  19. package/dist/index.d.ts +296 -64
  20. package/dist/index.js +1576 -376
  21. package/dist/index.js.map +1 -1
  22. package/dist/index.mjs +1455 -362
  23. package/dist/index.mjs.map +1 -1
  24. package/dist/parsing.d.mts +2 -2
  25. package/dist/parsing.d.ts +2 -2
  26. package/dist/parsing.js.map +1 -1
  27. package/dist/parsing.mjs +3 -3
  28. package/dist/{plan-utils-POIz5d8z.d.ts → plan-utils-BVJCzNvm.d.ts} +204 -19
  29. package/dist/{plan-utils-CtKceSsQ.d.mts → plan-utils-wI8MuDBC.d.mts} +204 -19
  30. package/dist/plan-utils.d.mts +4 -4
  31. package/dist/plan-utils.d.ts +4 -4
  32. package/dist/plan-utils.js +94 -5
  33. package/dist/plan-utils.js.map +1 -1
  34. package/dist/plan-utils.mjs +13 -3
  35. package/dist/testing.d.mts +6 -3
  36. package/dist/testing.d.ts +6 -3
  37. package/dist/testing.js +7 -0
  38. package/dist/testing.js.map +1 -1
  39. package/dist/testing.mjs +7 -2
  40. package/dist/testing.mjs.map +1 -1
  41. package/package.json +1 -1
  42. package/dist/chunk-A57ITQE7.mjs.map +0 -1
  43. package/dist/chunk-FH6M5QZG.mjs.map +0 -1
  44. package/dist/chunk-OYUSADFM.mjs.map +0 -1
  45. package/dist/chunk-SM6BV6XS.mjs.map +0 -1
  46. /package/dist/{chunk-645FALZX.mjs.map → chunk-F3KIX7WR.mjs.map} +0 -0
package/dist/index.js CHANGED
@@ -114,6 +114,7 @@ __export(index_exports, {
114
114
  PlannerNotReadyError: () => PlannerNotReadyError,
115
115
  PlannerTurnDiscardedError: () => PlannerTurnDiscardedError,
116
116
  PlannerTurnStoppedError: () => PlannerTurnStoppedError,
117
+ PlannerUsageLimitError: () => PlannerUsageLimitError,
117
118
  REFUSED_COMMANDS: () => REFUSED_COMMANDS,
118
119
  RUN_SETTINGS_DEFAULTS: () => RUN_SETTINGS_DEFAULTS,
119
120
  RunSettingsError: () => RunSettingsError,
@@ -170,6 +171,8 @@ __export(index_exports, {
170
171
  approvalScopes: () => approvalScopes,
171
172
  assertWorkspaceExists: () => assertWorkspaceExists,
172
173
  assertWorkspaceIsProject: () => assertWorkspaceIsProject,
174
+ assignmentIssue: () => assignmentIssue,
175
+ assignmentIssueText: () => assignmentIssueText,
173
176
  augmentedPath: () => augmentedPath,
174
177
  automergeSummary: () => automergeSummary,
175
178
  autonomyLevelLabel: () => autonomyLevelLabel,
@@ -186,6 +189,7 @@ __export(index_exports, {
186
189
  canSetDependencies: () => canSetDependencies,
187
190
  canSplitTask: () => canSplitTask,
188
191
  capConflictFiles: () => capConflictFiles,
192
+ checkoutRule: () => checkoutRule,
189
193
  classifyCommand: () => classifyCommand,
190
194
  classifyOutcome: () => classifyOutcome,
191
195
  clearAugmentedPathCache: () => clearAugmentedPathCache,
@@ -199,6 +203,7 @@ __export(index_exports, {
199
203
  commandAllowedBy: () => commandAllowedBy,
200
204
  configuredProviders: () => configuredProviders,
201
205
  continuability: () => continuability,
206
+ countedSessionCost: () => countedSessionCost,
202
207
  createAiService: () => createAiService,
203
208
  createEmptyPlan: () => createEmptyPlan,
204
209
  createSession: () => createSession,
@@ -262,6 +267,7 @@ __export(index_exports, {
262
267
  isMeasured: () => isMeasured,
263
268
  isOpenAiProvider: () => isOpenAiProvider,
264
269
  isPlannerBackend: () => isPlannerBackend,
270
+ isRepoGroup: () => isRepoGroup,
265
271
  isRunnerApproval: () => isRunnerApproval,
266
272
  isSupervisorLevel: () => isSupervisorLevel,
267
273
  isTaskRunning: () => isTaskRunning,
@@ -284,6 +290,7 @@ __export(index_exports, {
284
290
  markdownHtml: () => markdownHtml,
285
291
  markdownTokens: () => markdownTokens,
286
292
  mcpClientConfig: () => mcpClientConfig,
293
+ mergeTarget: () => mergeTarget,
287
294
  migrateOldConfigDir: () => migrateOldConfigDir,
288
295
  migratePlanIsolation: () => migratePlanIsolation,
289
296
  migratePlanState: () => migratePlanState,
@@ -333,6 +340,7 @@ __export(index_exports, {
333
340
  removedPluginNotice: () => removedPluginNotice,
334
341
  renderTaskQueryAnswer: () => renderTaskQueryAnswer,
335
342
  replayTaskLog: () => replayTaskLog,
343
+ reposWithWork: () => reposWithWork,
336
344
  researchShellWarning: () => researchShellWarning,
337
345
  researchToolsPath: () => researchToolsPath,
338
346
  resetInstalledPlanner: () => resetInstalledPlanner,
@@ -346,6 +354,7 @@ __export(index_exports, {
346
354
  resolveWithin: () => resolveWithin,
347
355
  resumeLabel: () => resumeLabel,
348
356
  resumeTime: () => resumeTime,
357
+ resumedSessionWindow: () => resumedSessionWindow,
349
358
  runSettingsOf: () => runSettingsOf,
350
359
  runnerForProvider: () => runnerForProvider,
351
360
  runnerModesFrom: () => runnerModesFrom,
@@ -386,6 +395,7 @@ __export(index_exports, {
386
395
  taskRowView: () => taskRowView,
387
396
  taskStartedNotice: () => taskStartedNotice,
388
397
  taskStatusKind: () => taskStatusKind,
398
+ taskUsageSummary: () => taskUsageSummary,
389
399
  textHasTaskOps: () => textHasTaskOps,
390
400
  textHasTaskQuery: () => textHasTaskQuery,
391
401
  titledRefs: () => titledRefs,
@@ -488,6 +498,7 @@ function migrateLoadedTasks(tasks) {
488
498
  for (const task of tasks) {
489
499
  if (typeof task.runnerSessionId !== "string") delete task.runnerSessionId;
490
500
  if (typeof task.carriedFrom !== "string") delete task.carriedFrom;
501
+ if (typeof task.runnerSessionOwner !== "string") delete task.runnerSessionOwner;
491
502
  const legacy = task;
492
503
  if (!task.runnerSessionId && legacy.transport?.kind === "structured" && typeof legacy.transport.nativeSessionId === "string") {
493
504
  task.runnerSessionId = legacy.transport.nativeSessionId;
@@ -747,6 +758,7 @@ function keepExecutionState(current, rewrite) {
747
758
  verdict: prior?.verdict,
748
759
  outputSummary: prior?.outputSummary,
749
760
  runnerSessionId: prior?.runnerSessionId,
761
+ runnerSessionOwner: prior?.runnerSessionOwner,
750
762
  carriedFrom: prior?.carriedFrom,
751
763
  attemptSkills: prior?.attemptSkills,
752
764
  attemptUsage: prior?.attemptUsage,
@@ -835,8 +847,21 @@ function addAttemptUsage(usage, record, now) {
835
847
  const folded = addPlannerUsage(usage, record);
836
848
  return { ...usage, ...folded, lastPromptTokens: folded.lastPromptTokens, contextWindow: folded.contextWindow, wallTimeMs: Math.max(0, Date.parse(now) - Date.parse(usage.startedAt)) };
837
849
  }
838
- function endAttemptUsage(usage, now) {
839
- return { ...usage, endedAt: now, wallTimeMs: Math.max(0, Date.parse(now) - Date.parse(usage.startedAt)) };
850
+ function endAttemptUsage(usage, now, runnerSessionCost, sessionId) {
851
+ return {
852
+ ...usage,
853
+ endedAt: now,
854
+ wallTimeMs: Math.max(0, Date.parse(now) - Date.parse(usage.startedAt)),
855
+ ...sessionId ? { sessionId } : {},
856
+ ...runnerSessionCost ? { runnerSessionCost } : {}
857
+ };
858
+ }
859
+ function countedSessionCost(attempts, sessionId) {
860
+ return [...attempts ?? []].reverse().find((usage) => usage.runnerSessionCost?.sessionId === sessionId)?.runnerSessionCost;
861
+ }
862
+ function resumedSessionWindow(attempts, runner, attempt, sessionId) {
863
+ const last = [...attempts ?? []].reverse().find((usage) => usage.attempt !== attempt && usage.runner === runner && usage.sessionId === sessionId && usage.contextWindow !== void 0);
864
+ return last ? { source: last.source, model: last.model, contextWindow: last.contextWindow } : void 0;
840
865
  }
841
866
  function addUsageTotals(totals, addition) {
842
867
  let next = addUsage(totals, { source: "", inputTokens: addition.inputTokens, outputTokens: addition.outputTokens, cachedInputTokens: addition.cachedInputTokens });
@@ -865,6 +890,16 @@ function planUsage(tasks) {
865
890
  }
866
891
  return result;
867
892
  }
893
+ function taskUsageSummary(task) {
894
+ const attempts = task.attemptUsage ?? [];
895
+ const latest = attempts.at(-1);
896
+ const contextFill = latest ? plannerContextFill(latest) : void 0;
897
+ return {
898
+ totals: attempts.reduce((sum, attempt) => addUsageTotals(sum, attempt.totals), {}),
899
+ attempts: attempts.length,
900
+ ...contextFill ? { contextFill } : {}
901
+ };
902
+ }
868
903
 
869
904
  // src/models/TaskLog.ts
870
905
  var HEAD_LINES = 60;
@@ -966,6 +1001,9 @@ function toTaskLogEvent(event) {
966
1001
  };
967
1002
  case "command_output":
968
1003
  return { type: "command_output", text: event.text };
1004
+ // Logged once the plan's write-back settles: see `TaskLogRecorder.note`.
1005
+ case "model_switched":
1006
+ case "model_drifted":
969
1007
  case "background_agent":
970
1008
  return null;
971
1009
  }
@@ -5417,8 +5455,8 @@ function forms(text3) {
5417
5455
  function sensitiveEnvValues(env) {
5418
5456
  return Object.entries(env).filter(([name, value]) => SENSITIVE_ENV_NAME.test(name) && !/_(?:URL|URI|PATH|FILE|DIR)$/i.test(name) && value !== void 0).map(([, value]) => (value ?? "").trim());
5419
5457
  }
5420
- function carriesSecret(texts, held) {
5421
- const secrets = [...held].map((s) => s.trim()).filter((s) => s.length >= MIN_SECRET_LENGTH);
5458
+ function carriesSecret(texts, held2) {
5459
+ const secrets = [...held2].map((s) => s.trim()).filter((s) => s.length >= MIN_SECRET_LENGTH);
5422
5460
  return texts.flatMap(forms).some((text3) => secrets.some((secret) => text3.includes(secret)) || TOKEN_SHAPES.some((shape) => shape.test(text3)) || [...text3.matchAll(SK_KEY)].some((m) => looksRandom(m[0].slice(3))));
5423
5461
  }
5424
5462
 
@@ -7239,6 +7277,12 @@ function commandOutput(view, { text: text3 }) {
7239
7277
  return { ...view, blocks: replaceAt(view.blocks, i, { ...block, output: block.output ? `${block.output}
7240
7278
  ${text3}` : text3 }) };
7241
7279
  }
7280
+ function modelSwitchNotice({ from, to, plan, kept }) {
7281
+ if (!plan) return `Model changed in the runner: ${from.label ?? from.model} \u2192 ${to.label ?? to.model}${kept ? ` (${kept})` : ""}`;
7282
+ const planned = plan.model ? `${plan.model.label ?? plan.model.model}${plan.effort ? `, ${plan.effort}` : ""}` : void 0;
7283
+ if (!kept) return `Model changed in the runner: ${from.label ?? from.model} \u2192 ${to.label ?? to.model}; the plan now says ${planned ?? to.label ?? to.model}`;
7284
+ return `The runner now answers on ${to.label ?? to.model}; ${planned ? `the plan still says ${planned}` : "the plan names no model"} (${kept})`;
7285
+ }
7242
7286
  function appendStep(view, make) {
7243
7287
  const next = append(view, make);
7244
7288
  return sealLane(next, null, findLastIndex(next.blocks, (b) => b.type !== "usage"));
@@ -7300,6 +7344,8 @@ function reduceTaskLog(view, event) {
7300
7344
  return startCommand(view, event);
7301
7345
  case "command_output":
7302
7346
  return commandOutput(view, event);
7347
+ case "model_switched":
7348
+ return append(view, (id) => settledMessage(id, "system", modelSwitchNotice(event)));
7303
7349
  case "compaction":
7304
7350
  return appendStep(view, (id) => ({
7305
7351
  type: "compaction",
@@ -7851,6 +7897,10 @@ function parseOpenCodeModels(stdout, manifest) {
7851
7897
  }
7852
7898
  return models;
7853
7899
  }
7900
+ function contextLimit(limit) {
7901
+ const context = limit?.context;
7902
+ return typeof context === "number" && Number.isFinite(context) && context > 0 ? { contextWindow: context } : {};
7903
+ }
7854
7904
  function parseOpenCodeModelsVerbose(stdout, manifest) {
7855
7905
  const normalized2 = stdout.endsWith("\n") ? stdout : stdout + "\n";
7856
7906
  const parts = normalized2.split(/^([A-Za-z0-9._-]+\/[^\s"{}]+)\n/m);
@@ -7866,12 +7916,14 @@ function parseOpenCodeModelsVerbose(stdout, manifest) {
7866
7916
  const jsonText = parts[i + 1].trim();
7867
7917
  if (seen.has(fullId)) continue;
7868
7918
  let variantIds = [];
7919
+ let window = {};
7869
7920
  let modelLabel = prefLabels.get(fullId) || fullId.split("/").pop() || fullId;
7870
7921
  const runnerProvider = fullId.split("/")[0];
7871
7922
  try {
7872
7923
  const data = JSON.parse(jsonText);
7873
7924
  if (data.name) modelLabel = prefLabels.get(fullId) || data.name;
7874
7925
  variantIds = Object.keys(data.variants ?? {});
7926
+ window = contextLimit(data.limit);
7875
7927
  } catch {
7876
7928
  }
7877
7929
  seen.add(fullId);
@@ -7879,7 +7931,8 @@ function parseOpenCodeModelsVerbose(stdout, manifest) {
7879
7931
  modelId: fullId,
7880
7932
  modelLabel,
7881
7933
  runnerProvider,
7882
- variants: variantIds.map((id) => ({ id, label: id.charAt(0).toUpperCase() + id.slice(1) }))
7934
+ variants: variantIds.map((id) => ({ id, label: id.charAt(0).toUpperCase() + id.slice(1) })),
7935
+ ...window
7883
7936
  });
7884
7937
  }
7885
7938
  return preferredFirst(models, manifest);
@@ -7920,7 +7973,8 @@ function parseOpenCodeModelsApi(stdout, manifest) {
7920
7973
  modelId: fullId,
7921
7974
  modelLabel: prefLabels.get(fullId) || typeof entry.name === "string" && entry.name || entry.modelID,
7922
7975
  runnerProvider: entry.providerID,
7923
- variants: variantIds.map((id) => ({ id, label: id.charAt(0).toUpperCase() + id.slice(1) }))
7976
+ variants: variantIds.map((id) => ({ id, label: id.charAt(0).toUpperCase() + id.slice(1) })),
7977
+ ...contextLimit(entry.limit)
7924
7978
  });
7925
7979
  }
7926
7980
  return preferredFirst(models, manifest);
@@ -9804,6 +9858,7 @@ var LOG_KINDS = {
9804
9858
  turn_start: true,
9805
9859
  command: true,
9806
9860
  command_output: true,
9861
+ model_switched: true,
9807
9862
  compaction: true,
9808
9863
  runner_loaded: true,
9809
9864
  turn_end: true,
@@ -9839,6 +9894,11 @@ ${s.content}` : ""}`).join("\n");
9839
9894
  return `${event.messageId ? `${event.messageId} ` : ""}/${event.name}${event.args ? ` ${event.args}` : ""}`;
9840
9895
  case "command_output":
9841
9896
  return event.text;
9897
+ case "model_switched": {
9898
+ const { plan } = event;
9899
+ const planned = plan ? `; plan: ${plan.model?.model ?? "no model"}${plan.effort ? ` (${plan.effort})` : ""}` : "";
9900
+ return `${event.from.model} \u2192 ${event.to.model}${planned}${event.kept ? `, kept: ${event.kept}` : ""}`;
9901
+ }
9842
9902
  case "compaction":
9843
9903
  return `${event.trigger}${event.preTokens !== void 0 && event.postTokens !== void 0 ? `, ${event.preTokens} \u2192 ${event.postTokens} tokens` : ""}`;
9844
9904
  case "runner_loaded":
@@ -11398,6 +11458,7 @@ function canUseToolAnswer({ input, suggestions }, decision) {
11398
11458
  ...decision.decision === "allowForTask" && suggestions.length > 0 ? { updatedPermissions: suggestions } : {}
11399
11459
  };
11400
11460
  }
11461
+ var MODEL_COMMAND = "model";
11401
11462
  var SUBAGENT_TOOLS = /* @__PURE__ */ new Set(["Agent", "Task"]);
11402
11463
  function usageRecord(usage, model, subagentId) {
11403
11464
  const record = {
@@ -11459,9 +11520,9 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
11459
11520
  /** The model answering the planner's current message, as its `message_start` named it. */
11460
11521
  plannerModel;
11461
11522
  /**
11462
- * The session's `total_cost_usd` as last reported. Undefined after a resume:
11463
- * the CLI restores the resumed session's running total, and what Ordewell
11464
- * already counted of it is not ours to know here.
11523
+ * The session's `total_cost_usd` as last counted. A resume starts from what
11524
+ * was counted of that session before, and is undefined without it: the CLI
11525
+ * restores the session's running total, which holds turns counted earlier.
11465
11526
  */
11466
11527
  reportedCostUsd = 0;
11467
11528
  reportedModelCostUsd = /* @__PURE__ */ new Map();
@@ -11502,6 +11563,19 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
11502
11563
  terminalOnly = /* @__PURE__ */ new Set();
11503
11564
  /** Whether this process's `runner_loaded` went out: its first `init` sends it. */
11504
11565
  loadedReported = false;
11566
+ /** The models `initialize` listed, which turn the ids the CLI reports back into the ones a plan holds. */
11567
+ listedModels = [];
11568
+ /** The id the task's main thread last reported answering on. */
11569
+ mainModel;
11570
+ /** What the plan should say the task runs: its spawn's model, then each switch followed. */
11571
+ followedModel;
11572
+ /**
11573
+ * Model commands sent through the chat whose switch has not been seen yet,
11574
+ * oldest first, each with the turns still to end before its own has. Several
11575
+ * can be pending: a command's switch is first reported by the next turn's
11576
+ * `init`, which can be another `/model`'s.
11577
+ */
11578
+ modelCommands = [];
11505
11579
  spawnSpec(opts, workspace) {
11506
11580
  if (opts.kind === "task") return this.taskSpawnSpec(opts);
11507
11581
  const args = [
@@ -11527,12 +11601,23 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
11527
11601
  if (opts.effort && opts.effort !== "adaptive") args.push("--effort", opts.effort);
11528
11602
  if (opts.resumeSessionId) {
11529
11603
  args.push("--resume", opts.resumeSessionId);
11530
- this.reportedCostUsd = void 0;
11604
+ this.countFrom(opts.resumeSessionId, opts.resumeCost);
11531
11605
  this.pendingResume = opts.resumeSessionId;
11532
11606
  }
11533
11607
  this.configDir = workspace.CLAUDE_CONFIG_DIR ?? process.env.CLAUDE_CONFIG_DIR ?? path13.join(os6.homedir(), ".claude");
11534
11608
  return { command: "claude", args, env: NO_AUTO_MEMORY };
11535
11609
  }
11610
+ sessionCost() {
11611
+ const sessionId = this.sessionId ?? this.pendingResume;
11612
+ if (!sessionId || this.reportedCostUsd === void 0) return null;
11613
+ return { sessionId, usd: this.reportedCostUsd, modelUsd: Object.fromEntries(this.reportedModelCostUsd) };
11614
+ }
11615
+ /** A count kept for another session says nothing of this one's. */
11616
+ countFrom(sessionId, counted) {
11617
+ const known = counted?.sessionId === sessionId ? counted : void 0;
11618
+ this.reportedCostUsd = known?.usd;
11619
+ this.reportedModelCostUsd = new Map(Object.entries(known?.modelUsd ?? {}));
11620
+ }
11536
11621
  savedOutput(abs) {
11537
11622
  return this.configDir !== null && this.sessionId !== null && inSavedOutput(abs, this.configDir, this.sessionId);
11538
11623
  }
@@ -11547,6 +11632,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
11547
11632
  */
11548
11633
  taskSpawnSpec(opts) {
11549
11634
  this.requestedMode = opts.flags.permissionMode;
11635
+ this.followedModel = opts.model;
11550
11636
  const args = [
11551
11637
  ...TASK_PROTOCOL_ARGS,
11552
11638
  "--permission-prompt-tool",
@@ -11560,7 +11646,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
11560
11646
  if (opts.model) args.push("--model", opts.model);
11561
11647
  if (opts.resumeSessionId) {
11562
11648
  args.push("--resume", opts.resumeSessionId);
11563
- this.reportedCostUsd = void 0;
11649
+ this.countFrom(opts.resumeSessionId, opts.resumeCost);
11564
11650
  this.pendingResume = opts.resumeSessionId;
11565
11651
  }
11566
11652
  if (opts.mcp) args.push("--append-system-prompt", TASK_COMPLETION_CONTRACT, ...this.ordewellServerArgs(opts.mcp, "task"));
@@ -11602,6 +11688,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
11602
11688
  if (opts.kind !== "task") return;
11603
11689
  const initialized = (await this.control({ subtype: "initialize" }, MODEL_PIN_TIMEOUT_MS))?.response;
11604
11690
  this.listedCommands = initialized?.commands ?? [];
11691
+ this.listedModels = initialized?.models ?? [];
11605
11692
  if (opts.flags.permissionMode !== "plan" || !opts.model) return;
11606
11693
  const resolved = initialized?.models?.find((m) => m.value === opts.model)?.resolvedModel;
11607
11694
  if (resolved && resolved !== opts.model) await this.control({ subtype: "set_model", model: resolved }, MODEL_PIN_TIMEOUT_MS);
@@ -11655,6 +11742,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
11655
11742
  if (this.role !== "task" || !this.process || this.interruptRequested || this.pendingResume) return false;
11656
11743
  const uuid = (0, import_crypto4.randomUUID)();
11657
11744
  this.steers.set(uuid, id);
11745
+ this.noteModelCommand(text3, 2);
11658
11746
  this.writeLine({ type: "user", uuid, message: { role: "user", content: [{ type: "text", text: text3 }] } });
11659
11747
  return true;
11660
11748
  }
@@ -11706,12 +11794,23 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
11706
11794
  }
11707
11795
  turnPayload(message) {
11708
11796
  this.openTurnState();
11797
+ this.noteModelCommand(message, 1);
11709
11798
  return `${JSON.stringify({
11710
11799
  type: "user",
11711
11800
  message: { role: "user", content: [{ type: "text", text: message }] }
11712
11801
  })}
11713
11802
  `;
11714
11803
  }
11804
+ /**
11805
+ * The CLI runs a message that starts with a slash command as that command,
11806
+ * whether the surface sent it as one or as plain text, so the message
11807
+ * itself says a model switch was asked for: every way into the chat counts.
11808
+ */
11809
+ noteModelCommand(text3, turnsLeft) {
11810
+ const [, name, args = ""] = /^\/(\S+)\s*([\s\S]*)$/.exec(text3) ?? [];
11811
+ if (this.role !== "task" || !name || (findRunnerCommand(this.commands(), name)?.name ?? name) !== MODEL_COMMAND) return;
11812
+ this.modelCommands.push({ args: args.trim(), turnsLeft });
11813
+ }
11715
11814
  openTurnState() {
11716
11815
  this.turnHasText = false;
11717
11816
  this.interruptRequested = false;
@@ -11823,6 +11922,7 @@ ${block.text}` : block.text });
11823
11922
  this.clearFollowOnTimer();
11824
11923
  if (this.refuseOtherMode(msg, emit)) return;
11825
11924
  this.readLoaded(msg, emit);
11925
+ this.followModel(msg.model, emit);
11826
11926
  }
11827
11927
  if (msg.subtype === "commands_changed" && Array.isArray(msg.commands)) this.listedCommands = msg.commands;
11828
11928
  if (msg.subtype === "compact_boundary") {
@@ -11845,6 +11945,7 @@ ${block.text}` : block.text });
11845
11945
  return;
11846
11946
  case "result":
11847
11947
  this.reportSessionCost(msg, emit);
11948
+ for (const command of this.modelCommands) command.turnsLeft = Math.max(0, command.turnsLeft - 1);
11848
11949
  if (this.interruptRequested && (msg.is_error || msg.subtype !== "success")) {
11849
11950
  this.interruptRequested = false;
11850
11951
  this.closeTurn({ type: "turn_end", interrupted: true }, emit);
@@ -11864,6 +11965,55 @@ ${block.text}` : block.text });
11864
11965
  return;
11865
11966
  }
11866
11967
  }
11968
+ /**
11969
+ * A task's main thread reported the model it answers on. A change is the
11970
+ * user's own switch when a model command went out before it: Ordewell never
11971
+ * passes `--fallback-model`, so nothing else should move it. A change with
11972
+ * no command before it is reported as drift, and the plan does not follow
11973
+ * it. A command whose turn ended with the model unchanged switched nothing,
11974
+ * and is let go.
11975
+ *
11976
+ * Effort is never reported (recorded on 2.1.302, `task-effort-command`):
11977
+ * `init` names the model alone, its `per_turn_effort_active` says effort is
11978
+ * on, not which; `/effort low` only prints that it set it; and `/model`
11979
+ * takes no effort (`/model haiku high` is "not found"). So a switch carries
11980
+ * no effort and the plan's stays as planned.
11981
+ */
11982
+ followModel(reported, emit) {
11983
+ if (this.role !== "task" || !reported || reported === "<synthetic>") return;
11984
+ const previous = this.mainModel;
11985
+ this.mainModel = reported;
11986
+ if (previous === void 0 || previous === reported) {
11987
+ this.modelCommands = this.modelCommands.filter((c) => c.turnsLeft > 0);
11988
+ return;
11989
+ }
11990
+ const matched = this.modelCommands.findIndex((c) => this.runsAs(c.args, reported));
11991
+ const command = this.modelCommands[Math.max(matched, 0)];
11992
+ this.modelCommands = this.modelCommands.slice(Math.max(matched, 0) + 1);
11993
+ const from = this.listedModel(previous, this.followedModel);
11994
+ if (!command) {
11995
+ emit({ type: "model_drifted", from, to: this.listedModel(reported, void 0) });
11996
+ return;
11997
+ }
11998
+ const to = this.listedModel(reported, command.args);
11999
+ this.followedModel = to.model;
12000
+ emit({ type: "model_switched", from, to });
12001
+ }
12002
+ /** Whether the model value `asked` runs as the id `resolved`, by what `initialize` lists. */
12003
+ runsAs(asked, resolved) {
12004
+ return asked === resolved || this.listedModels.some((m) => m.value === asked && m.resolvedModel === resolved);
12005
+ }
12006
+ /**
12007
+ * The value `initialize` lists for the id a model runs as. Several can share
12008
+ * one (`sonnet`, `default`); the one asked for wins, and `default` only as a
12009
+ * last resort, since it names whatever the CLI's default becomes.
12010
+ */
12011
+ listedModel(resolved, asked) {
12012
+ const listed3 = this.listedModels.filter((m) => m.value && (m.resolvedModel === resolved || m.value === resolved));
12013
+ const pick = listed3.find((m) => m.value === asked && asked !== "default") ?? listed3.find((m) => m.value !== "default") ?? listed3[0];
12014
+ if (!pick?.value) return { model: resolved };
12015
+ return { model: pick.value, ...pick.displayName ? { label: pick.displayName } : {} };
12016
+ }
11867
12017
  /**
11868
12018
  * `--permission-mode auto` on a model or account without auto mode is not
11869
12019
  * refused: the CLI says nothing and starts in `default`, then asks about every
@@ -11947,8 +12097,10 @@ ${block.text}` : block.text });
11947
12097
  * {@link AgentEvent}), so nothing here has to reconcile with it.
11948
12098
  */
11949
12099
  handleStreamEvent(event, emit) {
11950
- if (event.type === "message_start") this.plannerModel = event.message?.model;
11951
- else if (event.type === "message_delta" && event.usage) emit({ type: "usage", record: usageRecord(event.usage, this.plannerModel) });
12100
+ if (event.type === "message_start") {
12101
+ this.plannerModel = event.message?.model;
12102
+ this.followModel(this.plannerModel, emit);
12103
+ } else if (event.type === "message_delta" && event.usage) emit({ type: "usage", record: usageRecord(event.usage, this.plannerModel) });
11952
12104
  else if (event.type === "content_block_start" && event.content_block?.type === "text") {
11953
12105
  this.pendingBreak = this.turnHasText;
11954
12106
  } else if (event.type === "content_block_delta" && event.delta?.type === "text_delta" && event.delta.text) {
@@ -11963,7 +12115,8 @@ ${block.text}` : block.text });
11963
12115
  * What only the result line knows: the cost, which covers every call the
11964
12116
  * session made — subagents included, since none of their lines carries one —
11965
12117
  * and the planner model's window. `total_cost_usd` is a running total, so a
11966
- * turn reports its growth. The first turn after a resume only sets the
12118
+ * turn reports its growth. After a resume it grows from what was counted of
12119
+ * the session before; with no count kept, the first turn only sets the
11967
12120
  * baseline: its total includes turns counted before, and a turn's own share
11968
12121
  * cannot be told apart from them.
11969
12122
  */
@@ -12543,6 +12696,97 @@ var CodexRpc = class {
12543
12696
  }
12544
12697
  };
12545
12698
 
12699
+ // src/utils/wallTime.ts
12700
+ var MONTHS = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
12701
+ function lastMatch(re, text3) {
12702
+ let last = null;
12703
+ for (const m of text3.matchAll(re)) last = m;
12704
+ return last;
12705
+ }
12706
+ function hour24(hour, meridiem) {
12707
+ const h = Number(hour) % 12;
12708
+ return meridiem.toLowerCase() === "pm" ? h + 12 : h;
12709
+ }
12710
+ function month(name) {
12711
+ if (!name) return void 0;
12712
+ const at = MONTHS.indexOf(name.toLowerCase());
12713
+ return at < 0 ? void 0 : at;
12714
+ }
12715
+ function placeWallTime(wall, zone, now) {
12716
+ const today = wallFields(now, zone);
12717
+ const year = wall.year ?? today.year;
12718
+ const monthIndex = wall.month ?? today.month;
12719
+ const day = wall.day ?? today.day;
12720
+ const at = fromWall(year, monthIndex, day, wall.hour, wall.minute, zone);
12721
+ if (wall.day !== void 0 || at.getTime() >= now.getTime() - HOUR_MS) return at;
12722
+ return fromWall(year, monthIndex, day + 1, wall.hour, wall.minute, zone);
12723
+ }
12724
+ var HOUR_MS = 60 * 6e4;
12725
+ function wallFields(instant, zone) {
12726
+ if (!zone) {
12727
+ return { year: instant.getFullYear(), month: instant.getMonth(), day: instant.getDate(), hour: instant.getHours(), minute: instant.getMinutes(), second: instant.getSeconds() };
12728
+ }
12729
+ const parts = new Intl.DateTimeFormat("en-US", {
12730
+ timeZone: zone,
12731
+ hourCycle: "h23",
12732
+ year: "numeric",
12733
+ month: "numeric",
12734
+ day: "numeric",
12735
+ hour: "numeric",
12736
+ minute: "numeric",
12737
+ second: "numeric"
12738
+ }).formatToParts(instant);
12739
+ const field = (type) => Number(parts.find((p) => p.type === type)?.value ?? 0);
12740
+ return { year: field("year"), month: field("month") - 1, day: field("day"), hour: field("hour"), minute: field("minute"), second: field("second") };
12741
+ }
12742
+ function zoneOffsetMs(instant, zone) {
12743
+ const f = wallFields(new Date(instant), zone);
12744
+ return Date.UTC(f.year, f.month, f.day, f.hour, f.minute, f.second) - Math.floor(instant / 1e3) * 1e3;
12745
+ }
12746
+ function fromWall(year, monthIndex, day, hour, minute, zone) {
12747
+ if (!zone) return new Date(year, monthIndex, day, hour, minute);
12748
+ const asUtc = Date.UTC(year, monthIndex, day, hour, minute);
12749
+ let instant = asUtc - zoneOffsetMs(asUtc, zone);
12750
+ instant = asUtc - zoneOffsetMs(instant, zone);
12751
+ return new Date(instant);
12752
+ }
12753
+
12754
+ // src/services/harness/codexUsageLimit.ts
12755
+ var CODEX_RESET_RE = /[Tt]ry again at (?:([A-Z][a-z]{2}) (\d{1,2})(?:st|nd|rd|th)?, (\d{4}) )?(\d{1,2}):(\d{2}) ([AP]M)\./g;
12756
+ function codexUsageLimitReset(text3, now) {
12757
+ const m = lastMatch(CODEX_RESET_RE, text3);
12758
+ if (!m) return null;
12759
+ const [, mon, day, year, hour, minute, meridiem] = m;
12760
+ const wall = { hour: hour24(hour, meridiem), minute: Number(minute) };
12761
+ if (mon) {
12762
+ const at = month(mon);
12763
+ if (at === void 0) return null;
12764
+ Object.assign(wall, { month: at, day: Number(day), year: Number(year) });
12765
+ }
12766
+ return placeWallTime(wall, void 0, now);
12767
+ }
12768
+ function mergeCodexRateLimits(known, params) {
12769
+ const snapshot = params?.rateLimits;
12770
+ if (!snapshot || typeof snapshot !== "object") return known;
12771
+ const id = snapshot.limitId ?? "";
12772
+ const before = known.get(id);
12773
+ const next = new Map(known);
12774
+ next.set(id, { limitId: id, primary: snapshot.primary ?? before?.primary, secondary: snapshot.secondary ?? before?.secondary });
12775
+ return next;
12776
+ }
12777
+ function isCodexUsageLimit(error) {
12778
+ const info = error?.codexErrorInfo;
12779
+ if (info === void 0 || info === null || info === "other") return void 0;
12780
+ if (info === "usageLimitExceeded") return true;
12781
+ return typeof info === "object" && "usageLimitExceeded" in info;
12782
+ }
12783
+ function codexUsageLimitSignal(error, limits, now) {
12784
+ const limited = isCodexUsageLimit(error);
12785
+ if (limited !== true) return limited;
12786
+ const resets = [...limits.values()].flatMap((snapshot) => [snapshot.primary, snapshot.secondary]).flatMap((window) => window && (window.usedPercent ?? 0) >= 100 && typeof window.resetsAt === "number" ? [window.resetsAt * 1e3] : []).filter((at) => at > now.getTime());
12787
+ return resets.length > 0 ? { resetsAt: new Date(Math.max(...resets)).toISOString() } : {};
12788
+ }
12789
+
12546
12790
  // src/services/harness/CodexAdapter.ts
12547
12791
  var HANDSHAKE_TIMEOUT_MS = 3e4;
12548
12792
  var MCP_ATTACH_TIMEOUT_MS2 = 1e4;
@@ -12686,6 +12930,11 @@ var CodexAdapter = class extends StdioAgentAdapter {
12686
12930
  turn = null;
12687
12931
  /** The last turn this adapter ended — so its second end signal, landing late, ends nothing. */
12688
12932
  endedTurnId = null;
12933
+ /**
12934
+ * The account's limit windows as Codex last reported them. Only read when a
12935
+ * turn fails on the usage limit; an update on its own acts on nothing.
12936
+ */
12937
+ rateLimits = /* @__PURE__ */ new Map();
12689
12938
  /** An interrupt was asked for during this turn, so however it ends, it was cut short. */
12690
12939
  interruptRequested = false;
12691
12940
  /**
@@ -13081,6 +13330,9 @@ ${this.exitMessage()}`);
13081
13330
  case "thread/tokenUsage/updated":
13082
13331
  this.emitUsage(msg.params, emit);
13083
13332
  return;
13333
+ case "account/rateLimits/updated":
13334
+ this.rateLimits = mergeCodexRateLimits(this.rateLimits, msg.params);
13335
+ return;
13084
13336
  case "turn/started": {
13085
13337
  if (this.subagentOf(msg.params?.threadId)) return;
13086
13338
  this.turnHasText = false;
@@ -13121,7 +13373,7 @@ ${this.exitMessage()}`);
13121
13373
  const failure = msg.params;
13122
13374
  if (failure?.willRetry) return;
13123
13375
  this.closeTurn(emit);
13124
- emit({ type: "error", message: failure?.error?.message || "Codex ended the turn with an error." });
13376
+ emit(this.turnFailed(failure?.error));
13125
13377
  return;
13126
13378
  }
13127
13379
  case "turn/completed": {
@@ -13132,7 +13384,7 @@ ${this.exitMessage()}`);
13132
13384
  if (this.interruptRequested || turn?.status === "interrupted") {
13133
13385
  emit({ type: "turn_end", interrupted: true });
13134
13386
  } else if (turn?.status === "failed") {
13135
- emit({ type: "error", message: turn.error?.message || "Codex ended the turn with an error." });
13387
+ emit(this.turnFailed(turn.error));
13136
13388
  } else {
13137
13389
  emit({ type: "turn_end" });
13138
13390
  }
@@ -13144,6 +13396,11 @@ ${this.exitMessage()}`);
13144
13396
  return;
13145
13397
  }
13146
13398
  }
13399
+ turnFailed(error) {
13400
+ const message = error?.message || "Codex ended the turn with an error.";
13401
+ const usageLimit = codexUsageLimitSignal(error, this.rateLimits, /* @__PURE__ */ new Date());
13402
+ return usageLimit === void 0 ? { type: "error", message } : { type: "error", message, usageLimit };
13403
+ }
13147
13404
  turnStarted(id) {
13148
13405
  const turn = this.turn;
13149
13406
  if (!turn || turn.id || typeof id !== "string") return;
@@ -13601,9 +13858,9 @@ function recordUnusable(servers, approval) {
13601
13858
  for (const server4 of servers) approval?.record?.(startRequest(server4), false, "unsupported");
13602
13859
  }
13603
13860
  var NOT_APPROVED = "not approved for planning";
13604
- function heldServersNote(held, unreadable = []) {
13861
+ function heldServersNote(held2, unreadable = []) {
13605
13862
  const lines2 = [
13606
- ...held.map(({ server: server4, reason }) => `- ${server4.name} (${server4.source}): ${reason}`),
13863
+ ...held2.map(({ server: server4, reason }) => `- ${server4.name} (${server4.source}): ${reason}`),
13607
13864
  ...unreadable.map((file) => `- ${file.source}: Ordewell could not read it (${file.reason})`)
13608
13865
  ];
13609
13866
  if (lines2.length === 0) return "";
@@ -13740,8 +13997,8 @@ var PendingSteers = class {
13740
13997
  * False when it was already delivered, so a late failure does not recall it.
13741
13998
  */
13742
13999
  forget(id) {
13743
- for (const [messageId, held] of this.pending) {
13744
- if (held !== id) continue;
14000
+ for (const [messageId, held2] of this.pending) {
14001
+ if (held2 !== id) continue;
13745
14002
  this.pending.delete(messageId);
13746
14003
  return true;
13747
14004
  }
@@ -13819,9 +14076,9 @@ var ChildSessions = class {
13819
14076
  adopt(child, callId) {
13820
14077
  if (this.owners.get(child)) return null;
13821
14078
  this.owners.set(child, callId);
13822
- const held = this.held.get(child) ?? [];
14079
+ const held2 = this.held.get(child) ?? [];
13823
14080
  this.held.delete(child);
13824
- return held;
14081
+ return held2;
13825
14082
  }
13826
14083
  owns(callId) {
13827
14084
  return [...this.owners.values()].includes(callId);
@@ -14345,11 +14602,11 @@ var OpenCodeV2 = class {
14345
14602
  adoptChild(data, state, onEvent, turn) {
14346
14603
  const child = data.metadata?.sessionID;
14347
14604
  const callId = data.id;
14348
- const held = child && callId ? state.children.adopt(child, callId) : null;
14349
- if (!held || !callId) return;
14605
+ const held2 = child && callId ? state.children.adopt(child, callId) : null;
14606
+ if (!held2 || !callId) return;
14350
14607
  const model = state.stepModels.get(data.assistantMessageID ?? "");
14351
14608
  onEvent({ type: "subagent_started", subagentId: callId, brief: state.briefs.get(callId) ?? "", ...model ? { model } : {} });
14352
- for (const frame of held) this.onFrame(frame, state, turn, onEvent);
14609
+ for (const frame of held2) this.onFrame(frame, state, turn, onEvent);
14353
14610
  }
14354
14611
  finishChild(callId, success, output, state, onEvent) {
14355
14612
  if (!state.children.owns(callId)) return;
@@ -15061,13 +15318,13 @@ var OpenCodeAdapter = class {
15061
15318
  trackSubagent(part, callId, turn, onEvent) {
15062
15319
  const state = part.state;
15063
15320
  const child = state?.metadata?.sessionId;
15064
- const held = child ? turn.children.adopt(child, callId) : null;
15065
- if (held) {
15321
+ const held2 = child ? turn.children.adopt(child, callId) : null;
15322
+ if (held2) {
15066
15323
  const input = state?.input ?? {};
15067
15324
  const brief2 = typeof input.description === "string" ? input.description : typeof input.prompt === "string" ? input.prompt : "";
15068
15325
  const model = flatModelId(state?.metadata?.model?.providerID, state?.metadata?.model?.modelID);
15069
15326
  onEvent({ type: "subagent_started", subagentId: callId, brief: brief2, ...model ? { model } : {} });
15070
- for (const frame of held) this.onFrame(frame, turn, onEvent);
15327
+ for (const frame of held2) this.onFrame(frame, turn, onEvent);
15071
15328
  }
15072
15329
  const status = state?.status;
15073
15330
  if (status === "completed" || status === "error") {
@@ -15297,61 +15554,6 @@ var OPENCODE_REPO_CONFIG = {
15297
15554
  }
15298
15555
  };
15299
15556
 
15300
- // src/utils/wallTime.ts
15301
- var MONTHS = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
15302
- function lastMatch(re, text3) {
15303
- let last = null;
15304
- for (const m of text3.matchAll(re)) last = m;
15305
- return last;
15306
- }
15307
- function hour24(hour, meridiem) {
15308
- const h = Number(hour) % 12;
15309
- return meridiem.toLowerCase() === "pm" ? h + 12 : h;
15310
- }
15311
- function month(name) {
15312
- if (!name) return void 0;
15313
- const at = MONTHS.indexOf(name.toLowerCase());
15314
- return at < 0 ? void 0 : at;
15315
- }
15316
- function placeWallTime(wall, zone, now) {
15317
- const today = wallFields(now, zone);
15318
- const year = wall.year ?? today.year;
15319
- const monthIndex = wall.month ?? today.month;
15320
- const day = wall.day ?? today.day;
15321
- const at = fromWall(year, monthIndex, day, wall.hour, wall.minute, zone);
15322
- if (wall.day !== void 0 || at.getTime() >= now.getTime() - HOUR_MS) return at;
15323
- return fromWall(year, monthIndex, day + 1, wall.hour, wall.minute, zone);
15324
- }
15325
- var HOUR_MS = 60 * 6e4;
15326
- function wallFields(instant, zone) {
15327
- if (!zone) {
15328
- return { year: instant.getFullYear(), month: instant.getMonth(), day: instant.getDate(), hour: instant.getHours(), minute: instant.getMinutes(), second: instant.getSeconds() };
15329
- }
15330
- const parts = new Intl.DateTimeFormat("en-US", {
15331
- timeZone: zone,
15332
- hourCycle: "h23",
15333
- year: "numeric",
15334
- month: "numeric",
15335
- day: "numeric",
15336
- hour: "numeric",
15337
- minute: "numeric",
15338
- second: "numeric"
15339
- }).formatToParts(instant);
15340
- const field = (type) => Number(parts.find((p) => p.type === type)?.value ?? 0);
15341
- return { year: field("year"), month: field("month") - 1, day: field("day"), hour: field("hour"), minute: field("minute"), second: field("second") };
15342
- }
15343
- function zoneOffsetMs(instant, zone) {
15344
- const f = wallFields(new Date(instant), zone);
15345
- return Date.UTC(f.year, f.month, f.day, f.hour, f.minute, f.second) - Math.floor(instant / 1e3) * 1e3;
15346
- }
15347
- function fromWall(year, monthIndex, day, hour, minute, zone) {
15348
- if (!zone) return new Date(year, monthIndex, day, hour, minute);
15349
- const asUtc = Date.UTC(year, monthIndex, day, hour, minute);
15350
- let instant = asUtc - zoneOffsetMs(asUtc, zone);
15351
- instant = asUtc - zoneOffsetMs(instant, zone);
15352
- return new Date(instant);
15353
- }
15354
-
15355
15557
  // src/services/harness/claudeUsageLimit.ts
15356
15558
  var CLAUDE_RESET_RE = /You['’]ve hit your [^\n]*?· (?:your [^·\n]*? )?resets (?:([A-Z][a-z]{2}) (\d{1,2}), (?:(\d{4}), )?)?(\d{1,2})(?::(\d{2}))?([ap]m) \(([A-Za-z0-9_+\-/]+)\)/g;
15357
15559
  function claudeUsageLimitReset(text3, now) {
@@ -15367,21 +15569,6 @@ function claudeUsageLimitReset(text3, now) {
15367
15569
  return placeWallTime(wall, zone, now);
15368
15570
  }
15369
15571
 
15370
- // src/services/harness/codexUsageLimit.ts
15371
- var CODEX_RESET_RE = /[Tt]ry again at (?:([A-Z][a-z]{2}) (\d{1,2})(?:st|nd|rd|th)?, (\d{4}) )?(\d{1,2}):(\d{2}) ([AP]M)\./g;
15372
- function codexUsageLimitReset(text3, now) {
15373
- const m = lastMatch(CODEX_RESET_RE, text3);
15374
- if (!m) return null;
15375
- const [, mon, day, year, hour, minute, meridiem] = m;
15376
- const wall = { hour: hour24(hour, meridiem), minute: Number(minute) };
15377
- if (mon) {
15378
- const at = month(mon);
15379
- if (at === void 0) return null;
15380
- Object.assign(wall, { month: at, day: Number(day), year: Number(year) });
15381
- }
15382
- return placeWallTime(wall, void 0, now);
15383
- }
15384
-
15385
15572
  // src/services/harness/connectors.ts
15386
15573
  var CONNECTORS = {
15387
15574
  "claude-code": { create: (deps) => new ClaudeCodeAdapter(deps), ordewellTools: CLAUDE_ORDEWELL, repoConfig: CLAUDE_REPO_CONFIG, usageLimitReset: claudeUsageLimitReset },
@@ -15780,7 +15967,8 @@ async function discoverGeminiModels(apiKey, baseUrl, fetchImpl) {
15780
15967
  modelId,
15781
15968
  modelLabel: m.displayName || modelId,
15782
15969
  runnerProvider: "gemini",
15783
- variants: []
15970
+ variants: [],
15971
+ ...typeof m.inputTokenLimit === "number" && m.inputTokenLimit > 0 ? { contextWindow: m.inputTokenLimit } : {}
15784
15972
  });
15785
15973
  }
15786
15974
  }
@@ -16188,6 +16376,7 @@ function parseMaxParallel(value) {
16188
16376
  // src/services/SettingsService.ts
16189
16377
  var fs10 = __toESM(require("fs"));
16190
16378
  var path26 = __toESM(require("path"));
16379
+ var import_crypto10 = require("crypto");
16191
16380
 
16192
16381
  // src/services/TaskOps.ts
16193
16382
  var import_uuid3 = require("uuid");
@@ -16241,7 +16430,14 @@ function clampThinkingEffort(effort, variants) {
16241
16430
  }
16242
16431
  return void 0;
16243
16432
  }
16244
- function coerceAssignments(tasks, perRunnerAllowlist, modelsByRunner) {
16433
+ function coerceAssignments(tasks, perRunnerAllowlist, modelsByRunner, planned = []) {
16434
+ const prior = new Map(flattenTasks(planned).map((t) => [t.id, t]));
16435
+ const unwritten = (task) => {
16436
+ const was = prior.get(task.id);
16437
+ if (!was?.assignedModel || !task.assignedModel || was.assignedRunner !== task.assignedRunner) return void 0;
16438
+ if (was.assignedModel.modelId !== task.assignedModel.modelId || was.assignedModel.thinkingEffort !== task.assignedModel.thinkingEffort) return void 0;
16439
+ return was;
16440
+ };
16245
16441
  const clampVariant = (task) => {
16246
16442
  if (!task.assignedModel || !modelsByRunner) return task;
16247
16443
  const model = modelsByRunner[task.assignedRunner]?.find(
@@ -16270,6 +16466,11 @@ function coerceAssignments(tasks, perRunnerAllowlist, modelsByRunner) {
16270
16466
  };
16271
16467
  const coerceOne = (task) => {
16272
16468
  if (task.type === "user") return task;
16469
+ const kept = unwritten(task);
16470
+ if (kept) {
16471
+ const left = { ...task, assignedModel: kept.assignedModel };
16472
+ return kept.status === "in_progress" || kept.status === "awaiting_user" ? left : clampVariant(left);
16473
+ }
16273
16474
  const allowlist = allowedFor(task.assignedRunner);
16274
16475
  if (!task.assignedModel) {
16275
16476
  if (allowlist && allowlist.length > 0) {
@@ -16332,7 +16533,7 @@ function blockedByLabel(refs) {
16332
16533
  function skippedLabel(reason) {
16333
16534
  return reason ? `Skipped: ${reason}` : "Skipped";
16334
16535
  }
16335
- var WAITS_FOR_APPROVAL_LABEL = "Waiting for your approval to start";
16536
+ var WAITS_FOR_APPROVAL_LABEL = "Waits for your approval";
16336
16537
  function approvalLabel(count) {
16337
16538
  if (!count) return null;
16338
16539
  return count > 1 ? `Waiting for approval (${count})` : "Waiting for approval";
@@ -16517,6 +16718,15 @@ function lockedTaskRefusal(task, action = "changed") {
16517
16718
  if (task.status === "completed") return `"${task.title}" is completed and cannot be ${action}: rearm it to run it again, or add a new task for further work`;
16518
16719
  return null;
16519
16720
  }
16721
+ function liveModelRefusal(task) {
16722
+ return `"${task.title}" has a live runner, so its model and effort cannot change now: stop the task first, or use the runner's own /model`;
16723
+ }
16724
+ function liveRunnerRefusal(task) {
16725
+ return `"${task.title}" has a live runner, so its runner cannot change now: stop the task first`;
16726
+ }
16727
+ function sameAssignment(a, b) {
16728
+ return a?.modelId === b?.modelId && a?.thinkingEffort === b?.thinkingEffort;
16729
+ }
16520
16730
  var AI_ONLY_FIELDS = ["assignedModel", "taskMode", "autonomy", "ops"];
16521
16731
  function checkModelAndModeValidity(runner, assignedModel, taskMode, catalog) {
16522
16732
  if (!catalog) return { ok: true };
@@ -16576,11 +16786,18 @@ function validateTaskEdit(actor, tasks, taskId, changes, catalog) {
16576
16786
  const { task: target, parent } = found;
16577
16787
  const locked = actor === "planner" ? lockedTaskRefusal(target, "modified") : null;
16578
16788
  if (locked) return { ok: false, error: locked };
16789
+ const coherence = typeCoherenceCheck(target, changes);
16790
+ const landing = coherence?.clear?.includes("assignedModel") ? { ...changes, assignedModel: void 0 } : changes;
16791
+ if ("assignedModel" in landing && !sameAssignment(landing.assignedModel, target.assignedModel) && catalog?.liveRunner?.(taskId)) {
16792
+ return { ok: false, error: liveModelRefusal(target) };
16793
+ }
16794
+ if ("assignedRunner" in changes && changes.assignedRunner !== target.assignedRunner && catalog?.liveRunner?.(taskId)) {
16795
+ return { ok: false, error: liveRunnerRefusal(target) };
16796
+ }
16579
16797
  if (changes.dependencies) {
16580
16798
  const depCheck = canSetDependencies(parent?.subtasks ?? tasks, taskId, changes.dependencies);
16581
16799
  if (!depCheck.ok) return depCheck;
16582
16800
  }
16583
- const coherence = typeCoherenceCheck(target, changes);
16584
16801
  if (coherence && !coherence.ok) return coherence;
16585
16802
  const ops = opsCheck(target, parent !== null, changes);
16586
16803
  if (ops) return ops;
@@ -16818,9 +17035,9 @@ function idsOf(op) {
16818
17035
  return ids[next++];
16819
17036
  };
16820
17037
  }
16821
- function applyTaskOps(currentTasks, newOps, runners, catalog, held = []) {
16822
- const ops = [...held, ...newOps];
16823
- const opLabel = (i) => i < held.length ? `earlier op ${i + 1} (${ops[i].op})` : `op ${i - held.length + 1} (${ops[i].op})`;
17038
+ function applyTaskOps(currentTasks, newOps, runners, catalog, held2 = []) {
17039
+ const ops = [...held2, ...newOps];
17040
+ const opLabel = (i) => i < held2.length ? `earlier op ${i + 1} (${ops[i].op})` : `op ${i - held2.length + 1} (${ops[i].op})`;
16824
17041
  let tasks = JSON.parse(JSON.stringify(currentTasks));
16825
17042
  const errors = [];
16826
17043
  const summary = [];
@@ -17512,23 +17729,34 @@ function createWakePolicy(options) {
17512
17729
  // src/services/runnerExit.ts
17513
17730
  var USAGE_LIMIT_RE = /\b(?:usage|session|weekly|daily|monthly) limit\b|\brate limit (?:exceeded|reached)\b|\brate[- ]limited\b|\blimit (?:will )?reset\b|\bquota (?:exceeded|reached)\b|\btoo many requests\b/i;
17514
17731
  var USAGE_LIMIT_TAIL = 4096;
17515
- function classifyRunnerStop(output) {
17516
- return USAGE_LIMIT_RE.test(output.slice(-USAGE_LIMIT_TAIL)) ? "usage-limit" : "stopped";
17732
+ function classifyRunnerStop(said2) {
17733
+ if (typeof said2 !== "string" && said2.usageLimit !== void 0) return said2.usageLimit ? "usage-limit" : "stopped";
17734
+ return USAGE_LIMIT_RE.test(wordsOf(said2).slice(-USAGE_LIMIT_TAIL)) ? "usage-limit" : "stopped";
17735
+ }
17736
+ function wordsOf(said2) {
17737
+ return typeof said2 === "string" ? said2 : said2.message;
17517
17738
  }
17518
17739
  function stopsRunner(reason) {
17519
17740
  return reason !== "stop" && reason !== "load";
17520
17741
  }
17521
- function readUsageLimitReset(runner, output, now = /* @__PURE__ */ new Date()) {
17742
+ function readUsageLimitReset(runner, said2, now = /* @__PURE__ */ new Date()) {
17743
+ const reset = (typeof said2 === "string" ? null : reportedReset(said2)) ?? readReset(runner, wordsOf(said2), now);
17744
+ if (!reset || reset.getTime() < now.getTime() - DAY_MS) return null;
17745
+ return reset;
17746
+ }
17747
+ function reportedReset(failure) {
17748
+ const at = failure.usageLimit ? failure.usageLimit.resetsAt : void 0;
17749
+ const reset = at === void 0 ? null : new Date(at);
17750
+ return reset && !Number.isNaN(reset.getTime()) ? reset : null;
17751
+ }
17752
+ function readReset(runner, output, now) {
17522
17753
  const read2 = connectorFor(runner)?.usageLimitReset;
17523
17754
  if (!read2) return null;
17524
- let reset;
17525
17755
  try {
17526
- reset = read2(output.slice(-USAGE_LIMIT_TAIL), now);
17756
+ return read2(output.slice(-USAGE_LIMIT_TAIL), now);
17527
17757
  } catch {
17528
17758
  return null;
17529
17759
  }
17530
- if (!reset || reset.getTime() < now.getTime() - DAY_MS) return null;
17531
- return reset;
17532
17760
  }
17533
17761
  var DAY_MS = 24 * 60 * 6e4;
17534
17762
 
@@ -17709,15 +17937,15 @@ var ReplySplitter = class {
17709
17937
  push(segmentId, text3) {
17710
17938
  const state = this.segments.get(segmentId);
17711
17939
  if (state === "text" || state === "plan") return { route: state, text: text3 };
17712
- const held = (state?.held ?? "") + text3;
17713
- const opensWithObject = opensWithJsonObject(held);
17940
+ const held2 = (state?.held ?? "") + text3;
17941
+ const opensWithObject = opensWithJsonObject(held2);
17714
17942
  if (opensWithObject === void 0) {
17715
- this.segments.set(segmentId, { held });
17943
+ this.segments.set(segmentId, { held: held2 });
17716
17944
  return null;
17717
17945
  }
17718
17946
  const route = opensWithObject ? "plan" : "text";
17719
17947
  this.segments.set(segmentId, route);
17720
- return { route, text: held };
17948
+ return { route, text: held2 };
17721
17949
  }
17722
17950
  };
17723
17951
  var TurnStream = class {
@@ -17872,6 +18100,7 @@ function plannerTranscript(history) {
17872
18100
  const loadsOf = /* @__PURE__ */ new Map();
17873
18101
  let owner = -1;
17874
18102
  for (const entry of history) {
18103
+ if (entry.kind === "error") continue;
17875
18104
  if (entry.kind === "skill_load" && entry.skill) {
17876
18105
  const loads = entry.skill.invokedBy === "user" ? loadsOf.get(owner) : void 0;
17877
18106
  if (loads) loads.push(entry.skill);
@@ -17960,6 +18189,8 @@ function handoffOf(run) {
17960
18189
  path: repo.path,
17961
18190
  integrationBranch: repo.integrationBranch,
17962
18191
  baseRef: repo.baseRef,
18192
+ ...repo.baseBranch ? { baseBranch: repo.baseBranch } : {},
18193
+ ...repo.startedDetached ? { startedDetached: true } : {},
17963
18194
  landed: merged.filter((r) => r.repos[repo.path]?.changed).map(entry)
17964
18195
  })),
17965
18196
  landed: merged.map(entry)
@@ -19289,6 +19520,9 @@ ${task.prompt ?? task.description}`;
19289
19520
  }
19290
19521
 
19291
19522
  // src/services/continuation.ts
19523
+ function sessionOwner(task) {
19524
+ return task.runnerSessionOwner ?? task.attemptUsage?.at(-1)?.runner;
19525
+ }
19292
19526
  function continuability(task) {
19293
19527
  const refuse = (reason) => ({ ok: false, reason: `Task "${task.title}" cannot be continued: ${reason}` });
19294
19528
  if (task.type !== "ai") return refuse("it is not run by an agent.");
@@ -19298,6 +19532,10 @@ function continuability(task) {
19298
19532
  const finished = task.status === "completed" || task.status === "failed" || task.status === "skipped";
19299
19533
  if (!finished && !pausedOnLimit(task)) return refuse("only a completed, failed or skipped task, or one paused on a usage limit, can be.");
19300
19534
  if (!task.runnerSessionId) return refuse("its runner left no saved session to resume. Use Retry instead.");
19535
+ const owner = sessionOwner(task);
19536
+ if (owner && owner !== task.assignedRunner) {
19537
+ return refuse(`its saved session belongs to ${owner}; Retry starts fresh on ${task.assignedRunner} and carries over its files.`);
19538
+ }
19301
19539
  return { ok: true, sessionId: task.runnerSessionId };
19302
19540
  }
19303
19541
  function canContinue(task) {
@@ -19421,6 +19659,7 @@ function serializePlan(plan) {
19421
19659
  runners: plan.runners,
19422
19660
  generatedAt: plan.generatedAt,
19423
19661
  runSettings: plan.runSettings,
19662
+ ...plan.plannerResend ? { plannerResend: plan.plannerResend } : {},
19424
19663
  conversationHistory: surfaceConversation(plan.conversationHistory),
19425
19664
  prdMarkdown: plan.prdMarkdown
19426
19665
  };
@@ -19461,6 +19700,70 @@ function markOf(was, now) {
19461
19700
  return editedFields(was, now).length > 0 || was.status !== now.status ? "changed" : void 0;
19462
19701
  }
19463
19702
 
19703
+ // src/services/AssignmentScope.ts
19704
+ var OVER = /* @__PURE__ */ new Set(["completed", "skipped"]);
19705
+ function assignmentIssue(task, scope) {
19706
+ if (task.type !== "ai" || OVER.has(task.status)) return null;
19707
+ if (!scope.enabledRunners.includes(task.assignedRunner)) return { kind: "runner-off", runner: task.assignedRunner };
19708
+ const model = task.assignedModel;
19709
+ const allowed = effectiveAllowlist(scope.allowlist[task.assignedRunner], task.assignedRunner, scope.models);
19710
+ if (model && allowed && !allowed.includes(model.modelId)) {
19711
+ return { kind: "model-off-list", runner: task.assignedRunner, modelId: model.modelId, modelLabel: model.modelLabel || model.modelId };
19712
+ }
19713
+ return null;
19714
+ }
19715
+ function assignmentIssueText(issue) {
19716
+ return issue.kind === "runner-off" ? `${issue.runner} is switched off` : `${issue.modelLabel} is not in the allowlist`;
19717
+ }
19718
+ function assignmentConsequence({ status, awaitingReason, resumeAt }) {
19719
+ if (status === "failed") return "failed; a retry would run it there unless moved";
19720
+ if (status === "awaiting_user" && awaitingReason === "usage-limit") {
19721
+ return resumeAt ? "paused on a usage limit; resumes there at the reset unless moved" : "paused on a usage limit; a retry would run it there unless moved";
19722
+ }
19723
+ if (status === "awaiting_user" && (awaitingReason === "conflict" || awaitingReason === "files-changed")) return "waiting for the user; what runs next runs there unless moved";
19724
+ if (status === "in_progress" || status === "awaiting_user") return "already running as assigned";
19725
+ return "runs as assigned unless moved";
19726
+ }
19727
+
19728
+ // src/services/RunnerChange.ts
19729
+ var sameSet = (a, b) => a === void 0 || b === void 0 ? a === b : a.length === b.length && a.every((id) => b.includes(id));
19730
+ function scopeChange(told, now) {
19731
+ const switchedOn = now.enabledRunners.filter((r) => !told.enabledRunners.includes(r));
19732
+ const switchedOff = told.enabledRunners.filter((r) => !now.enabledRunners.includes(r));
19733
+ const models = now.models;
19734
+ const allowlists = [.../* @__PURE__ */ new Set([...Object.keys(told.allowlist), ...Object.keys(now.allowlist)])].map((runner) => ({
19735
+ runner,
19736
+ before: effectiveAllowlist(told.allowlist[runner], runner, models),
19737
+ after: effectiveAllowlist(now.allowlist[runner], runner, models)
19738
+ })).filter((c) => !sameSet(c.before, c.after));
19739
+ return switchedOn.length || switchedOff.length || allowlists.length ? { switchedOn, switchedOff, allowlists } : null;
19740
+ }
19741
+ function flaggedTasks(rows, scope) {
19742
+ return rows.flatMap(({ ref, task }) => {
19743
+ const issue = assignmentIssue(task, scope);
19744
+ return issue ? [{ ref, title: task.title, task, text: assignmentIssueText(issue), status: task.status, awaitingReason: task.awaitingReason, resumeAt: task.resumeAt }] : [];
19745
+ });
19746
+ }
19747
+ function allowlistLine({ runner, before, after }) {
19748
+ if (!after) return `${runner} is no longer limited: any of its models may be assigned.`;
19749
+ if (!before) return `${runner} is now limited to: ${after.join(", ")}.`;
19750
+ const added = after.filter((id) => !before.includes(id));
19751
+ const removed = before.filter((id) => !after.includes(id));
19752
+ return `${runner} models: ${[added.length ? `now allowed ${added.join(", ")}` : "", removed.length ? `no longer allowed ${removed.join(", ")}` : ""].filter(Boolean).join("; ")}.`;
19753
+ }
19754
+ function runnersChangedBlock(change, flagged, tools) {
19755
+ return [
19756
+ "<runners_changed>",
19757
+ "The user changed which runners and models you may assign since your last turn.",
19758
+ change.switchedOn.length ? `Runners switched on: ${change.switchedOn.join(", ")}.` : "",
19759
+ change.switchedOff.length ? `Runners switched off: ${change.switchedOff.join(", ")}.` : "",
19760
+ ...change.allowlists.map(allowlistLine),
19761
+ flagged.length ? ["These tasks are now outside that:", ...flagged.map((f) => `- ${f.ref} "${f.title}" (${f.text}) \u2014 ${assignmentConsequence(f)}`)].join("\n") : "",
19762
+ `${tools ? "list_runners and list_models give" : "<available_models> shows"} what you may assign now.${flagged.length ? " Consider moving the tasks above to an enabled runner and an allowed model, or tell the user why you left them." : ""}`,
19763
+ "</runners_changed>"
19764
+ ].filter(Boolean).join("\n");
19765
+ }
19766
+
19464
19767
  // src/services/PlannerConversation.ts
19465
19768
  var CATALOG_MODEL_CAP = 100;
19466
19769
  var PLANNER_TOOLS_REMINDER = "Runners, models and modes may have changed since you last read them: call list_runners and list_models just before submit_plan.";
@@ -19525,6 +19828,17 @@ var PlannerTurnFailedError = class extends Error {
19525
19828
  this.name = "PlannerTurnFailedError";
19526
19829
  }
19527
19830
  };
19831
+ var PlannerUsageLimitError = class extends PlannerTurnFailedError {
19832
+ resendAt;
19833
+ /** Why nothing is sent again on its own, when nothing is. */
19834
+ held;
19835
+ constructor(limit) {
19836
+ super(limit.text);
19837
+ this.name = "PlannerUsageLimitError";
19838
+ this.resendAt = limit.resendAt;
19839
+ if (limit.held) this.held = limit.held;
19840
+ }
19841
+ };
19528
19842
  var PlannerTurnStoppedError = class extends Error {
19529
19843
  constructor(options) {
19530
19844
  super("The planner turn was stopped.", options);
@@ -19550,6 +19864,8 @@ var PlannerConversation = class {
19550
19864
  savedInBackground = 0;
19551
19865
  /** What the planner was last told of autorun: by its opening prompt, or a turn's block since. */
19552
19866
  toldAutorun = false;
19867
+ /** The runners and allowlist the planner was last told of: its opening prompt, or a turn's block since. Null until a conversation opens. */
19868
+ toldScope = null;
19553
19869
  live = null;
19554
19870
  openTurn = null;
19555
19871
  get transcript() {
@@ -19665,8 +19981,8 @@ var PlannerConversation = class {
19665
19981
  * from the moment it is handed in until it lands, is refused, or the turn
19666
19982
  * ends: the backend's reply claims it, but it has not landed by then.
19667
19983
  */
19668
- showHeld(turn, held) {
19669
- const result = held?.kind === "task_ops" ? this.host.validateOps(held.ops) : void 0;
19984
+ showHeld(turn, held2) {
19985
+ const result = held2?.kind === "task_ops" ? this.host.validateOps(held2.ops) : void 0;
19670
19986
  if (!result?.ok) {
19671
19987
  this.clearHeld(turn);
19672
19988
  return;
@@ -19685,9 +20001,9 @@ var PlannerConversation = class {
19685
20001
  * a whole plan, which an edit cannot be layered on.
19686
20002
  */
19687
20003
  pendingOps() {
19688
- const held = this.openTurn?.submitted;
19689
- if (!held) return [];
19690
- return held.kind === "task_ops" ? held.ops : null;
20004
+ const held2 = this.openTurn?.submitted;
20005
+ if (!held2) return [];
20006
+ return held2.kind === "task_ops" ? held2.ops : null;
19691
20007
  }
19692
20008
  /**
19693
20009
  * A read the planner makes through a tool. It spends the same per-turn budget
@@ -19727,7 +20043,8 @@ var PlannerConversation = class {
19727
20043
  history: plan.conversationHistory,
19728
20044
  researchLog: plan.researchLog,
19729
20045
  persisted: this.persisted,
19730
- savedInBackground: this.savedInBackground
20046
+ savedInBackground: this.savedInBackground,
20047
+ toldScope: this.toldScope
19731
20048
  };
19732
20049
  }
19733
20050
  /**
@@ -19741,7 +20058,9 @@ var PlannerConversation = class {
19741
20058
  * the undo is saved in turn, or a reload would bring the writes back.
19742
20059
  */
19743
20060
  restore(snapshot) {
19744
- if (this.host.plan() !== snapshot.plan || this.persisted !== snapshot.persisted) return false;
20061
+ if (this.host.plan() !== snapshot.plan) return false;
20062
+ this.toldScope = snapshot.toldScope;
20063
+ if (this.persisted !== snapshot.persisted) return false;
19745
20064
  const undo = () => {
19746
20065
  const actions = (snapshot.plan.conversationHistory ?? []).filter((entry) => entry.actor === "Ordewell setting" && !(snapshot.history ?? []).includes(entry));
19747
20066
  snapshot.plan.conversationHistory = actions.length > 0 ? [...snapshot.history ?? [], ...actions] : snapshot.history;
@@ -19776,8 +20095,9 @@ var PlannerConversation = class {
19776
20095
  * at it.
19777
20096
  */
19778
20097
  rewindTargets() {
20098
+ const opener = this.transcript.findIndex((m) => m.kind !== "error");
19779
20099
  return this.transcript.flatMap((m, index) => {
19780
- if (!isUserMessage(m) || index === 0) return [];
20100
+ if (!isUserMessage(m) || index === opener) return [];
19781
20101
  const line = m.content.split("\n")[0];
19782
20102
  const preview = line.length > REWIND_PREVIEW_WIDTH ? `${line.slice(0, REWIND_PREVIEW_WIDTH - 1)}\u2026` : line;
19783
20103
  return [{ index, preview, content: m.content, timestamp: m.timestamp }];
@@ -19904,8 +20224,10 @@ var PlannerConversation = class {
19904
20224
  return this.userTurn("supersede", goal, signal, async (userTurn) => {
19905
20225
  const opening = await prepare();
19906
20226
  this.assertCurrent(userTurn);
20227
+ userTurn.checkpoint = this.snapshot();
19907
20228
  this.recordUser(goal, (/* @__PURE__ */ new Date()).toISOString());
19908
20229
  this.toldAutorun = opening.autorun ?? false;
20230
+ this.toldScope = this.host.assignmentScope();
19909
20231
  const turn = await this.host.aiService().startConversation({
19910
20232
  ...opening,
19911
20233
  // The system prompt's goal is the text as typed; skill bodies ride the
@@ -19915,7 +20237,9 @@ var PlannerConversation = class {
19915
20237
  onProgress: userTurn.stream.sink(),
19916
20238
  signal: userTurn.signal
19917
20239
  });
19918
- return this.settle(await this.drainTaskQueries(turn, userTurn), userTurn);
20240
+ const settleable = await this.drainTaskQueries(turn, userTurn);
20241
+ this.assertNotLimited(settleable, userTurn);
20242
+ return this.settle(settleable, userTurn);
19919
20243
  });
19920
20244
  }
19921
20245
  /**
@@ -19948,13 +20272,16 @@ var PlannerConversation = class {
19948
20272
  // anything the turn appends as it runs goes after this point.
19949
20273
  at: priorHistory.length
19950
20274
  };
19951
- const contextBlock = [this.catalogBlock(), this.planContextBlock(), this.autorunBlock(), this.runUpdatesBlock()].filter(Boolean).join("\n\n");
20275
+ const toldBefore = this.toldScope;
20276
+ const contextBlock = [this.catalogBlock(), this.planContextBlock(), this.autorunBlock(), this.runnersChangedBlock(), this.runUpdatesBlock()].filter(Boolean).join("\n\n");
19952
20277
  const said2 = fromUser ? plannerMessage(message.text, message.skills) : "";
19953
20278
  const outgoing = [contextBlock, ...wakeUps, userTurn.wakeUp?.brief, said2].filter(Boolean).join("\n\n");
19954
20279
  const ai = this.host.aiService();
19955
20280
  try {
19956
20281
  const turn = this.canContinueLive() ? await ai.continueConversation(outgoing, userTurn.stream.sink(), signal) : await this.resume(outgoing, priorHistory, signal, userTurn.stream.sink());
19957
20282
  const settleable = await this.drainTaskQueries(turn, userTurn);
20283
+ if (settleable.kind === "message" && (settleable.failed || userTurn.signal.aborted)) this.toldScope = toldBefore;
20284
+ this.assertNotLimited(settleable, userTurn);
19958
20285
  if (userTurn.wakeUp) this.assertWakeUpAnswered(settleable, userTurn);
19959
20286
  this.assertCurrent(userTurn, settleable);
19960
20287
  return await this.settle(settleable, userTurn);
@@ -19973,6 +20300,34 @@ var PlannerConversation = class {
19973
20300
  if (userTurn.signal.aborted) throw new PlannerTurnStoppedError();
19974
20301
  if (turn.kind === "message" && turn.failed) throw new PlannerTurnFailedError(turn.text);
19975
20302
  }
20303
+ /**
20304
+ * A turn that ended on the planner's usage limit lands nothing. What it
20305
+ * wrote is taken back, its failure is kept in its place, and the limit's
20306
+ * wait is armed in the same save. Only a turn Ordewell started gets here,
20307
+ * so a limit is never acted on for a planner nobody asked anything of.
20308
+ */
20309
+ assertNotLimited(turn, userTurn) {
20310
+ if (turn.kind !== "message" || !turn.failed || userTurn.signal.aborted) return;
20311
+ const said2 = { message: turn.text, usageLimit: turn.usageLimit };
20312
+ if (classifyRunnerStop(said2) !== "usage-limit") return;
20313
+ this.assertCurrent(userTurn);
20314
+ if (userTurn.checkpoint) this.restore(userTurn.checkpoint);
20315
+ const limit = this.host.usageLimit(said2);
20316
+ this.host.mutate(() => {
20317
+ this.recordFailure(limit.text);
20318
+ return true;
20319
+ }, () => this.host.broadcastPlan());
20320
+ throw new PlannerUsageLimitError(limit);
20321
+ }
20322
+ /**
20323
+ * A failure kept in the transcript, drawn as one on reload too, and never
20324
+ * replayed to the planner. Stamped after the newest entry: surfaces take
20325
+ * only entries newer than the last they synced.
20326
+ */
20327
+ recordFailure(text3) {
20328
+ const newest = this.transcript.reduce((latest, e) => Math.max(latest, Date.parse(e.timestamp) || 0), 0);
20329
+ this.append("assistant", text3, { kind: "error", timestamp: new Date(Math.max(Date.now(), newest + 1)).toISOString() });
20330
+ }
19976
20331
  assertIdle(operation) {
19977
20332
  if (this.isTurnInFlight) throw new ConversationBusyError(operation);
19978
20333
  }
@@ -20114,6 +20469,7 @@ var PlannerConversation = class {
20114
20469
  const goal = opener || this.host.goal() || message;
20115
20470
  this.reset();
20116
20471
  this.toldAutorun = opening.autorun ?? false;
20472
+ this.toldScope = this.host.assignmentScope();
20117
20473
  return this.host.aiService().startConversation({
20118
20474
  ...opening,
20119
20475
  runners,
@@ -20352,6 +20708,24 @@ The plan is unchanged. Rephrase the request, or adjust the tasks manually.`,
20352
20708
  this.toldAutorun = now;
20353
20709
  return now ? AUTORUN_ON : AUTORUN_OFF;
20354
20710
  }
20711
+ /**
20712
+ * The user's runner or allowlist change, when the planner was not told yet —
20713
+ * the same once-per-change rule as autorun, for API and harness planners
20714
+ * alike. Tasks the change leaves outside the scope are named, not touched.
20715
+ *
20716
+ * A reloaded session is not told: {@link resume} takes the scope as it is
20717
+ * then, as it does the autorun rule, since the catalog the first turn carries
20718
+ * and `list_runners` already show the runners as they are now.
20719
+ */
20720
+ runnersChangedBlock() {
20721
+ const now = this.host.assignmentScope();
20722
+ const told = this.toldScope;
20723
+ this.toldScope = now;
20724
+ const change = told && scopeChange(told, now);
20725
+ if (!change) return null;
20726
+ const flagged = flaggedTasks(flattenTasksWithParents(this.host.tasks()), now);
20727
+ return runnersChangedBlock(change, flagged, this.host.aiService().plannerToolsAttached?.() ?? false);
20728
+ }
20355
20729
  runUpdatesBlock() {
20356
20730
  const updates = this.host.takeRunUpdates();
20357
20731
  return updates.length > 0 ? ["Run update:", ...updates.map((update) => `- ${update}`)].join("\n") : null;
@@ -20432,7 +20806,6 @@ var WAKE_DEBOUNCE_MS = 5e3;
20432
20806
  var WAKE_UP_TURN_TIMEOUT_MS = 10 * 6e4;
20433
20807
  var HUNG_TURN_GRACE_MS = 3e4;
20434
20808
  var DEFAULT_WAKE_UP_READ_BUDGET = 12;
20435
- var RESET_MARGIN_MS = 6e4;
20436
20809
  var DETAIL_CHARS = 240;
20437
20810
  var WakeUpTimeoutError = class extends Error {
20438
20811
  constructor(timeoutMs) {
@@ -20458,8 +20831,6 @@ var Supervisor = class {
20458
20831
  /** The run's end was reported: the scheduler can announce it more than once. */
20459
20832
  finished = false;
20460
20833
  runEndPending = false;
20461
- /** While the planner's usage limit holds wake-ups back: when it lets them go. */
20462
- limited = null;
20463
20834
  /** When each wake-up since the last pass was delivered, for the wake-up rate guard. */
20464
20835
  wakeUps = [];
20465
20836
  debounceMs;
@@ -20480,9 +20851,9 @@ var Supervisor = class {
20480
20851
  get awaitsRunEnd() {
20481
20852
  return this.runEndPending;
20482
20853
  }
20483
- /** Whether wake-ups alone may open a turn now: the supervisor watches, and no usage limit holds it. */
20854
+ /** Whether wake-ups alone may open a turn now: the supervisor watches. A usage limit holds every turn, and is the planner turns' to wait out. */
20484
20855
  mayWake() {
20485
- return this.watching && this.limited === null;
20856
+ return this.watching;
20486
20857
  }
20487
20858
  /**
20488
20859
  * What a wake-up turn is held to under the run's level. The brief is the
@@ -20549,8 +20920,6 @@ var Supervisor = class {
20549
20920
  /** The plan these events were about is gone: nothing gathered or waited on carries over. */
20550
20921
  reset() {
20551
20922
  this.disarm();
20552
- if (this.limited) clearTimeout(this.limited.timer);
20553
- this.limited = null;
20554
20923
  this.failures = 0;
20555
20924
  this.finished = false;
20556
20925
  this.runEndPending = false;
@@ -20601,9 +20970,13 @@ var Supervisor = class {
20601
20970
  }
20602
20971
  failed(err) {
20603
20972
  if (err instanceof PlannerTurnStoppedError || err instanceof PlannerTurnDiscardedError) return;
20973
+ if (err instanceof PlannerUsageLimitError) {
20974
+ if (err.resendAt === null) this.giveUp(`the planner hit its usage limit and ${err.held ?? "did not say when it resets"}`);
20975
+ return;
20976
+ }
20604
20977
  const reason = err instanceof Error ? err.message : String(err);
20605
- if (!(err instanceof WakeUpTimeoutError) && classifyRunnerStop(reason) === "usage-limit") {
20606
- this.waitForReset(reason);
20978
+ if (!(err instanceof WakeUpTimeoutError) && !(err instanceof PlannerTurnFailedError) && classifyRunnerStop(reason) === "usage-limit") {
20979
+ this.giveUp("the planner hit its usage limit and did not say when it resets");
20607
20980
  return;
20608
20981
  }
20609
20982
  this.failures++;
@@ -20614,22 +20987,6 @@ var Supervisor = class {
20614
20987
  }
20615
20988
  this.giveUp(`its wake-up turn failed twice (${brief(reason)})`);
20616
20989
  }
20617
- waitForReset(said2) {
20618
- const runner = this.host.plannerRunner();
20619
- const reset = runner ? readUsageLimitReset(runner, said2, new Date(this.now())) : null;
20620
- if (!reset) {
20621
- this.giveUp("the planner hit its usage limit and did not say when it resets");
20622
- return;
20623
- }
20624
- if (this.limited) clearTimeout(this.limited.timer);
20625
- const until = reset.getTime() + RESET_MARGIN_MS;
20626
- const timer = setTimeout(() => {
20627
- this.limited = null;
20628
- this.host.pump();
20629
- }, Math.max(0, until - this.now()));
20630
- this.limited = { until, timer };
20631
- this.host.notice("info", `The planner hit its usage limit. The supervisor's wake-ups wait until ${new Date(until).toISOString()}, and are delivered together then.`);
20632
- }
20633
20990
  /** The run goes on without a supervisor, and nothing that happened is lost (see {@link standDown}). */
20634
20991
  giveUp(reason) {
20635
20992
  this.host.goUnsupervised(reason);
@@ -20643,8 +21000,8 @@ var Supervisor = class {
20643
21000
  standDown() {
20644
21001
  this.disarm();
20645
21002
  this.runEndPending = false;
20646
- const held = this.policy.poll(Number.POSITIVE_INFINITY);
20647
- if (held) this.deliver(held);
21003
+ const held2 = this.policy.poll(Number.POSITIVE_INFINITY);
21004
+ if (held2) this.deliver(held2);
20648
21005
  }
20649
21006
  arm() {
20650
21007
  const due = this.policy.dueAt();
@@ -20835,78 +21192,224 @@ function runSettingsOf(settings) {
20835
21192
  }
20836
21193
  return out;
20837
21194
  }
21195
+ var SAVE_TRIES = 5;
21196
+ var LOCK_STALE_MS = 2e3;
21197
+ var LOCK_TIMEOUT_MS = 1e4;
21198
+ var LOCK_POLL_MS = 5;
21199
+ var LOCK_POLL_MAX_MS = 100;
21200
+ function sleepSync(ms) {
21201
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
21202
+ }
21203
+ function withFileLock(file, swap) {
21204
+ const lock = `${file}.lock`;
21205
+ const token = `${process.pid}:${(0, import_crypto10.randomUUID)()}`;
21206
+ const deadline = Date.now() + LOCK_TIMEOUT_MS;
21207
+ let last = null;
21208
+ let poll = LOCK_POLL_MS;
21209
+ for (; ; ) {
21210
+ try {
21211
+ const fd = fs10.openSync(lock, "wx");
21212
+ try {
21213
+ fs10.writeSync(fd, token);
21214
+ } finally {
21215
+ fs10.closeSync(fd);
21216
+ }
21217
+ break;
21218
+ } catch (err) {
21219
+ if (err.code !== "EEXIST") throw err;
21220
+ }
21221
+ if (Date.now() >= deadline) {
21222
+ throw new Error(`Cannot save the settings file: ${lock} is still held after ${Math.round(LOCK_TIMEOUT_MS / 1e3)} seconds by a process that is not finishing, or it cannot be removed. Check for a process stuck saving there, and delete the file if there is none.`);
21223
+ }
21224
+ let holder;
21225
+ let mtimeMs;
21226
+ try {
21227
+ holder = fs10.readFileSync(lock, "utf8");
21228
+ mtimeMs = fs10.statSync(lock).mtimeMs;
21229
+ } catch (err) {
21230
+ if (gone(err)) {
21231
+ last = null;
21232
+ continue;
21233
+ }
21234
+ throw err;
21235
+ }
21236
+ const stale = Date.now() - mtimeMs > LOCK_STALE_MS;
21237
+ if (last && last.token === holder && last.mtimeMs === mtimeMs && stale) {
21238
+ unlinkLock(lock);
21239
+ last = null;
21240
+ continue;
21241
+ }
21242
+ last = stale ? { token: holder, mtimeMs } : null;
21243
+ sleepSync(poll);
21244
+ poll = Math.min(poll * 2, LOCK_POLL_MAX_MS);
21245
+ }
21246
+ try {
21247
+ return swap();
21248
+ } finally {
21249
+ releaseLock(lock, token);
21250
+ }
21251
+ }
21252
+ function gone(err) {
21253
+ return err.code === "ENOENT";
21254
+ }
21255
+ function releaseLock(lock, token) {
21256
+ let holder;
21257
+ try {
21258
+ holder = fs10.readFileSync(lock, "utf8");
21259
+ } catch (err) {
21260
+ if (gone(err)) return;
21261
+ throw err;
21262
+ }
21263
+ if (holder === token) unlinkLock(lock);
21264
+ }
21265
+ function unlinkLock(lock) {
21266
+ try {
21267
+ fs10.unlinkSync(lock);
21268
+ } catch (err) {
21269
+ if (!gone(err)) throw err;
21270
+ }
21271
+ }
20838
21272
  var SettingsService = class {
20839
21273
  filePath;
20840
21274
  cache = null;
20841
- cachedMtimeMs = null;
21275
+ cachedStamp = null;
20842
21276
  constructor(filePath = getSettingsPath()) {
20843
21277
  this.filePath = filePath;
20844
21278
  }
20845
21279
  // Several processes share the settings file (VS Code extension host, web
20846
- // server, CLI), so the cache is keyed on the file's mtime: a write from any
21280
+ // server, CLI), so the cache is keyed on the file's identity: a write from any
20847
21281
  // other process invalidates it. A stale cache here previously meant an
20848
21282
  // allowlist set in one surface never reached another's planner.
20849
21283
  getAll() {
20850
- const mtime = this.fileMtimeMs();
20851
- if (this.cache && mtime === this.cachedMtimeMs) return this.cache;
21284
+ const stamp = this.fileStamp();
21285
+ if (this.cache && stamp === this.cachedStamp) return this.cache;
20852
21286
  this.cache = this.load();
20853
- this.cachedMtimeMs = mtime;
21287
+ this.cachedStamp = stamp;
20854
21288
  return this.cache;
20855
21289
  }
20856
- fileMtimeMs() {
21290
+ /**
21291
+ * Every write replaces the file, so its inode changes with it; the mtime
21292
+ * alone cannot tell two writes apart on a filesystem whose timestamps are
21293
+ * coarse, and a write that shares the last one's tick stayed unseen.
21294
+ */
21295
+ fileStamp(file = this.filePath) {
20857
21296
  try {
20858
- return fs10.statSync(this.filePath).mtimeMs;
21297
+ const stat = fs10.statSync(file);
21298
+ return `${stat.ino}:${stat.mtimeMs}:${stat.size}`;
20859
21299
  } catch {
20860
21300
  return null;
20861
21301
  }
20862
21302
  }
21303
+ /**
21304
+ * What a change is applied to: the file as it is now, never a copy held since
21305
+ * an earlier read. Another process may have saved in between, and writing
21306
+ * the whole file back would silently undo it.
21307
+ */
21308
+ current() {
21309
+ this.cache = null;
21310
+ return this.getAll();
21311
+ }
21312
+ /**
21313
+ * Calls `listener` when another process (or this one) saves the file. The
21314
+ * directory is watched, not the file: a write that swaps the file in leaves
21315
+ * a watch on the old one pointing at nothing.
21316
+ */
21317
+ watch(listener) {
21318
+ const dir = path26.dirname(this.filePath);
21319
+ const name = path26.basename(this.filePath);
21320
+ let seen = this.fileStamp();
21321
+ let timer;
21322
+ let watcher;
21323
+ try {
21324
+ fs10.mkdirSync(dir, { recursive: true });
21325
+ watcher = fs10.watch(dir, (_event, changed) => {
21326
+ if (changed && changed !== name) return;
21327
+ clearTimeout(timer);
21328
+ timer = setTimeout(() => {
21329
+ const stamp = this.fileStamp();
21330
+ if (stamp === seen) return;
21331
+ seen = stamp;
21332
+ listener();
21333
+ }, 50);
21334
+ });
21335
+ } catch {
21336
+ return { dispose() {
21337
+ } };
21338
+ }
21339
+ return { dispose: () => {
21340
+ clearTimeout(timer);
21341
+ watcher.close();
21342
+ } };
21343
+ }
20863
21344
  getModelAllowlist(runner) {
20864
21345
  return this.getAll().modelAllowlist?.[runner];
20865
21346
  }
20866
21347
  setModelAllowlist(runner, ids) {
20867
- this.getAll();
20868
- if (ids === void 0) {
20869
- if (this.cache.modelAllowlist) {
20870
- delete this.cache.modelAllowlist[runner];
20871
- if (Object.keys(this.cache.modelAllowlist).length === 0) {
20872
- this.cache.modelAllowlist = void 0;
21348
+ this.save(() => {
21349
+ if (ids === void 0) {
21350
+ if (this.cache.modelAllowlist) {
21351
+ delete this.cache.modelAllowlist[runner];
21352
+ if (Object.keys(this.cache.modelAllowlist).length === 0) {
21353
+ this.cache.modelAllowlist = void 0;
21354
+ }
20873
21355
  }
21356
+ } else {
21357
+ if (!this.cache.modelAllowlist) {
21358
+ this.cache.modelAllowlist = {};
21359
+ }
21360
+ this.cache.modelAllowlist[runner] = ids;
20874
21361
  }
20875
- } else {
20876
- if (!this.cache.modelAllowlist) {
20877
- this.cache.modelAllowlist = {};
20878
- }
20879
- this.cache.modelAllowlist[runner] = ids;
20880
- }
20881
- this.persist();
21362
+ });
20882
21363
  }
20883
21364
  getPlannerModel(provider) {
20884
21365
  return this.getAll().plannerModels?.[provider];
20885
21366
  }
20886
21367
  setPlannerModel(provider, entry) {
20887
- this.getAll();
20888
- if (entry === void 0) {
20889
- if (this.cache.plannerModels) {
20890
- delete this.cache.plannerModels[provider];
20891
- if (Object.keys(this.cache.plannerModels).length === 0) {
20892
- this.cache.plannerModels = void 0;
21368
+ this.save(() => {
21369
+ if (entry === void 0) {
21370
+ if (this.cache.plannerModels) {
21371
+ delete this.cache.plannerModels[provider];
21372
+ if (Object.keys(this.cache.plannerModels).length === 0) {
21373
+ this.cache.plannerModels = void 0;
21374
+ }
20893
21375
  }
21376
+ } else {
21377
+ if (!this.cache.plannerModels) {
21378
+ this.cache.plannerModels = {};
21379
+ }
21380
+ this.cache.plannerModels[provider] = entry.effort ? { model: entry.model, effort: entry.effort } : { model: entry.model };
20894
21381
  }
20895
- } else {
20896
- if (!this.cache.plannerModels) {
20897
- this.cache.plannerModels = {};
20898
- }
20899
- this.cache.plannerModels[provider] = entry.effort ? { model: entry.model, effort: entry.effort } : { model: entry.model };
20900
- }
20901
- this.persist();
21382
+ });
20902
21383
  }
20903
21384
  getEnabledRunners() {
20904
21385
  return this.getAll().enabledRunners;
20905
21386
  }
20906
21387
  setEnabledRunners(ids) {
20907
- this.getAll();
20908
- this.cache.enabledRunners = [...ids];
20909
- this.persist();
21388
+ this.save(() => {
21389
+ this.cache.enabledRunners = [...ids];
21390
+ });
21391
+ }
21392
+ /**
21393
+ * Change the list from what the file holds at this moment: the change is
21394
+ * handed the saved list (undefined when the user never chose) and its result
21395
+ * is saved in the same call. A list worked out from an earlier read would
21396
+ * save over what another surface set since. It may be handed the list again,
21397
+ * when another process saved before this one could (see {@link save}).
21398
+ */
21399
+ updateEnabledRunners(change) {
21400
+ return this.save(() => {
21401
+ const next = [...change(this.cache.enabledRunners)];
21402
+ this.cache.enabledRunners = next;
21403
+ return next;
21404
+ });
21405
+ }
21406
+ /** Save `ids` as the user's choice unless one is saved already, judged on the file as it is now. */
21407
+ setEnabledRunnersIfUnset(ids) {
21408
+ return this.save(() => {
21409
+ if (this.cache.enabledRunners !== void 0) return false;
21410
+ this.cache.enabledRunners = [...ids];
21411
+ return true;
21412
+ });
20910
21413
  }
20911
21414
  /**
20912
21415
  * Full or Guarded (ADR-0001), as saved here (ADR-0030). A value set in the
@@ -20924,9 +21427,9 @@ var SettingsService = class {
20924
21427
  return env !== void 0 && env.trim() !== "";
20925
21428
  }
20926
21429
  setAutonomousMode(enabled) {
20927
- this.getAll();
20928
- this.cache.autonomousMode = enabled;
20929
- this.persist();
21430
+ this.save(() => {
21431
+ this.cache.autonomousMode = enabled;
21432
+ });
20930
21433
  }
20931
21434
  getRunSettings() {
20932
21435
  return runSettingsOf(this.getAll());
@@ -20938,11 +21441,11 @@ var SettingsService = class {
20938
21441
  throw new RunSettingsError(RUN_SETTING_CHECKS[key].refusal);
20939
21442
  }
20940
21443
  }
20941
- this.getAll();
20942
- for (const key of RUN_SETTING_KEYS) {
20943
- if (changes[key] !== void 0) this.cache[key] = changes[key];
20944
- }
20945
- this.persist();
21444
+ this.save(() => {
21445
+ for (const key of RUN_SETTING_KEYS) {
21446
+ if (changes[key] !== void 0) this.cache[key] = changes[key];
21447
+ }
21448
+ });
20946
21449
  return this.getRunSettings();
20947
21450
  }
20948
21451
  load() {
@@ -20971,13 +21474,43 @@ var SettingsService = class {
20971
21474
  }
20972
21475
  return {};
20973
21476
  }
20974
- persist() {
21477
+ /**
21478
+ * Makes `change` to the file as it is now, and saves it. Another process can
21479
+ * save between that read and this write; the file is looked at again just
21480
+ * before ours replaces it, and when it moved the change is made afresh from
21481
+ * what that process saved, so neither save undoes the other. A file that
21482
+ * keeps moving is written over after {@link SAVE_TRIES} tries rather than
21483
+ * never. The look and the swap are one step under the file's lock
21484
+ * ({@link withFileLock}). A change that changes nothing writes nothing.
21485
+ */
21486
+ save(change) {
21487
+ for (let tries = 1; ; tries++) {
21488
+ const before = JSON.stringify(this.current());
21489
+ const read2 = this.cachedStamp;
21490
+ const result = change();
21491
+ if (JSON.stringify(this.cache) === before || this.persist(tries < SAVE_TRIES ? read2 : void 0)) return result;
21492
+ }
21493
+ }
21494
+ /** False, saving nothing, when the file is no longer the one stamped `expected`. */
21495
+ persist(expected) {
20975
21496
  const dir = path26.dirname(this.filePath);
20976
21497
  if (!fs10.existsSync(dir)) {
20977
21498
  fs10.mkdirSync(dir, { recursive: true });
20978
21499
  }
20979
- fs10.writeFileSync(this.filePath, JSON.stringify(this.cache, null, 2));
20980
- this.cachedMtimeMs = this.fileMtimeMs();
21500
+ const staging = `${this.filePath}.${process.pid}.tmp`;
21501
+ fs10.writeFileSync(staging, JSON.stringify(this.cache, null, 2));
21502
+ const stamp = this.fileStamp(staging);
21503
+ const swapped = withFileLock(this.filePath, () => {
21504
+ if (expected !== void 0 && this.fileStamp() !== expected) return false;
21505
+ fs10.renameSync(staging, this.filePath);
21506
+ return true;
21507
+ });
21508
+ if (!swapped) {
21509
+ fs10.rmSync(staging, { force: true });
21510
+ return false;
21511
+ }
21512
+ this.cachedStamp = stamp;
21513
+ return true;
20981
21514
  }
20982
21515
  };
20983
21516
 
@@ -21466,7 +21999,7 @@ function capConflictFiles(files, max = 5) {
21466
21999
  }
21467
22000
 
21468
22001
  // src/services/mergeResultNotice.ts
21469
- function blockNotice({ repo, reason, files }) {
22002
+ function blockNotice({ repo, reason, files, expected, actual, rebasing }) {
21470
22003
  switch (reason) {
21471
22004
  case "merge-in-progress":
21472
22005
  return `${repo} has a merge in progress`;
@@ -21476,10 +22009,22 @@ function blockNotice({ repo, reason, files }) {
21476
22009
  return `${repo} has uncommitted changes to ${files.join(", ")}`;
21477
22010
  case "partial-landing":
21478
22011
  return `${repo} holds part of a task whose landing could not be rolled back`;
22012
+ case "wrong-branch":
22013
+ return `${repo === SELF_REPO ? "your checkout" : repo} is ${actual ? `on ${actual}` : rebasing ? "in the middle of a rebase" : "on a detached HEAD"}; this run started on ${expected}`;
21479
22014
  case "git-error":
21480
22015
  return `git could not check ${repo}`;
21481
22016
  }
21482
22017
  }
22018
+ function branchFix(blocked) {
22019
+ if (blocked.length === 0 || blocked.some((b) => b.reason !== "wrong-branch")) return "";
22020
+ const rebasing = blocked.filter((b) => b.rebasing);
22021
+ if (rebasing.length > 0) {
22022
+ const where = rebasing.map((b) => b.repo === SELF_REPO ? "your checkout" : b.repo).join(", ");
22023
+ return ` Finish or abort the rebase in ${where}${rebasing.length < blocked.length ? " and check out the branch each other repository started on" : ""}, then press Merge all again.`;
22024
+ }
22025
+ const started = [...new Set(blocked.map((b) => b.expected))];
22026
+ return started.length === 1 && blocked.length === 1 ? ` Check out ${started[0]} and press Merge all again.` : " Check out the branch each repository started on and press Merge all again.";
22027
+ }
21483
22028
  function landedNotice(landed2) {
21484
22029
  if (landed2.length === 0) return "Nothing was merged.";
21485
22030
  return landed2.length === 1 ? `${landed2[0]} was merged already and stays merged.` : `${landed2.join(", ")} were merged already and stay merged.`;
@@ -21496,7 +22041,7 @@ function describeMergeResult(result, branch, group, repaired = []) {
21496
22041
  return { level: "info", message: repaired.length > 0 ? `${base} ${repairedNotice(repaired)}` : base };
21497
22042
  }
21498
22043
  case "blocked":
21499
- return { level: "warn", message: `Merged nothing, so every tree is as it was: ${result.blocked.map(blockNotice).join("; ")}.` };
22044
+ return { level: "warn", message: `Merged nothing, so every tree is as it was: ${result.blocked.map(blockNotice).join("; ")}.${branchFix(result.blocked)}` };
21500
22045
  case "conflict":
21501
22046
  return {
21502
22047
  level: "warn",
@@ -21606,7 +22151,7 @@ ${await file.patch()}`);
21606
22151
  }
21607
22152
 
21608
22153
  // src/services/GitWorktreeIsolation.ts
21609
- var import_crypto10 = require("crypto");
22154
+ var import_crypto11 = require("crypto");
21610
22155
  var fs13 = __toESM(require("fs"));
21611
22156
  var path29 = __toESM(require("path"));
21612
22157
 
@@ -21734,7 +22279,7 @@ var GitWorktreeIsolation = class {
21734
22279
  this.resolvePath = deps.resolvePath ?? (() => augmentedPath());
21735
22280
  this.gitInvoker = { exec: deps.execFileImpl ?? defaultExecFile2, resolvePath: this.resolvePath };
21736
22281
  this.platform = deps.platform ?? process.platform;
21737
- this.mintRunId = deps.mintRunId ?? (() => (0, import_crypto10.randomBytes)(4).toString("hex"));
22282
+ this.mintRunId = deps.mintRunId ?? (() => (0, import_crypto11.randomBytes)(4).toString("hex"));
21738
22283
  }
21739
22284
  deps;
21740
22285
  resolvePath;
@@ -21860,7 +22405,7 @@ var GitWorktreeIsolation = class {
21860
22405
  path: repoPath,
21861
22406
  root,
21862
22407
  baseRef,
21863
- ...branch ? { baseBranch: branch } : {},
22408
+ ...branch ? { baseBranch: branch } : { startedDetached: true },
21864
22409
  integrationBranch: integrationBranchFor(run.id)
21865
22410
  };
21866
22411
  if (!(await this.tryGit(root, ["branch", repo.integrationBranch, baseRef])).ok) return null;
@@ -22213,7 +22758,9 @@ var GitWorktreeIsolation = class {
22213
22758
  async mergeAll(run) {
22214
22759
  const partial = await this.settleLanding(run);
22215
22760
  if (partial.length > 0) return { outcome: "blocked", blocked: partial.map((repo) => ({ repo, reason: "partial-landing", files: [] })) };
22761
+ const misplaced = await this.offBaseBranch(run.repos);
22216
22762
  const repos = await this.reposWithWork(run);
22763
+ if (misplaced.length > 0) return { outcome: "blocked", blocked: misplaced };
22217
22764
  if (run.repos.length === 1 || !await this.canPreflight()) return this.mergeInTurn(repos);
22218
22765
  const blocked = [];
22219
22766
  for (const repo of repos) {
@@ -22231,6 +22778,27 @@ var GitWorktreeIsolation = class {
22231
22778
  }
22232
22779
  return withWork;
22233
22780
  }
22781
+ /**
22782
+ * A merge goes into whatever is checked out, which is only what the user
22783
+ * meant if it is the branch the run started on. A run that started on a
22784
+ * detached HEAD named no branch, so anything is as good as it was.
22785
+ */
22786
+ async offBaseBranch(repos) {
22787
+ const off = [];
22788
+ for (const repo of repos) {
22789
+ if (!repo.baseBranch) continue;
22790
+ const current = (await this.tryGit(repo.root, ["symbolic-ref", "--short", "-q", "HEAD"])).stdout.trim();
22791
+ if (current === repo.baseBranch) continue;
22792
+ off.push({
22793
+ repo: repo.path,
22794
+ reason: "wrong-branch",
22795
+ files: [],
22796
+ expected: repo.baseBranch,
22797
+ ...current ? { actual: current } : await this.rebaseInProgress(repo.root) ? { rebasing: true } : {}
22798
+ });
22799
+ }
22800
+ return off;
22801
+ }
22234
22802
  /** `git merge-tree --write-tree`, which merges without touching a tree, arrived in git 2.38. */
22235
22803
  async canPreflight() {
22236
22804
  const version = await this.tryGit(void 0, ["--version"]);
@@ -22718,6 +23286,14 @@ var GitWorktreeIsolation = class {
22718
23286
  if ((await this.tryGit(cwd, ["rev-parse", "-q", "--verify", "MERGE_HEAD"])).ok) return true;
22719
23287
  return (await this.tryGit(cwd, ["ls-files", "-u"])).stdout.trim() !== "";
22720
23288
  }
23289
+ /** A rebase leaves HEAD detached, so it is told from a checkout the user detached on purpose. */
23290
+ async rebaseInProgress(cwd) {
23291
+ for (const dir of ["rebase-merge", "rebase-apply"]) {
23292
+ const found = await this.tryGit(cwd, ["rev-parse", "--git-path", dir]);
23293
+ if (found.ok && fs13.existsSync(path29.resolve(cwd, found.stdout.trim()))) return true;
23294
+ }
23295
+ return false;
23296
+ }
22721
23297
  async abortMerge(cwd) {
22722
23298
  if (!(await this.tryGit(cwd, ["merge", "--abort"])).ok) await this.tryGit(cwd, ["reset", "--hard"]);
22723
23299
  }
@@ -22738,6 +23314,29 @@ function createWorktreeIsolation(deps) {
22738
23314
  return new GitWorktreeIsolation(deps);
22739
23315
  }
22740
23316
 
23317
+ // src/services/mergeWording.ts
23318
+ function isRepoGroup(handoff) {
23319
+ return handoff.repos.some((r) => r.path !== ".");
23320
+ }
23321
+ function reposWithWork(handoff) {
23322
+ const withWork = handoff.repos.filter((r) => r.landed.length > 0);
23323
+ return (withWork.length > 0 ? withWork : handoff.repos).map((r) => r.path);
23324
+ }
23325
+ function mergeTarget(handoff) {
23326
+ if (!isRepoGroup(handoff)) return handoff.repos[0]?.baseBranch ?? "the branch you have checked out";
23327
+ const targets = handoff.repos.filter((r) => reposWithWork(handoff).includes(r.path));
23328
+ const branches = new Set(targets.map((r) => r.baseBranch));
23329
+ const [only] = branches;
23330
+ if (branches.size === 1 && only) return `${only} in each of ${targets.map((r) => r.path).join(", ")}`;
23331
+ return `each of ${targets.map((r) => `${r.path} (${r.baseBranch ?? "as checked out"})`).join(", ")}`;
23332
+ }
23333
+ function checkoutRule(handoff) {
23334
+ const named3 = handoff.repos.filter((r) => r.baseBranch);
23335
+ if (named3.length === 0) return "";
23336
+ if (!isRepoGroup(handoff)) return ` Nothing is merged unless ${named3[0].baseBranch} is checked out.`;
23337
+ return " Nothing is merged unless each repository is on the branch it started on.";
23338
+ }
23339
+
22741
23340
  // src/interfaces/ILogger.ts
22742
23341
  var ConsoleLogger = class {
22743
23342
  warn(scope, message, err) {
@@ -22790,7 +23389,10 @@ async function settleReply(opts) {
22790
23389
  opts.onProgress({ type: "interrupted" });
22791
23390
  return { done: turn };
22792
23391
  }
22793
- if (attempt.failure !== void 0) return { done: { ...message(attempt.failure), failed: true } };
23392
+ if (attempt.failure !== void 0) {
23393
+ const { usageLimit } = attempt;
23394
+ return { done: { ...message(attempt.failure), failed: true, ...usageLimit !== void 0 && { usageLimit } } };
23395
+ }
22794
23396
  if (!attempt.text.trim()) return { done: message(emptyReplyReport(deniedStep(attempt))) };
22795
23397
  const reply2 = opts.classify ? classifyPlannerReply(attempt.text, opts.classify) : { kind: "prose" };
22796
23398
  switch (reply2.kind) {
@@ -23237,8 +23839,8 @@ function summarizeToolCall(tool2, argsJson, toolLabel) {
23237
23839
  }
23238
23840
  const path35 = args.filePath || args.file_path || args.path;
23239
23841
  if (path35) {
23240
- const basename4 = String(path35).split("/").pop() || String(path35);
23241
- return `${display} ${basename4}`;
23842
+ const basename5 = String(path35).split("/").pop() || String(path35);
23843
+ return `${display} ${basename5}`;
23242
23844
  }
23243
23845
  if (args.pattern) {
23244
23846
  return `${display} ${args.pattern}`;
@@ -23788,7 +24390,7 @@ var CONVERSATION_TOOL_DEFINITIONS = toGeminiToolDeclarations(true);
23788
24390
  function tokenCount(value) {
23789
24391
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
23790
24392
  }
23791
- function usageRecordFrom(usage) {
24393
+ function usageRecordFrom(usage, model) {
23792
24394
  if (!usage) return void 0;
23793
24395
  const inputTokens = tokenCount(usage.promptTokenCount);
23794
24396
  const answerTokens = tokenCount(usage.candidatesTokenCount);
@@ -23796,6 +24398,7 @@ function usageRecordFrom(usage) {
23796
24398
  const reportedOutput = [answerTokens, thoughtTokens].filter((t) => t !== void 0);
23797
24399
  const record = {
23798
24400
  source: "google",
24401
+ model,
23799
24402
  ...inputTokens !== void 0 ? { inputTokens } : {},
23800
24403
  ...reportedOutput.length > 0 ? { outputTokens: reportedOutput.reduce((a, b) => a + b, 0) } : {},
23801
24404
  ...tokenCount(usage.cachedContentTokenCount) !== void 0 ? { cachedInputTokens: tokenCount(usage.cachedContentTokenCount) } : {}
@@ -23804,15 +24407,31 @@ function usageRecordFrom(usage) {
23804
24407
  return anyReported ? record : void 0;
23805
24408
  }
23806
24409
  var GeminiResearchChat = class {
23807
- constructor(chat, hooks = {}) {
23808
- this.chat = chat;
24410
+ constructor(getModel, open, hooks = {}) {
24411
+ this.getModel = getModel;
24412
+ this.open = open;
23809
24413
  this.hooks = hooks;
24414
+ this.model = getModel();
24415
+ this.chat = open(this.model);
23810
24416
  }
23811
- chat;
24417
+ getModel;
24418
+ open;
23812
24419
  hooks;
24420
+ chat;
24421
+ /** The model `chat` calls, named on its usage records. */
24422
+ model;
24423
+ /** The SDK binds a chat to one model, so a switch reopens it on the new one over the history so far. */
24424
+ async current() {
24425
+ const model = this.getModel();
24426
+ if (model !== this.model) {
24427
+ this.chat = this.open(model, await this.chat.getHistory());
24428
+ this.model = model;
24429
+ }
24430
+ return this.chat;
24431
+ }
23813
24432
  async sendMessage(text3, signal) {
23814
24433
  if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
23815
- const result = await this.chat.sendMessageStream(text3);
24434
+ const result = await (await this.current()).sendMessageStream(text3);
23816
24435
  return this.consume(result.stream);
23817
24436
  }
23818
24437
  async sendToolResults(results, signal) {
@@ -23823,7 +24442,7 @@ var GeminiResearchChat = class {
23823
24442
  response: { output: r.output, truncated: r.truncated, totalChars: r.totalChars }
23824
24443
  }
23825
24444
  }));
23826
- const result = await this.chat.sendMessageStream(funcResponses);
24445
+ const result = await (await this.current()).sendMessageStream(funcResponses);
23827
24446
  return this.consume(result.stream);
23828
24447
  }
23829
24448
  /**
@@ -23863,7 +24482,7 @@ var GeminiResearchChat = class {
23863
24482
  }
23864
24483
  }
23865
24484
  }
23866
- const usageRecord3 = usageRecordFrom(usage);
24485
+ const usageRecord3 = usageRecordFrom(usage, this.model);
23867
24486
  if (usageRecord3) this.hooks.onUsage?.(usageRecord3);
23868
24487
  return {
23869
24488
  text: text3,
@@ -23879,6 +24498,8 @@ var GeminiService = class extends BaseAiService {
23879
24498
  genAI = null;
23880
24499
  genAIKey = "";
23881
24500
  model = null;
24501
+ /** The id `model` was made for; the SDK's own `model.model` is its `models/…` resource path. */
24502
+ modelId = "";
23882
24503
  constructor(config) {
23883
24504
  super(config);
23884
24505
  }
@@ -23892,13 +24513,18 @@ var GeminiService = class extends BaseAiService {
23892
24513
  }
23893
24514
  return true;
23894
24515
  }
23895
- getPlanningModel() {
24516
+ /** The planner model picked for this session, as the API names it. */
24517
+ plannerModel() {
24518
+ return normalizeGeminiModel(this.config.orchestratorModel || this.config.planningModel);
24519
+ }
24520
+ getPlanningModel(model = this.plannerModel()) {
23896
24521
  this.init();
23897
- if (!this.model || this.model.model !== this.config.planningModel) {
24522
+ if (!this.model || this.modelId !== model) {
23898
24523
  this.model = this.genAI.getGenerativeModel({
23899
- model: this.config.planningModel,
24524
+ model,
23900
24525
  generationConfig: { temperature: 0.3, topP: 0.95, maxOutputTokens: 16384 }
23901
24526
  });
24527
+ this.modelId = model;
23902
24528
  }
23903
24529
  return this.model;
23904
24530
  }
@@ -23913,7 +24539,6 @@ var GeminiService = class extends BaseAiService {
23913
24539
  }
23914
24540
  async startConversation(req) {
23915
24541
  this.ensureInit();
23916
- const genModel = this.getPlanningModel();
23917
24542
  const contextStr = await BaseAiService.collectResearchContext(req.fs, req.runners);
23918
24543
  const systemPrompt = buildConversationSystemPrompt(
23919
24544
  req.goal,
@@ -23942,13 +24567,14 @@ ${m.content}` });
23942
24567
  if ((req.priorHistory?.length ?? 0) > 0 && seededHistory[seededHistory.length - 1].role === "user") {
23943
24568
  seededHistory.push({ role: "model", parts: [{ text: "Understood." }] });
23944
24569
  }
23945
- const chat = genModel.startChat({
23946
- history: seededHistory,
23947
- tools: [{ functionDeclarations: req.plannerTools ? CONVERSATION_TOOL_DEFINITIONS : TOOL_DEFINITIONS }],
24570
+ const tools = [{ functionDeclarations: req.plannerTools ? CONVERSATION_TOOL_DEFINITIONS : TOOL_DEFINITIONS }];
24571
+ const open = (model, history = seededHistory) => this.getPlanningModel(model).startChat({
24572
+ history,
24573
+ tools,
23948
24574
  generationConfig: { temperature: 0.3, topP: 0.95, maxOutputTokens: 16384 }
23949
24575
  });
23950
24576
  let currentProgress = req.onProgress;
23951
- const researchChat = new GeminiResearchChat(chat, {
24577
+ const researchChat = new GeminiResearchChat(() => this.plannerModel(), open, {
23952
24578
  onReasoning: (text3, segmentId) => currentProgress({ type: "thinking", text: text3, segmentId }),
23953
24579
  onContent: (text3, segmentId) => currentProgress({ type: "text_delta", text: text3, segmentId }),
23954
24580
  onUsage: (record) => currentProgress({ type: "usage", record })
@@ -24021,18 +24647,17 @@ ${repairHint}` }] : parts;
24021
24647
  async researchAndPlan(userDescription, runners, modelsByRunner, fs18, onProgress, fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
24022
24648
  this.ensureInit();
24023
24649
  const { autonomousDefault } = modes;
24024
- const genModel = this.getPlanningModel();
24025
24650
  const contextStr = await BaseAiService.collectResearchContext(fs18, runners);
24026
24651
  const systemPrompt = buildResearchPrompt(userDescription, contextStr, modelsByRunner, runners, runnerModes, modes);
24027
24652
  const firstMessage = `${buildResearchToolsPrompt()}
24028
24653
 
24029
24654
  Explore the workspace to understand the codebase, then generate the plan.`;
24030
- const chat = genModel.startChat({
24031
- history: [{ role: "user", parts: [{ text: systemPrompt }] }],
24655
+ const open = (model, history = [{ role: "user", parts: [{ text: systemPrompt }] }]) => this.getPlanningModel(model).startChat({
24656
+ history,
24032
24657
  tools: [{ functionDeclarations: TOOL_DEFINITIONS }],
24033
24658
  generationConfig: { temperature: 0.3, topP: 0.95, maxOutputTokens: 16384 }
24034
24659
  });
24035
- const researchChat = new GeminiResearchChat(chat, {
24660
+ const researchChat = new GeminiResearchChat(() => this.plannerModel(), open, {
24036
24661
  onUsage: (record) => onProgress({ type: "usage", record })
24037
24662
  });
24038
24663
  const result = await this.runResearchLoop(researchChat, firstMessage, fs18, onProgress, runners, void 0, fetcher, userDescription, runnerModes, autonomousDefault, signal);
@@ -24068,7 +24693,7 @@ var import_openai = __toESM(require("openai"));
24068
24693
  var import_uuid7 = require("uuid");
24069
24694
  var KEYLESS_PLACEHOLDER = "not-needed";
24070
24695
  var OpenAiResearchChat = class {
24071
- constructor(messages, getClient, getModel, tools, onReasoning, onContent, getSource = () => "openai", onUsage, contextWindow) {
24696
+ constructor(messages, getClient, getModel, tools, onReasoning, onContent, getSource = () => "openai", onUsage) {
24072
24697
  this.messages = messages;
24073
24698
  this.getClient = getClient;
24074
24699
  this.getModel = getModel;
@@ -24077,7 +24702,6 @@ var OpenAiResearchChat = class {
24077
24702
  this.onContent = onContent;
24078
24703
  this.getSource = getSource;
24079
24704
  this.onUsage = onUsage;
24080
- this.contextWindow = contextWindow;
24081
24705
  }
24082
24706
  messages;
24083
24707
  getClient;
@@ -24087,7 +24711,6 @@ var OpenAiResearchChat = class {
24087
24711
  onContent;
24088
24712
  getSource;
24089
24713
  onUsage;
24090
- contextWindow;
24091
24714
  async sendMessage(text3, signal) {
24092
24715
  this.answerAbandonedCalls();
24093
24716
  this.messages.push({ role: "user", content: text3 });
@@ -24202,7 +24825,6 @@ var OpenAiResearchChat = class {
24202
24825
  record.reportedCost = { amount: reported.cost, currency: "USD" };
24203
24826
  hasMeasure = true;
24204
24827
  }
24205
- if (this.contextWindow && this.contextWindow > 0) record.contextWindow = this.contextWindow;
24206
24828
  return hasMeasure ? record : void 0;
24207
24829
  }
24208
24830
  };
@@ -24329,8 +24951,7 @@ ${buildResearchToolsPrompt()}` }
24329
24951
  (delta, segmentId) => currentProgress({ type: "thinking", text: delta, segmentId }),
24330
24952
  (delta, segmentId) => currentProgress({ type: "text_delta", text: delta, segmentId }),
24331
24953
  () => this.config.aiProvider,
24332
- (record) => currentProgress({ type: "usage", record }),
24333
- req.contextWindow
24954
+ (record) => currentProgress({ type: "usage", record })
24334
24955
  );
24335
24956
  const ctx = {
24336
24957
  chat,
@@ -24892,6 +25513,10 @@ var VerdictEngine = class {
24892
25513
  this.generations.clear();
24893
25514
  }
24894
25515
  };
25516
+ var EXIT_CHECK = "exit_code";
25517
+ function endedByExit(verdict) {
25518
+ return verdict.checks.some((check2) => check2.name === EXIT_CHECK);
25519
+ }
24895
25520
  function exitVerdict(exitCode) {
24896
25521
  const normalized2 = exitCode == null ? 0 : exitCode;
24897
25522
  const exitOk = normalized2 === 0;
@@ -24900,7 +25525,7 @@ function exitVerdict(exitCode) {
24900
25525
  reason: exitOk ? "Failed verification: the runner exited cleanly but never called task_complete." : `Failed verification: the runner exited with code ${normalized2} without calling task_complete.`,
24901
25526
  checks: [
24902
25527
  { name: "task_complete", passed: false, skipped: false, detail: "the runner exited before calling task_complete" },
24903
- { name: "exit_code", passed: exitOk, skipped: false, detail: exitOk ? "the runner exited cleanly (code 0)" : `the runner exited with code ${normalized2}` }
25528
+ { name: EXIT_CHECK, passed: exitOk, skipped: false, detail: exitOk ? "the runner exited cleanly (code 0)" : `the runner exited with code ${normalized2}` }
24904
25529
  ],
24905
25530
  decidedAt: (/* @__PURE__ */ new Date()).toISOString()
24906
25531
  };
@@ -25099,6 +25724,7 @@ var PlanStore = class {
25099
25724
  this.rebuild();
25100
25725
  for (const task of this._allTasks) {
25101
25726
  if (task.status === "failed") task.status = "pending";
25727
+ if (task.runnerSessionId && !task.runnerSessionOwner) task.runnerSessionOwner = sessionOwner(task) ?? task.assignedRunner;
25102
25728
  }
25103
25729
  this.validateAssignedRunners();
25104
25730
  }
@@ -25228,9 +25854,17 @@ var PlanStore = class {
25228
25854
  const task = this._taskMap.get(id);
25229
25855
  if (task) task.outputSummary = summary;
25230
25856
  }
25231
- setTaskRunnerSessionId(id, runnerSessionId) {
25857
+ /** The owner is saved with the session it belongs to, and dropped with it. */
25858
+ setTaskRunnerSessionId(id, runnerSessionId, owner) {
25859
+ const task = this._taskMap.get(id);
25860
+ if (!task) return;
25861
+ task.runnerSessionId = runnerSessionId;
25862
+ if (runnerSessionId && owner) task.runnerSessionOwner = owner;
25863
+ else delete task.runnerSessionOwner;
25864
+ }
25865
+ setCarriedFrom(id, runner) {
25232
25866
  const task = this._taskMap.get(id);
25233
- if (task) task.runnerSessionId = runnerSessionId;
25867
+ if (task) task.carriedFrom = runner;
25234
25868
  }
25235
25869
  clearCarriedFrom(id) {
25236
25870
  const task = this._taskMap.get(id);
@@ -25244,9 +25878,9 @@ var PlanStore = class {
25244
25878
  const task = this._taskMap.get(id);
25245
25879
  if (task?.attemptUsage) task.attemptUsage = task.attemptUsage.map((usage) => usage.attempt === attempt ? addAttemptUsage(usage, record, now) : usage);
25246
25880
  }
25247
- endAttemptUsage(id, attempt, now) {
25881
+ endAttemptUsage(id, attempt, now, runnerSessionCost, sessionId) {
25248
25882
  const task = this._taskMap.get(id);
25249
- if (task?.attemptUsage) task.attemptUsage = task.attemptUsage.map((usage) => usage.attempt === attempt ? endAttemptUsage(usage, now) : usage);
25883
+ if (task?.attemptUsage) task.attemptUsage = task.attemptUsage.map((usage) => usage.attempt === attempt ? endAttemptUsage(usage, now, runnerSessionCost, sessionId) : usage);
25250
25884
  }
25251
25885
  setTaskAttemptSkills(id, skills) {
25252
25886
  const task = this._taskMap.get(id);
@@ -26104,6 +26738,8 @@ function conflictResolverTask(task, run, plan) {
26104
26738
  }
26105
26739
 
26106
26740
  // src/services/UsageLimitWaits.ts
26741
+ var RESUME_MARGIN_MS = 6e4;
26742
+ var RELAPSE_MS = 5 * 6e4;
26107
26743
  var UsageLimitWaits = class {
26108
26744
  constructor(onDue) {
26109
26745
  this.onDue = onDue;
@@ -26226,6 +26862,9 @@ function selectReadyTasks(input) {
26226
26862
  gated
26227
26863
  };
26228
26864
  }
26865
+ function isBlocked(task, store) {
26866
+ return blockedIn(task, blockingFailures(store.allTasks));
26867
+ }
26229
26868
  var blockedIn = (task, blocked) => task.status === "blocked" || blocked.has(task.id);
26230
26869
  function dependencyMet(depId, store, runs) {
26231
26870
  return waitingBehind([depId], store, runs).length === 0;
@@ -26355,6 +26994,12 @@ var TaskOrchestrator = class _TaskOrchestrator {
26355
26994
  * task whose spawn always throws.
26356
26995
  */
26357
26996
  onHold = /* @__PURE__ */ new Set();
26997
+ /**
26998
+ * Force starts queued behind dependencies the scheduler will not start them
26999
+ * after: outside a run it drives, or past a failed dependency, which it
27000
+ * never forces. Each starts once those still under way have finished.
27001
+ */
27002
+ queuedStarts = /* @__PURE__ */ new Set();
26358
27003
  /**
26359
27004
  * Tasks Mark complete is landing after it ended their attempt: in progress
26360
27005
  * with no attempt for as long as that takes, and not orphans for it.
@@ -26376,6 +27021,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
26376
27021
  observers = [];
26377
27022
  skillsAt;
26378
27023
  previousAttemptFromLog;
27024
+ lastLoggedAttempt;
26379
27025
  runSettings;
26380
27026
  isApproved;
26381
27027
  constructor(deps) {
@@ -26391,6 +27037,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
26391
27037
  this.workspaceEnv = deps.workspaceEnv;
26392
27038
  this.skillsAt = deps.skillsAt;
26393
27039
  this.previousAttemptFromLog = deps.previousAttemptFromLog;
27040
+ this.lastLoggedAttempt = deps.lastLoggedAttempt;
26394
27041
  this.runSettings = deps.runSettings;
26395
27042
  this.isApproved = deps.isApproved;
26396
27043
  this.store.onMutate = () => this.emit("onTaskChanged");
@@ -26455,6 +27102,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
26455
27102
  workspaceEnv: options.workspaceEnv ?? ((cwd) => resolveWorkspaceEnv(cwd)),
26456
27103
  skillsAt: options.skillsAt ?? ((roots) => new SkillsService(roots)),
26457
27104
  previousAttemptFromLog: options.previousAttemptFromLog ?? (() => null),
27105
+ lastLoggedAttempt: options.lastLoggedAttempt ?? (() => 0),
26458
27106
  runSettings: options.runSettings ?? (() => void 0),
26459
27107
  isApproved: options.isApproved ?? (() => true)
26460
27108
  });
@@ -26711,10 +27359,10 @@ var TaskOrchestrator = class _TaskOrchestrator {
26711
27359
  */
26712
27360
  holdStarts() {
26713
27361
  this.startHolds++;
26714
- let held = true;
27362
+ let held2 = true;
26715
27363
  return () => {
26716
- if (!held) return;
26717
- held = false;
27364
+ if (!held2) return;
27365
+ held2 = false;
26718
27366
  this.startHolds--;
26719
27367
  };
26720
27368
  }
@@ -26725,24 +27373,25 @@ var TaskOrchestrator = class _TaskOrchestrator {
26725
27373
  */
26726
27374
  admitStart() {
26727
27375
  const generation2 = this.stopGeneration;
26728
- let held = true;
27376
+ let held2 = true;
26729
27377
  this.admissions++;
26730
27378
  return {
26731
27379
  current: () => generation2 === this.stopGeneration,
26732
27380
  release: () => {
26733
- if (!held) return;
26734
- held = false;
27381
+ if (!held2) return;
27382
+ held2 = false;
26735
27383
  this.admissions--;
26736
27384
  }
26737
27385
  };
26738
27386
  }
26739
27387
  /**
26740
27388
  * Something can still start that nobody has to ask for again: a start
26741
- * admitted and not yet claimed, or a task that resumes on its own at its
26742
- * usage limit's reset. A Stop is owed to it, whether or not anything runs.
27389
+ * admitted and not yet claimed, a force start queued behind its
27390
+ * dependencies, or a task that resumes on its own at its usage limit's
27391
+ * reset. A Stop is owed to it, whether or not anything runs.
26743
27392
  */
26744
27393
  get startsPending() {
26745
- return this.admissions > 0 || this.store.allTasks.some(waitsForReset);
27394
+ return this.admissions > 0 || this.queuedStarts.size > 0 || this.store.allTasks.some(waitsForReset);
26746
27395
  }
26747
27396
  /** Run `work`, which awaits before it claims, under an admission it holds throughout; it is handed the generation it was admitted under. */
26748
27397
  async whileAdmitted(work) {
@@ -26841,6 +27490,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
26841
27490
  this.retryCounts.clear();
26842
27491
  this.spawnCounts.clear();
26843
27492
  this.onHold.clear();
27493
+ this.queuedStarts.clear();
26844
27494
  this.recoverOrphans();
26845
27495
  this.rearmLimitWaits();
26846
27496
  }
@@ -26928,6 +27578,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
26928
27578
  }
26929
27579
  this.runs.interrupt();
26930
27580
  this.onHold.clear();
27581
+ if (reason === "user") this.queuedStarts.clear();
26931
27582
  for (const t of this.store.allTasks.filter(waitsForReset)) this.cancelScheduledResume(t.id);
26932
27583
  this.sayStopped(stopped);
26933
27584
  this.recoverOrphans();
@@ -27027,6 +27678,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
27027
27678
  const attempt = this.attempts.get(taskId);
27028
27679
  if (!task || !attempt) return;
27029
27680
  attempt.decided = true;
27681
+ const stoppedMidTurn = endedByExit(verdict) ? saidInFlight(attempt) : null;
27030
27682
  console.error(`[TaskOrchestrator] Task ${taskRefOf(this.store.planTasks, task.id) ?? `#${task.order}`} "${task.title}" verdict=${verdict.outcome}`);
27031
27683
  console.error(`[TaskOrchestrator] Runner: ${task.assignedRunner}, Model: ${task.assignedModel?.modelId ?? "default"}`);
27032
27684
  console.error(`[TaskOrchestrator] Prompt preview: ${(task.prompt ?? "").slice(0, 200)}`);
@@ -27048,8 +27700,8 @@ ${summary || "(empty: no output captured)"}`);
27048
27700
  this.tell("warn", `Ops task "${task.title}" finished, but changed tracked files in your checkout: ${capConflictFiles(changedFiles)}. An ops task must not change repository files, so nothing was committed: look at the changes, then mark it complete or retry it.`);
27049
27701
  } else if (landing) {
27050
27702
  await this.applyLanding(task, landing, () => this.notifications.info(`Task "${task.title}" completed.`));
27051
- } else if (classifyRunnerStop(attempt.session?.getOutput() ?? "") === "usage-limit") {
27052
- this.pauseOnLimit(task, attempt, attempt.session?.getOutput() ?? "");
27703
+ } else if (stoppedMidTurn !== null && classifyRunnerStop(stoppedMidTurn) === "usage-limit") {
27704
+ this.pauseOnLimit(task, attempt, stoppedMidTurn);
27053
27705
  if (attempt.worktree) await this.runs.release(taskId, { keep: true });
27054
27706
  } else {
27055
27707
  this.store.markFailed(taskId);
@@ -27064,16 +27716,17 @@ ${summary || "(empty: no output captured)"}`);
27064
27716
  const status = this.store.get(taskId)?.status;
27065
27717
  if (status === "completed" || status === "failed" || status === "awaiting_user") this.emit("onTaskSettled", { taskId });
27066
27718
  this.emit("onTaskChanged");
27067
- if (!this.running) {
27068
- if (this.attempts.size === 0) {
27069
- if (this.store.isFinished()) this.planStatus = "completed";
27070
- this.emit("onTick");
27071
- if (this.settled) await this.closeRun();
27072
- this.emit("onExecutionComplete");
27073
- }
27719
+ if (this.running) {
27720
+ await this.tick();
27074
27721
  return;
27075
27722
  }
27076
- await this.tick();
27723
+ await this.startQueued();
27724
+ if (this.attempts.size === 0) {
27725
+ if (this.store.isFinished()) this.planStatus = "completed";
27726
+ this.emit("onTick");
27727
+ if (this.settled) await this.closeRun();
27728
+ this.emit("onExecutionComplete");
27729
+ }
27077
27730
  }
27078
27731
  /**
27079
27732
  * A repair's verdict never replaces the verdict the task's own work earned,
@@ -27341,11 +27994,17 @@ ${summary || "(empty: no output captured)"}`);
27341
27994
  const resumes = this.awaitsResume;
27342
27995
  this.retryCounts.set(taskId, (this.retryCounts.get(taskId) ?? 0) + 1);
27343
27996
  const ended = this.endAttempt(taskId, "retry");
27997
+ const movedFrom = this.movedFrom(task);
27344
27998
  this.store.retry(taskId);
27999
+ if (movedFrom) this.store.setCarriedFrom(taskId, movedFrom);
27345
28000
  this.store.unblockDependents(taskId);
27346
28001
  this.onHold.delete(taskId);
27347
28002
  this.emit("onTaskChanged");
27348
- await this.runs.release(taskId, { keep: false }, ended?.integration);
28003
+ if (movedFrom) {
28004
+ await ended?.integration;
28005
+ } else {
28006
+ await this.runs.release(taskId, { keep: false }, ended?.integration);
28007
+ }
27349
28008
  if (admittedAt !== this.stopGeneration) return;
27350
28009
  if (resumes) await this.start(origin);
27351
28010
  else await this.tick();
@@ -27396,6 +28055,16 @@ ${summary || "(empty: no output captured)"}`);
27396
28055
  this.emit("onTaskChanged");
27397
28056
  return true;
27398
28057
  }
28058
+ /**
28059
+ * The runner whose attempt left the task's work, when the task has been
28060
+ * moved off it since: a session cannot cross runners, so whatever starts
28061
+ * the task next is a carry-over of that work, the rule a paused task's move
28062
+ * follows (ADR-0018, U1).
28063
+ */
28064
+ movedFrom(task) {
28065
+ const owner = sessionOwner(task);
28066
+ return owner && owner !== this.store.resolveTaskRunner(task) ? owner : void 0;
28067
+ }
27399
28068
  /**
27400
28069
  * Run before writing assignments so cancellation sees the old runner's
27401
28070
  * shared wait. A session cannot cross runners, so the next attempt is a
@@ -27453,7 +28122,7 @@ ${summary || "(empty: no output captured)"}`);
27453
28122
  const summary = this.output.finalText(taskId);
27454
28123
  this.endAttempt(taskId, "usage-limit");
27455
28124
  this.pauseOnLimit(task, attempt, attempt.turnError ?? "");
27456
- this.store.setTaskOutputSummary(taskId, summarizeOutput(attempt.turnError ?? `${attempt.runner} hit its usage limit`, summary));
28125
+ this.store.setTaskOutputSummary(taskId, summarizeOutput(attempt.turnError?.message ?? `${attempt.runner} hit its usage limit`, summary));
27457
28126
  if (attempt.worktree) await this.runs.release(taskId, { keep: true });
27458
28127
  await this.afterVerdict(taskId);
27459
28128
  }
@@ -27552,10 +28221,14 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
27552
28221
  return false;
27553
28222
  }
27554
28223
  /**
27555
- * Manually start a single AI task right now, bypassing dependency/readiness
27556
- * gating (the "force start" affordance on a task card) but never the claim:
27557
- * it still needs the user's approval, a free slot, and no Stop since it was
27558
- * asked for, and throws {@link TaskControlError} saying which it lacks.
28224
+ * Manually start a single AI task right now (the "force start" affordance on
28225
+ * a task card), past a failed dependency (ADR-0034) or a merge gate
28226
+ * (ADR-0020), but never past the claim: it still needs the user's approval,
28227
+ * a free slot, and no Stop since it was asked for, and throws
28228
+ * {@link TaskControlError} saying which it lacks. A dependency still under
28229
+ * way or yet to run is not forced past: the task is released to the
28230
+ * scheduler instead, which starts it once they finish, and the line saying
28231
+ * so is the answer.
27559
28232
  * Reuses the scheduler's own claim so a force-started task gets the same
27560
28233
  * augmented prompt, session tracking, and exit handling — callers must not
27561
28234
  * re-spawn the runner themselves. No-op if the task is unknown, not an AI
@@ -27566,8 +28239,12 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
27566
28239
  if (!task || task.type !== "ai") return;
27567
28240
  if (this.attempts.has(taskId)) return;
27568
28241
  this.refuseOpsDuringMerge(task);
28242
+ const queued2 = this.queuedBehindDependencies(task);
28243
+ if (queued2) return queued2;
27569
28244
  await this.whileAdmitted(async (admittedAt) => {
27570
- if (!await this.openForAdmitted(taskId, origin, () => this.forceStartTask(taskId, origin), admittedAt)) return;
28245
+ if (!await this.openForAdmitted(taskId, origin, async () => {
28246
+ await this.forceStartTask(taskId, origin);
28247
+ }, admittedAt)) return;
27571
28248
  if (this.attempts.has(taskId)) return;
27572
28249
  this.refuseOpsDuringMerge(task);
27573
28250
  this.refuseClaim(task, admittedAt);
@@ -27576,6 +28253,68 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
27576
28253
  await this.claimed(task, admittedAt);
27577
28254
  });
27578
28255
  }
28256
+ /**
28257
+ * A force start of a task whose dependencies are still under way or yet to
28258
+ * run: started now, it would build on work that is not there, so it waits
28259
+ * for them in the run, off any hold, and says so. Dependencies are never
28260
+ * bypassed silently. A blocked task is forced past its failure as asked.
28261
+ */
28262
+ queuedBehindDependencies(task) {
28263
+ const waiting = this.underWayBefore(task);
28264
+ if (waiting.length === 0) return null;
28265
+ this.onHold.delete(task.id);
28266
+ if (!this.running || isBlocked(task, this.store)) this.queuedStarts.add(task.id);
28267
+ const names = waiting.map((id) => `task "${this.store.get(id)?.title ?? id}"`);
28268
+ const listed3 = names.length === 1 ? names[0] : `${names.slice(0, -1).join(", ")} and ${names.at(-1)}`;
28269
+ const idle = this.running || waiting.some((id) => this.attempts.has(id)) ? "" : " Nothing is running now: run the plan to start them.";
28270
+ const queued2 = `Task "${task.title}" is approved and starts once ${listed3} ${names.length === 1 ? "finishes" : "finish"}.${idle}`;
28271
+ this.tell("info", queued2);
28272
+ this.emit("onTaskChanged");
28273
+ return queued2;
28274
+ }
28275
+ /**
28276
+ * A task's unmet dependencies, less the failed or blocked ones a force start
28277
+ * goes past. A blocked one is gone past for its failure only: the work under
28278
+ * way or yet to run that it waits on is still waited for, through it.
28279
+ */
28280
+ underWayBefore(task) {
28281
+ const blocked = blockingFailures(this.store.allTasks);
28282
+ const waiting = /* @__PURE__ */ new Set();
28283
+ const seen = /* @__PURE__ */ new Set();
28284
+ const walk = (of) => {
28285
+ for (const id of unmetDependencies(of, this.store, this.runs)) {
28286
+ if (seen.has(id)) continue;
28287
+ seen.add(id);
28288
+ const dep = this.store.get(id);
28289
+ if (dep?.status === "failed") continue;
28290
+ if (dep && (dep.status === "blocked" || blocked.has(id))) walk(dep);
28291
+ else waiting.add(id);
28292
+ }
28293
+ };
28294
+ walk(task);
28295
+ return [...waiting];
28296
+ }
28297
+ /**
28298
+ * Starts the queued force starts whose dependencies have now finished. One
28299
+ * the scheduler now drives the run for, and nothing blocks, is left to it.
28300
+ */
28301
+ async startQueued() {
28302
+ for (const id of [...this.queuedStarts]) {
28303
+ const task = this.store.get(id);
28304
+ if (!task || this.attempts.has(id) || task.status === "completed" || task.status === "skipped") {
28305
+ this.queuedStarts.delete(id);
28306
+ continue;
28307
+ }
28308
+ if (this.underWayBefore(task).length > 0) continue;
28309
+ this.queuedStarts.delete(id);
28310
+ if (this.running && !isBlocked(task, this.store)) continue;
28311
+ try {
28312
+ await this.forceStartTask(id);
28313
+ } catch (err) {
28314
+ this.tell("warn", `Task "${task.title}" did not start once its dependencies finished: ${err instanceof Error ? err.message : String(err)}`);
28315
+ }
28316
+ }
28317
+ }
27579
28318
  refuseOpsDuringMerge(task) {
27580
28319
  if (this.merging && mergeExcludes(this.nextAttemptKind(task.id))) {
27581
28320
  throw new TaskControlError(`A Merge all is under way: ops task "${task.title}" can start once it has finished.`);
@@ -27640,6 +28379,7 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
27640
28379
  async tick() {
27641
28380
  const admittedAt = this.stopGeneration;
27642
28381
  this.recoverOrphans();
28382
+ await this.startQueued();
27643
28383
  if (!this.running) {
27644
28384
  if (this.attempts.size === 0 && this.runs.isOpen && this.settled) await this.closeRun();
27645
28385
  return;
@@ -27796,7 +28536,7 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
27796
28536
  if (refused3) return { refused: refused3 };
27797
28537
  const attempt = {
27798
28538
  taskId: task.id,
27799
- attempt: Math.max(this.spawnCounts.get(task.id) ?? 0, ...task.attemptUsage?.map((u) => u.attempt) ?? []) + 1,
28539
+ attempt: Math.max(this.spawnCounts.get(task.id) ?? 0, this.lastLoggedAttempt(task.id), ...task.attemptUsage?.map((u) => u.attempt) ?? []) + 1,
27800
28540
  phase: "starting",
27801
28541
  session: null,
27802
28542
  runner: this.store.resolveTaskRunner(task),
@@ -27810,6 +28550,7 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
27810
28550
  skills: [],
27811
28551
  autoResumed: opts.autoResumed === true,
27812
28552
  turnError: null,
28553
+ turnOutputFrom: 0,
27813
28554
  reminded: false
27814
28555
  };
27815
28556
  this.spawnCounts.set(task.id, attempt.attempt);
@@ -27883,6 +28624,8 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
27883
28624
  title: task.title,
27884
28625
  env,
27885
28626
  resumeSessionId: kind.kind === "continuation" ? kind.resumeSessionId : void 0,
28627
+ resumeCost: kind.kind === "continuation" ? countedSessionCost(task.attemptUsage, kind.resumeSessionId) : void 0,
28628
+ resumeWindow: kind.kind === "continuation" ? resumedSessionWindow(task.attemptUsage, attempt.runner, attempt.attempt, kind.resumeSessionId) : void 0,
27886
28629
  attempt: attempt.attempt,
27887
28630
  ...attempt.skills.length > 0 ? { skills: attempt.skills } : {},
27888
28631
  onNotice: (message) => this.tell("warn", `Task "${task.title}": ${message}`)
@@ -27907,8 +28650,21 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
27907
28650
  this.output.attach(task.id, session);
27908
28651
  this.verifier.watch(task, session);
27909
28652
  session.onEvent((event) => {
27910
- if (event.type === "turn_start") attempt.turnError = null;
27911
- else if (event.type === "error") attempt.turnError = event.message;
28653
+ if (event.type === "turn_start") {
28654
+ attempt.turnError = null;
28655
+ attempt.turnOutputFrom = session.getOutput().length;
28656
+ } else if (event.type === "error") attempt.turnError = event;
28657
+ else if (event.type === "model_switched" || event.type === "model_drifted") {
28658
+ this.emit("onModelSwitched", {
28659
+ taskId: task.id,
28660
+ sessionId: session.id,
28661
+ from: event.from,
28662
+ model: event.to,
28663
+ ...event.type === "model_switched" && event.effort ? { effort: event.effort } : {},
28664
+ asked: event.type === "model_switched",
28665
+ attemptEnded: this.attempts.get(task.id) !== attempt
28666
+ });
28667
+ }
27912
28668
  });
27913
28669
  session.onTurnEnd((reason) => this.onTurnEnd(task.id, attempt, session, reason));
27914
28670
  return true;
@@ -27918,6 +28674,16 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
27918
28674
  return false;
27919
28675
  }
27920
28676
  this.endAttempt(task.id, "spawn-failed");
28677
+ const refusal = err instanceof Error ? err.message : String(err);
28678
+ if (attempt.kind.kind !== "repair" && classifyRunnerStop(refusal) === "usage-limit") {
28679
+ this.pauseOnLimit(task, attempt, refusal);
28680
+ this.store.setTaskOutputSummary(task.id, summarizeOutput(refusal, ""));
28681
+ await this.runs.release(task.id, { keep: true });
28682
+ this.emit("onTaskSettled", { taskId: task.id });
28683
+ this.emit("onTaskChanged");
28684
+ await this.tick();
28685
+ return false;
28686
+ }
27921
28687
  if (attempt.kind.kind === "continuation") {
27922
28688
  const refused3 = err instanceof RunnerStartupExit;
27923
28689
  const reason = refused3 ? unresumedMessage(task, `${attempt.runner} could not find its saved session`) : unresumedMessage(task, `could not start: ${err instanceof Error ? err.message : String(err)}`);
@@ -28037,12 +28803,13 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
28037
28803
  endAttempt(taskId, reason) {
28038
28804
  const attempt = this.attempts.get(taskId);
28039
28805
  this.attempts.delete(taskId);
28806
+ const session = attempt?.session ?? null;
28807
+ const nativeSessionId = session?.nativeSessionId() ?? null;
28040
28808
  if (attempt) {
28041
- this.store.endAttemptUsage(taskId, attempt.attempt, (/* @__PURE__ */ new Date()).toISOString());
28809
+ this.store.endAttemptUsage(taskId, attempt.attempt, (/* @__PURE__ */ new Date()).toISOString(), session?.sessionCost?.() ?? void 0, nativeSessionId ?? void 0);
28042
28810
  this.output.detach(taskId);
28043
28811
  }
28044
- const session = attempt?.session ?? null;
28045
- if (session) this.saveNativeSession(taskId, session.nativeSessionId());
28812
+ if (attempt && session) this.saveNativeSession(taskId, nativeSessionId, attempt.runner);
28046
28813
  if (stopsRunner(reason)) {
28047
28814
  const task = this.store.get(taskId);
28048
28815
  if (task) this.verifier.clear(task);
@@ -28051,8 +28818,8 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
28051
28818
  return attempt;
28052
28819
  }
28053
28820
  /** Kept on the task once the attempt ends, so a continue can resume it after a reload (ADR-0018, K1). */
28054
- saveNativeSession(taskId, nativeSessionId) {
28055
- if (nativeSessionId) this.store.setTaskRunnerSessionId(taskId, nativeSessionId);
28821
+ saveNativeSession(taskId, nativeSessionId, runner) {
28822
+ if (nativeSessionId) this.store.setTaskRunnerSessionId(taskId, nativeSessionId, runner);
28056
28823
  }
28057
28824
  endAllAttempts(reason) {
28058
28825
  const ended = [...this.attempts.values()];
@@ -28062,13 +28829,17 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
28062
28829
  return ended;
28063
28830
  }
28064
28831
  };
28065
- var RESUME_MARGIN_MS = 6e4;
28066
- var RELAPSE_MS = 5 * 6e4;
28067
28832
  var LIMIT_RESET_MESSAGE = "Your usage limit has reset. Carry on with the task from where you stopped.";
28068
28833
  var OPS_RETRY_TAIL_LINES = 60;
28069
28834
  function unresumedMessage(task, why) {
28070
28835
  return `Could not continue task "${task.title}": ${why}. Retry starts it afresh.`;
28071
28836
  }
28837
+ function saidInFlight(attempt) {
28838
+ const session = attempt.session;
28839
+ if (!session || session.turnState() !== "working") return null;
28840
+ if (attempt.turnError?.usageLimit !== void 0) return attempt.turnError;
28841
+ return session.getOutput().slice(attempt.turnOutputFrom);
28842
+ }
28072
28843
 
28073
28844
  // src/services/Planner.ts
28074
28845
  function plannerModesWith(req) {
@@ -28226,21 +28997,18 @@ var ModelResolver = class {
28226
28997
  return this.cachedPickerOptions ?? [];
28227
28998
  }
28228
28999
  /**
28229
- * The planner model's context window, from whatever catalog is already
28230
- * cached (#49): a vendor model comes from the picker catalog, a harness
28231
- * planner's model from the runner's own discovered models. Reads only —
28232
- * never triggers a fetch or discovery of its own, so an unknown window stays
28233
- * unknown and context fill is omitted rather than guessed.
29000
+ * A model's context window from whatever catalog is already cached (#49),
29001
+ * for a usage record whose reporter stated none: a vendor's model from the
29002
+ * picker catalog (its own `/models` endpoint), a runner's model from that
29003
+ * runner's own discovered listing. `source` is the record's reporter, and
29004
+ * `model` the id it named. Reads only — never triggers a fetch or discovery
29005
+ * of its own, so an unknown window stays unknown and context fill is
29006
+ * omitted rather than guessed.
28234
29007
  */
28235
- contextWindowFor(modelId) {
28236
- if (!modelId) return void 0;
28237
- const picker = this.cachedPickerOptions?.find((o) => o.id === modelId)?.contextWindow;
28238
- if (picker && picker > 0) return picker;
28239
- for (const runner of this.config.enabledRunners ?? []) {
28240
- const found = this.discovery.getCached(runner)?.find((m) => m.modelId === modelId)?.contextWindow;
28241
- if (found && found > 0) return found;
28242
- }
28243
- return void 0;
29008
+ contextWindowFor(source, model) {
29009
+ if (!model) return void 0;
29010
+ const window = isApiProvider(source) ? this.cachedPickerOptions?.find((o) => o.apiProvider === source && o.id === prefixModelId(source, stripModelPrefix(model, source)))?.contextWindow : this.discovery.getCached(source)?.find((m) => m.modelId === model)?.contextWindow;
29011
+ return window && window > 0 ? window : void 0;
28244
29012
  }
28245
29013
  invalidate() {
28246
29014
  this.discovery.clear();
@@ -28744,6 +29512,8 @@ var PlannerSession = class {
28744
29512
  * outlives the session — nothing from one goal may reach the next.
28745
29513
  */
28746
29514
  nativeSessionId = null;
29515
+ /** What was counted of that session's running cost, so a resume counts on from it rather than dropping its first turn. */
29516
+ sessionCost = null;
28747
29517
  /** Set once the first spawn attached. */
28748
29518
  start = null;
28749
29519
  /**
@@ -28934,6 +29704,7 @@ var CliAgentAiService = class {
28934
29704
  // hit its rate limit, or lost its login must say so in the chat rather
28935
29705
  // than leave an empty planner bubble.
28936
29706
  failure: turn.error,
29707
+ usageLimit: turn.usageLimit,
28937
29708
  fullText: [...carried, turn.text].filter((part) => part.trim()).join("\n\n")
28938
29709
  };
28939
29710
  };
@@ -29000,6 +29771,7 @@ var CliAgentAiService = class {
29000
29771
  };
29001
29772
  const subagentUsage = /* @__PURE__ */ new Map();
29002
29773
  let error;
29774
+ let usageLimit;
29003
29775
  let stepIndex = 0;
29004
29776
  let backgroundAgents = 0;
29005
29777
  const truncate = (value) => value.length > LOG_MAX_CHARS ? `${value.slice(0, LOG_MAX_CHARS)}
@@ -29108,6 +29880,7 @@ var CliAgentAiService = class {
29108
29880
  }
29109
29881
  case "error":
29110
29882
  error = event.message;
29883
+ usageLimit = event.usageLimit;
29111
29884
  return;
29112
29885
  case "turn_end":
29113
29886
  return;
@@ -29126,12 +29899,13 @@ var CliAgentAiService = class {
29126
29899
  settle(id, "The agent ended the turn without reporting this call's result.", false, "not_executed");
29127
29900
  }
29128
29901
  session.nativeSessionId = adapter.nativeSessionId() ?? session.nativeSessionId;
29902
+ session.sessionCost = adapter.sessionCost?.() ?? session.sessionCost;
29129
29903
  if (error || signal?.aborted) session.stop();
29130
29904
  if (error) {
29131
29905
  const failure = failureAfterReply(text3.slice(finalRunStart), Math.max(blockStart, finalRunStart) - finalRunStart, error);
29132
29906
  onProgress({ type: "text_retracted", segmentId: finalSegment });
29133
29907
  text3 = text3.slice(0, finalRunStart);
29134
- return { text: text3, researchLog, error: failure, aborted: signal?.aborted, backgroundAgents };
29908
+ return { text: text3, researchLog, error: failure, usageLimit, aborted: signal?.aborted, backgroundAgents };
29135
29909
  }
29136
29910
  return { text: text3, researchLog, error, aborted: signal?.aborted, backgroundAgents };
29137
29911
  }
@@ -29187,7 +29961,7 @@ var CliAgentAiService = class {
29187
29961
  session.stop();
29188
29962
  const start = session.start;
29189
29963
  if (!start) throw new Error("No active planner conversation.");
29190
- return this.startAdapter({ ...start.startOptions, resumeSessionId: session.nativeSessionId ?? void 0 }, start.tools, session);
29964
+ return this.startAdapter({ ...start.startOptions, resumeSessionId: session.nativeSessionId ?? void 0, resumeCost: session.sessionCost ?? void 0 }, start.tools, session);
29191
29965
  }
29192
29966
  /**
29193
29967
  * What the planner gets of the repository's config, its runner being
@@ -29199,9 +29973,9 @@ var CliAgentAiService = class {
29199
29973
  const reader = connectorFor(this.runner)?.repoConfig;
29200
29974
  if (!reader) return { systemPrompt };
29201
29975
  const found = reader.projectServers(cwd);
29202
- const { started, held } = await decideProjectServers(found.servers, { repoRoot: repoRoot(cwd), approval, approvals: this.projectServerApprovals });
29976
+ const { started, held: held2 } = await decideProjectServers(found.servers, { repoRoot: repoRoot(cwd), approval, approvals: this.projectServerApprovals });
29203
29977
  return {
29204
- systemPrompt: [systemPrompt, instructionsBlock(reader.instructions(cwd)), heldServersNote(held.map((server4) => ({ server: server4, reason: NOT_APPROVED })), found.unreadable)].filter(Boolean).join("\n\n"),
29978
+ systemPrompt: [systemPrompt, instructionsBlock(reader.instructions(cwd)), heldServersNote(held2.map((server4) => ({ server: server4, reason: NOT_APPROVED })), found.unreadable)].filter(Boolean).join("\n\n"),
29205
29979
  ...started.length > 0 ? { projectServers: started } : {}
29206
29980
  };
29207
29981
  }
@@ -29366,6 +30140,10 @@ var SessionCatalog = class {
29366
30140
  allowlist() {
29367
30141
  return this.deps.settings().modelAllowlist ?? {};
29368
30142
  }
30143
+ /** The enabled runners and allowlist together, for whoever compares an assignment with them. */
30144
+ scope() {
30145
+ return { enabledRunners: this.enabledRunners(), allowlist: this.allowlist(), models: this.models() };
30146
+ }
29369
30147
  /** The models the user allows on one runner; unset means no restriction. */
29370
30148
  allowlistFor(runner) {
29371
30149
  return this.allowlist()[runner];
@@ -29380,7 +30158,7 @@ var SessionCatalog = class {
29380
30158
  */
29381
30159
  models() {
29382
30160
  const out = { ...this.discovered };
29383
- for (const runner of /* @__PURE__ */ new Set([...Object.keys(out), ...this.deps.planRunners()])) {
30161
+ for (const runner of /* @__PURE__ */ new Set([...Object.keys(out), ...this.deps.planRunners(), ...this.enabledRunners()])) {
29384
30162
  const cached2 = this.deps.modelResolver.getCachedRunnerModels(runner);
29385
30163
  if (cached2.length > 0) out[runner] = cached2;
29386
30164
  }
@@ -29417,9 +30195,16 @@ var SessionCatalog = class {
29417
30195
  * asked for.
29418
30196
  */
29419
30197
  async live() {
29420
- const runners = this.enabledRunners();
29421
- await this.discover(runners);
29422
- return this.queryCatalog(runners);
30198
+ await this.discover(this.enabledRunners());
30199
+ return this.current();
30200
+ }
30201
+ /**
30202
+ * What {@link live} answers, from what is cached and without discovering:
30203
+ * the enabled runners, however the plan was started. The per-turn block
30204
+ * reads this, so it can never offer a different set than `list_runners`.
30205
+ */
30206
+ current() {
30207
+ return this.queryCatalog(this.enabledRunners());
29423
30208
  }
29424
30209
  queryCatalog(runners) {
29425
30210
  const registered = runners.filter((runner) => this.deps.registry.getManifest(runner) !== void 0);
@@ -29445,7 +30230,8 @@ var SessionCatalog = class {
29445
30230
  modelsByRunner: this.models(),
29446
30231
  runnerModes: this.modes(runners),
29447
30232
  perRunnerAllowlist: this.allowlist(),
29448
- autonomousDefault: this.deps.config.autonomousMode
30233
+ autonomousDefault: this.deps.config.autonomousMode,
30234
+ ...this.deps.liveRunner ? { liveRunner: this.deps.liveRunner } : {}
29449
30235
  };
29450
30236
  }
29451
30237
  /** What a runner offers, as `runnerAssignment` needs it. Spawns the runner's CLI to list models. */
@@ -29510,6 +30296,108 @@ function forkPlanState(plan, tasks, dialogue, now) {
29510
30296
  };
29511
30297
  }
29512
30298
 
30299
+ // src/services/PlannerResend.ts
30300
+ var HELD = "Send a message once it has reset: what waits goes with it.";
30301
+ var WAIT = "planner";
30302
+ var PlannerResend = class {
30303
+ constructor(host) {
30304
+ this.host = host;
30305
+ }
30306
+ host;
30307
+ waits = new UsageLimitWaits(() => this.due());
30308
+ /** When the last resend at a reset went out, until a turn lands: a limit in that window is a relapse. */
30309
+ resentAt = null;
30310
+ /** Whether turns wait on the limit: until the reset, or for the user's next message. */
30311
+ holds() {
30312
+ return this.host.plan()?.plannerResend !== void 0;
30313
+ }
30314
+ /** A planner turn Ordewell started ended on the planner's usage limit, which it reported as `said`. */
30315
+ limited(said2) {
30316
+ const plan = this.host.plan();
30317
+ const runner = this.host.plannerRunner() ?? void 0;
30318
+ const now = /* @__PURE__ */ new Date();
30319
+ const relapsed = this.resentAt !== null && now.getTime() - this.resentAt < RELAPSE_MS;
30320
+ this.resentAt = null;
30321
+ this.waits.clear();
30322
+ const name = runner ?? "Planner";
30323
+ const reset = runner && !relapsed ? readUsageLimitReset(runner, said2, now) : null;
30324
+ if (!reset) {
30325
+ if (plan) plan.plannerResend = runner ? { runner } : {};
30326
+ const held2 = relapsed ? "hit it again right after the resend" : "did not say when it resets";
30327
+ const text3 = relapsed ? `${name} usage limit again right after the resend, so nothing is resent on its own. ${HELD}` : `${name} usage limit; it did not say when it resets. ${HELD}`;
30328
+ return { text: text3, resendAt: null, held: held2 };
30329
+ }
30330
+ const at = new Date(reset.getTime() + RESUME_MARGIN_MS);
30331
+ if (plan) plan.plannerResend = { runner, at: at.toISOString() };
30332
+ this.waits.arm(WAIT, at);
30333
+ return { text: `${name} usage limit, resets ${resumeTime(reset, now)}. Resending at ${resumeTime(at, now)}; anything you send meanwhile goes with it.`, resendAt: at };
30334
+ }
30335
+ /**
30336
+ * A new input arrived — the user's message, or a wake-up the supervisor
30337
+ * asked for. What was held for one goes with it now; so does what waits on
30338
+ * a runner the planner has left, since nothing it waits for applies to the
30339
+ * planner that will answer. True when it lifted a wait.
30340
+ */
30341
+ arrived() {
30342
+ const saved = this.host.plan()?.plannerResend;
30343
+ if (!saved || saved.at !== void 0 && !this.switched(saved)) return false;
30344
+ this.lift();
30345
+ return true;
30346
+ }
30347
+ /** Keep what waits from going on its own; it goes with the user's next message. False when nothing waits on a reset. */
30348
+ cancel() {
30349
+ const plan = this.host.plan();
30350
+ const saved = plan?.plannerResend;
30351
+ if (!plan || saved?.at === void 0) return false;
30352
+ this.waits.clear();
30353
+ plan.plannerResend = held(saved);
30354
+ return true;
30355
+ }
30356
+ /** A turn landed: the resend before it got past the limit. */
30357
+ landed() {
30358
+ this.resentAt = null;
30359
+ }
30360
+ /** A plan adopted off disk: re-arm the wait it saved. A time already past resends at once. */
30361
+ restore() {
30362
+ this.reset();
30363
+ const saved = this.host.plan()?.plannerResend;
30364
+ if (saved?.at === void 0) return;
30365
+ const at = Date.parse(saved.at);
30366
+ if (Number.isNaN(at)) delete saved.at;
30367
+ else this.waits.arm(WAIT, new Date(at));
30368
+ }
30369
+ /** The plan is gone: nothing it waited on carries over. */
30370
+ reset() {
30371
+ this.waits.clear();
30372
+ this.resentAt = null;
30373
+ }
30374
+ due() {
30375
+ const plan = this.host.plan();
30376
+ const saved = plan?.plannerResend;
30377
+ if (!plan || saved?.at === void 0) return;
30378
+ if (this.switched(saved)) {
30379
+ plan.plannerResend = held(saved);
30380
+ this.host.changed();
30381
+ this.host.notice(`The planner no longer runs on ${saved.runner}, whose usage limit it waited on, so nothing was resent. ${HELD}`);
30382
+ return;
30383
+ }
30384
+ delete plan.plannerResend;
30385
+ this.resentAt = Date.now();
30386
+ this.host.due();
30387
+ }
30388
+ lift() {
30389
+ this.waits.clear();
30390
+ const plan = this.host.plan();
30391
+ if (plan) delete plan.plannerResend;
30392
+ }
30393
+ switched(saved) {
30394
+ return saved.runner !== void 0 && saved.runner !== (this.host.plannerRunner() ?? void 0);
30395
+ }
30396
+ };
30397
+ function held({ runner }) {
30398
+ return runner ? { runner } : {};
30399
+ }
30400
+
29513
30401
  // src/services/PlanTurns.ts
29514
30402
  var START_HOLD_TIMEOUT_MS = 5 * 6e4;
29515
30403
  function turnPrompt(batch) {
@@ -29526,6 +30414,16 @@ var PlanTurns = class {
29526
30414
  constructor(host, holdTimeoutMs = START_HOLD_TIMEOUT_MS) {
29527
30415
  this.host = host;
29528
30416
  this.holdTimeoutMs = holdTimeoutMs;
30417
+ this.resend = new PlannerResend({
30418
+ plan: () => host.plan(),
30419
+ plannerRunner: () => host.plannerRunner(),
30420
+ due: () => {
30421
+ host.announce();
30422
+ this.pump();
30423
+ },
30424
+ changed: () => host.announce(),
30425
+ notice: (message) => host.notice(message)
30426
+ });
29529
30427
  }
29530
30428
  host;
29531
30429
  holdTimeoutMs;
@@ -29538,6 +30436,33 @@ var PlanTurns = class {
29538
30436
  /** The user stopped the turn wake-ups waited behind: they wait for the next input to open one, not for the stop's release. */
29539
30437
  quiet = false;
29540
30438
  seq = 0;
30439
+ resend;
30440
+ /** A turn ended on the planner's usage limit: arm the wait for its inputs. Saved with the failure the turn records. */
30441
+ usageLimit(said2) {
30442
+ return this.resend.limited(said2);
30443
+ }
30444
+ /** Keep what waits on the planner's usage limit from being sent at the reset; see {@link PlannerResend.cancel}. */
30445
+ cancelResend() {
30446
+ if (!this.resend.cancel()) return false;
30447
+ this.host.announce();
30448
+ return true;
30449
+ }
30450
+ /**
30451
+ * The conversation's opening ended on the planner's usage limit: its goal
30452
+ * waits with the limit, ahead of what was sent behind it, whose senders are
30453
+ * answered now as any sent during the wait are.
30454
+ */
30455
+ waitOnLimit(input) {
30456
+ const plan = this.host.plan();
30457
+ if (!plan) return;
30458
+ plan.pendingTurns = [this.pending(input, false), ...plan.pendingTurns ?? []];
30459
+ this.host.persist();
30460
+ for (const t of plan.pendingTurns) if (t.source === "user") this.settle(t.id, (w) => w.resolve(plan));
30461
+ }
30462
+ /** A plan adopted off disk: the wait it saved is armed again. */
30463
+ restoreResend() {
30464
+ this.resend.restore();
30465
+ }
29541
30466
  /** Whether a turn this module opened is in flight. */
29542
30467
  get busy() {
29543
30468
  return this.running !== null;
@@ -29579,25 +30504,33 @@ var PlanTurns = class {
29579
30504
  admit(input, forced) {
29580
30505
  const plan = this.host.plan();
29581
30506
  if (!plan) return Promise.reject(new Error("No planning conversation to continue"));
29582
- const entry = {
29583
- id: `turn-${Date.now()}-${++this.seq}`,
29584
- source: input.source,
29585
- text: input.text,
29586
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
29587
- ...input.skills && input.skills.length > 0 ? { skills: [...input.skills] } : {},
29588
- ...forced ? { forced: true } : {}
29589
- };
30507
+ const entry = this.pending(input, forced);
29590
30508
  this.quiet = false;
30509
+ if (this.resend.arrived()) this.host.announce();
29591
30510
  const pending = plan.pendingTurns ?? [];
29592
30511
  const at = forced ? pending.findIndex((t) => !t.forced && !this.running?.ids.has(t.id)) : -1;
29593
30512
  plan.pendingTurns = at === -1 ? [...pending, entry] : [...pending.slice(0, at), entry, ...pending.slice(at)];
29594
30513
  this.host.persist();
30514
+ if (input.source === "user" && this.resend.holds()) {
30515
+ this.pump();
30516
+ return Promise.resolve(plan);
30517
+ }
29595
30518
  const settled = new Promise((resolve9, reject) => {
29596
30519
  this.waiters.set(entry.id, { resolve: resolve9, reject, signal: input.signal });
29597
30520
  });
29598
30521
  this.pump();
29599
30522
  return settled;
29600
30523
  }
30524
+ pending(input, forced) {
30525
+ return {
30526
+ id: `turn-${Date.now()}-${++this.seq}`,
30527
+ source: input.source,
30528
+ text: input.text,
30529
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
30530
+ ...input.skills && input.skills.length > 0 ? { skills: [...input.skills] } : {},
30531
+ ...forced ? { forced: true } : {}
30532
+ };
30533
+ }
29601
30534
  /**
29602
30535
  * Start the next turn if one is due: nothing of ours is in flight, the
29603
30536
  * conversation is free, and inputs wait. The host calls it when any planner
@@ -29608,7 +30541,7 @@ var PlanTurns = class {
29608
30541
  * the plan, what still waits, and the planner in use at that moment.
29609
30542
  */
29610
30543
  pump() {
29611
- if (this.running || this.detecting) return;
30544
+ if (this.running || this.detecting || this.resend.holds()) return;
29612
30545
  const plan = this.host.plan();
29613
30546
  if (!plan || this.host.conversationBusy()) return;
29614
30547
  const batch = this.liveBatch(plan);
@@ -29688,6 +30621,7 @@ var PlanTurns = class {
29688
30621
  * conversation's to abandon; whatever it settles lands nothing here.
29689
30622
  */
29690
30623
  abandon() {
30624
+ this.resend.reset();
29691
30625
  this.running?.release();
29692
30626
  this.running = null;
29693
30627
  this.quiet = false;
@@ -29731,8 +30665,10 @@ var PlanTurns = class {
29731
30665
  clearTimeout(timeout);
29732
30666
  if (this.running !== running2) return;
29733
30667
  const forceStopped = !outcome.ok && running2.forceStopped === true;
30668
+ const limited = !outcome.ok && outcome.err instanceof PlannerUsageLimitError;
30669
+ if (outcome.ok) this.resend.landed();
29734
30670
  try {
29735
- const dropped = outcome.ok || isInterruption(outcome.err) && !forceStopped ? ids : new Set(batch.filter((t) => t.source === "user").map((t) => t.id));
30671
+ const dropped = limited ? /* @__PURE__ */ new Set() : outcome.ok || isInterruption(outcome.err) && !forceStopped ? ids : new Set(batch.filter((t) => t.source === "user").map((t) => t.id));
29736
30672
  const took = this.take(plan, dropped);
29737
30673
  if (forceStopped) plan.pendingTurns = [...(plan.pendingTurns ?? []).filter((t) => t.forced), ...(plan.pendingTurns ?? []).filter((t) => !t.forced)];
29738
30674
  if (took || forceStopped) this.host.persist();
@@ -29746,7 +30682,11 @@ var PlanTurns = class {
29746
30682
  const ended = outcome;
29747
30683
  const waiting = new Set((plan.pendingTurns ?? []).map((t) => t.id));
29748
30684
  for (const t of batch) {
29749
- if (forceStopped && waiting.has(t.id)) continue;
30685
+ if (limited && t.source === "user") {
30686
+ this.settle(t.id, (w) => w.resolve(plan));
30687
+ continue;
30688
+ }
30689
+ if ((forceStopped || limited) && waiting.has(t.id)) continue;
29750
30690
  const delivered = this.settle(t.id, (w) => ended.ok ? w.resolve(ended.plan) : w.reject(ended.err));
29751
30691
  if (!delivered && !ended.ok && !isInterruption(ended.err) && t.source === "user") this.host.notice(replayFailed(t, ended.err));
29752
30692
  }
@@ -29823,7 +30763,6 @@ var RunSupervision = class {
29823
30763
  blockedBy: (failedId) => [...blockingFailures([...this.host.tasks()])].filter(([, failures]) => failures.includes(failedId)).map(([taskId]) => taskId),
29824
30764
  readBudget: () => this.settings()?.wakeUpReadBudget ?? host.userSettings().wakeUpReadBudget,
29825
30765
  retryLimit: () => this.retryLimit(),
29826
- plannerRunner: () => host.plannerRunner(),
29827
30766
  restartPlanner: () => host.restartPlanner(),
29828
30767
  abandonTurn: () => host.abandonTurn(),
29829
30768
  goUnsupervised: (reason) => this.goUnsupervised(reason),
@@ -30032,14 +30971,14 @@ function split({ verdict, idleSince, isolation, checkpoint, queued: queued2, con
30032
30971
  return { plan, status: { verdict, idleSince, isolation, checkpoint, queued: queued2, continuable, awaitingApproval, mergeGate: mergeGate2, forcedPastGate, waitsForApproval: waitsForApproval2 } };
30033
30972
  }
30034
30973
  function refresh(previous, tasks) {
30035
- const held = /* @__PURE__ */ new Map();
30974
+ const held2 = /* @__PURE__ */ new Map();
30036
30975
  const index = (list) => list.forEach((t) => {
30037
- held.set(t.id, t);
30976
+ held2.set(t.id, t);
30038
30977
  index(t.subtasks);
30039
30978
  });
30040
30979
  index(previous);
30041
30980
  const fold = ({ subtasks, ...task }) => {
30042
- const was = held.get(task.id);
30981
+ const was = held2.get(task.id);
30043
30982
  const { checkpoint, ...status } = was ? split(was).status : {};
30044
30983
  const waits = task.status === "awaiting_user" && task.awaitingReason === "checkpoint";
30045
30984
  return { ...task, ...status, ...waits && checkpoint ? { checkpoint } : {}, subtasks: subtasks.map(fold) };
@@ -30413,9 +31352,9 @@ var fs14 = __toESM(require("fs"));
30413
31352
  var path31 = __toESM(require("path"));
30414
31353
 
30415
31354
  // src/utils/sessionId.ts
30416
- var import_crypto11 = require("crypto");
31355
+ var import_crypto12 = require("crypto");
30417
31356
  function mintSessionId() {
30418
- return `session-${(0, import_crypto11.randomBytes)(16).toString("hex")}`;
31357
+ return `session-${(0, import_crypto12.randomBytes)(16).toString("hex")}`;
30419
31358
  }
30420
31359
 
30421
31360
  // src/utils/sessionStore.ts
@@ -30452,8 +31391,8 @@ function dropLegacyFile(sessionsDir, meta) {
30452
31391
  const legacy = path31.join(sessionsDir, legacySessionFilename(meta));
30453
31392
  if (legacy === path31.join(sessionsDir, sessionFilename(meta)) || !fs14.existsSync(legacy)) return;
30454
31393
  try {
30455
- const held = JSON.parse(fs14.readFileSync(legacy, "utf-8")).meta?.id;
30456
- if (held === meta.id) fs14.unlinkSync(legacy);
31394
+ const held2 = JSON.parse(fs14.readFileSync(legacy, "utf-8")).meta?.id;
31395
+ if (held2 === meta.id) fs14.unlinkSync(legacy);
30457
31396
  } catch {
30458
31397
  }
30459
31398
  }
@@ -30586,24 +31525,25 @@ function listTaskLogAttempts(location, taskId) {
30586
31525
  }
30587
31526
  return names.map((name) => /^([1-9]\d*)\.jsonl$/.exec(name)?.[1]).filter((n) => n !== void 0).map(Number).sort((a, b) => a - b);
30588
31527
  }
30589
- function openTaskLog(location, taskId) {
31528
+ function openTaskLog(location, taskId, attempt) {
30590
31529
  ensureStateDirIgnored(location.baseDir);
30591
31530
  const dir = taskDir(location, taskId);
30592
31531
  fs15.mkdirSync(dir, { recursive: true });
30593
- let attempt = (listTaskLogAttempts(location, taskId).pop() ?? 0) + 1;
31532
+ const next = (listTaskLogAttempts(location, taskId).pop() ?? 0) + 1;
31533
+ let number = attempt !== void 0 && Number.isInteger(attempt) && attempt >= 1 ? attempt : next;
30594
31534
  for (; ; ) {
30595
- const file = path32.join(dir, `${attempt}${EXTENSION}`);
31535
+ const file = path32.join(dir, `${number}${EXTENSION}`);
30596
31536
  try {
30597
31537
  fs15.writeFileSync(file, "", { flag: "wx" });
30598
31538
  } catch (err) {
30599
31539
  if (err.code === "EEXIST") {
30600
- attempt += 1;
31540
+ number = Math.max(number + 1, next);
30601
31541
  continue;
30602
31542
  }
30603
31543
  throw err;
30604
31544
  }
30605
31545
  return {
30606
- attempt,
31546
+ attempt: number,
30607
31547
  append(events) {
30608
31548
  if (events.length === 0) return;
30609
31549
  fs15.appendFileSync(file, events.map((e) => `${JSON.stringify(e)}
@@ -30649,11 +31589,13 @@ var TaskLogRecorder = class {
30649
31589
  open;
30650
31590
  flushMs;
30651
31591
  logger;
31592
+ /** Each task's latest attempt, by the session it records: its log takes a {@link note} until the next attempt starts. */
31593
+ latest = /* @__PURE__ */ new Map();
30652
31594
  wrap(runner) {
30653
31595
  return {
30654
31596
  spawn: async (opts) => {
30655
31597
  const session = await runner.spawn(opts);
30656
- this.record(opts.taskId, session, opts.skills);
31598
+ this.record(opts.taskId, session, opts.skills, opts.attempt);
30657
31599
  return session;
30658
31600
  },
30659
31601
  stop: (sessionId) => runner.stop(sessionId),
@@ -30663,11 +31605,20 @@ var TaskLogRecorder = class {
30663
31605
  }
30664
31606
  };
30665
31607
  }
31608
+ /**
31609
+ * An entry the session did not emit, written into the log of the attempt
31610
+ * that runs `sessionId` and sent at once: what Ordewell made of one of its
31611
+ * events once that settled. Dropped once the task's next attempt started.
31612
+ */
31613
+ note(sessionId, taskId, entry) {
31614
+ const attempt = this.latest.get(taskId);
31615
+ if (attempt?.sessionId === sessionId) attempt.note(entry);
31616
+ }
30666
31617
  /** Start a new attempt's log for `session`, opening with the skills it was given, and keep it until the session exits. */
30667
- record(taskId, session, skills = []) {
31618
+ record(taskId, session, skills = [], attemptNumber) {
30668
31619
  let file;
30669
31620
  try {
30670
- file = this.open(this.deps.location(), taskId);
31621
+ file = this.open(this.deps.location(), taskId, attemptNumber);
30671
31622
  } catch (err) {
30672
31623
  this.logger.warn("taskLog", `could not create the log of task ${taskId}; it streams but is not saved`, err);
30673
31624
  file = null;
@@ -30694,6 +31645,10 @@ var TaskLogRecorder = class {
30694
31645
  }
30695
31646
  this.deps.broadcast({ type: "task_log", taskId, attempt, events: taskLogForSurface(events) });
30696
31647
  };
31648
+ this.latest.set(taskId, { sessionId: session.id, note: (entry) => {
31649
+ pending.push(entry);
31650
+ flush();
31651
+ } });
30697
31652
  const openCalls = /* @__PURE__ */ new Set();
30698
31653
  session.onEvent((event) => {
30699
31654
  const entry = toTaskLogEvent(event);
@@ -30714,21 +31669,82 @@ var TaskLogRecorder = class {
30714
31669
  }
30715
31670
  };
30716
31671
 
31672
+ // src/services/ContextWindowFill.ts
31673
+ var ContextWindowFill = class {
31674
+ constructor(catalog, last = {}) {
31675
+ this.catalog = catalog;
31676
+ if (last.contextWindow !== void 0) this.known.set(modelKey(last.source, last.model), last.contextWindow);
31677
+ }
31678
+ catalog;
31679
+ /** The window known for each model of the stream, by reporter and model. */
31680
+ known = /* @__PURE__ */ new Map();
31681
+ fill(record) {
31682
+ if (record.subagentId || record.aggregate) return record;
31683
+ const key = modelKey(record.source, record.model);
31684
+ const reported = record.contextWindow !== void 0 && record.contextWindow > 0 ? record.contextWindow : void 0;
31685
+ const window = reported ?? this.known.get(key) ?? (record.model ? positive(this.catalog(record.source, record.model)) : void 0);
31686
+ if (window !== void 0) this.known.set(key, window);
31687
+ return window === void 0 || window === record.contextWindow ? record : { ...record, contextWindow: window };
31688
+ }
31689
+ };
31690
+ function modelKey(source, model) {
31691
+ return JSON.stringify([source ?? null, model ?? null]);
31692
+ }
31693
+ function withContextWindows(runner, catalog) {
31694
+ return {
31695
+ spawn: async (opts) => windowedSession(await runner.spawn(opts), new ContextWindowFill(catalog, opts.resumeWindow)),
31696
+ stop: (sessionId) => runner.stop(sessionId),
31697
+ stopAll: () => runner.stopAll(),
31698
+ get activeCount() {
31699
+ return runner.activeCount;
31700
+ }
31701
+ };
31702
+ }
31703
+ function windowedSession(session, windows) {
31704
+ const listeners = [];
31705
+ session.onEvent((event) => {
31706
+ const filled = event.type === "usage" ? { ...event, record: windows.fill(event.record) } : event;
31707
+ for (const listener of listeners) listener(filled);
31708
+ });
31709
+ const onEvent = (listener) => {
31710
+ listeners.push(listener);
31711
+ };
31712
+ return new Proxy(session, {
31713
+ get: (target, prop) => {
31714
+ if (prop === "onEvent") return onEvent;
31715
+ const value = Reflect.get(target, prop, target);
31716
+ return typeof value === "function" ? value.bind(target) : value;
31717
+ }
31718
+ });
31719
+ }
31720
+ function positive(n) {
31721
+ return n !== void 0 && n > 0 ? n : void 0;
31722
+ }
31723
+
30717
31724
  // src/services/PlannerUsage.ts
30718
31725
  var PlannerUsageLedger = class {
31726
+ /** `catalogWindow` serves the window of a planner model whose backend reports none. */
31727
+ constructor(catalogWindow = () => void 0) {
31728
+ this.catalogWindow = catalogWindow;
31729
+ this.windows = new ContextWindowFill(catalogWindow);
31730
+ }
31731
+ catalogWindow;
30719
31732
  usage = { totals: {} };
31733
+ windows;
30720
31734
  /** Fold one model call's record into the totals; returns the running ledger. */
30721
31735
  record(record) {
30722
- this.usage = addPlannerUsage(this.usage, record);
31736
+ this.usage = addPlannerUsage(this.usage, this.windows.fill(record));
30723
31737
  return this.usage;
30724
31738
  }
30725
31739
  /** Adopt a persisted ledger (a reopened session) or start from zero. */
30726
31740
  restore(usage) {
30727
31741
  this.usage = usage ?? { totals: {} };
31742
+ this.windows = new ContextWindowFill(this.catalogWindow, this.usage);
30728
31743
  }
30729
31744
  /** Start a fresh session's ledger from zero. */
30730
31745
  clear() {
30731
31746
  this.usage = { totals: {} };
31747
+ this.windows = new ContextWindowFill(this.catalogWindow);
30732
31748
  }
30733
31749
  /** Whether anything has been recorded — a plan with no usage says nothing. */
30734
31750
  get hasUsage() {
@@ -30823,19 +31839,54 @@ var PlanEditor = class {
30823
31839
  * between AI and MAN, are the patches that can leave a task incoherent
30824
31840
  * (unschedulable, or carrying fields that mean nothing for its new type),
30825
31841
  * so both go through the same {@link validateTaskEdit} guard the planner's
30826
- * task-ops applier uses (as the 'direct' actor, which skips the lock rule)
30827
- * rather than a second copy of the rules — and throws, so the surface can
30828
- * say why. Gated on the task existing so an edit to an unknown id still
31842
+ * task-ops applier uses (as the 'direct' actor, which skips the lock rule
31843
+ * but not a live runner's hold on its model) rather than a second copy of
31844
+ * the rules — and throws, so the surface can say why. Gated on the task existing so an edit to an unknown id still
30829
31845
  * falls through to the no-op `store.update` below instead of throwing.
30830
31846
  * Attached skills meet the rule a planner's do: a planner skill is refused,
30831
- * a name not found yet only warns.
31847
+ * a name not found yet only warns. A runner change is made by
31848
+ * {@link setTaskRunner}, which re-derives what the runner scopes, ahead of
31849
+ * the rest of the patch.
30832
31850
  *
30833
31851
  * Edits to one task commit in call order: the skill check waits on git, so
30834
31852
  * two quick chip edits (`[a]` then `[a, b]`) could otherwise land the first
30835
31853
  * one last.
30836
31854
  */
30837
31855
  updateTask(taskId, changes) {
30838
- const edit = (this.taskEdits.get(taskId) ?? Promise.resolve()).then(() => this.applyTaskUpdate(taskId, changes));
31856
+ return this.inTaskOrder(taskId, () => this.applyTaskUpdate(taskId, changes));
31857
+ }
31858
+ /**
31859
+ * The plan follows a model switch the user made with the runner's own
31860
+ * command (#106), as a direct edit of theirs: saved and announced as
31861
+ * `task_updated`, so every surface shows it, and what a later retry or
31862
+ * continue spawns on. It is the one model edit a live runner allows, since
31863
+ * it says what that runner already runs, and the one that skips the
31864
+ * allowlist: the user picked it in the runner. The runner's own label is
31865
+ * used only for a model discovery does not list. Effort is the runner's
31866
+ * when it reported one, else the plan's, either clamped to what the new
31867
+ * model offers; none for a model discovery does not list. `live` is asked
31868
+ * as the edit lands, after the edits queued ahead of it: by then the plan
31869
+ * may be another one, whose task of the same id the switch says nothing of.
31870
+ */
31871
+ followRunnerModel(taskId, model, effort, live) {
31872
+ return this.inTaskOrder(taskId, async () => {
31873
+ const task = this.store.get(taskId);
31874
+ if (!task || task.type === "user" || !live()) return null;
31875
+ const listed3 = this.catalog.models()[task.assignedRunner]?.find((m) => m.modelId === model.model);
31876
+ const thinkingEffort = clampThinkingEffort(effort ?? task.assignedModel?.thinkingEffort, listed3?.variants ?? []);
31877
+ if (task.assignedModel?.modelId === model.model && task.assignedModel.thinkingEffort === thinkingEffort) return this.plan();
31878
+ const assignedModel = {
31879
+ modelId: model.model,
31880
+ modelLabel: listed3?.modelLabel ?? model.label ?? model.model,
31881
+ ...thinkingEffort ? { thinkingEffort } : {},
31882
+ ...listed3 ? { availableVariants: listed3.variants.map((v) => v.id) } : {}
31883
+ };
31884
+ return this.commitTaskUpdate(taskId, { assignedModel });
31885
+ });
31886
+ }
31887
+ /** Edits to one task commit in call order; see {@link updateTask}. */
31888
+ inTaskOrder(taskId, apply) {
31889
+ const edit = (this.taskEdits.get(taskId) ?? Promise.resolve()).then(apply);
30839
31890
  const settled = edit.catch(() => {
30840
31891
  });
30841
31892
  this.taskEdits.set(taskId, settled);
@@ -30851,22 +31902,31 @@ var PlanEditor = class {
30851
31902
  const target = this.store.get(taskId);
30852
31903
  const skills = "skills" in changes && target ? await checkTaskSkillsEdit(target, typedSkills, this.taskSkills()) : void 0;
30853
31904
  if (skills?.errors.length) throw new PlanEditError(skills.errors.map((e) => e.message).join(" "));
30854
- if ((changes.dependencies || changes.type || changes.assignedModel || changes.taskMode || "ops" in changes) && target) {
31905
+ if ((changes.dependencies || changes.type || "assignedModel" in changes || "assignedRunner" in changes || changes.taskMode || "ops" in changes) && target) {
30855
31906
  const check2 = validateTaskEdit("direct", this.store.planTasks, taskId, changes, this.catalog.edit());
30856
31907
  if (!check2.ok) throw new PlanEditError(check2.error ?? "Those changes are not valid");
30857
31908
  if (check2.clear?.length || check2.set) {
30858
31909
  changes = { ...changes, ...Object.fromEntries((check2.clear ?? []).map((f) => [f, void 0])), ...check2.set };
30859
31910
  }
30860
31911
  }
30861
- const plan = await this.commit(
31912
+ const { assignedRunner, ...rest } = changes;
31913
+ if (target && assignedRunner && assignedRunner !== target.assignedRunner) {
31914
+ const moved = await this.setTaskRunner(taskId, assignedRunner);
31915
+ if (!moved || Object.keys(rest).length === 0) return moved;
31916
+ changes = rest;
31917
+ }
31918
+ const plan = await this.commitTaskUpdate(taskId, changes);
31919
+ if (plan) for (const warning of skills?.warnings ?? []) this.notice("warn", warning);
31920
+ return plan;
31921
+ }
31922
+ commitTaskUpdate(taskId, changes) {
31923
+ return this.commit(
30862
31924
  () => {
30863
31925
  changes = this.scheduler.releaseMovedResume(taskId, changes);
30864
31926
  return Boolean(this.store.update(taskId, changes));
30865
31927
  },
30866
31928
  () => this.broadcast({ type: "task_updated", taskId, changes })
30867
31929
  );
30868
- if (plan) for (const warning of skills?.warnings ?? []) this.notice("warn", warning);
30869
- return plan;
30870
31930
  }
30871
31931
  /**
30872
31932
  * Replace one task's dependency list — the named entry point the surfaces'
@@ -30883,7 +31943,8 @@ var PlanEditor = class {
30883
31943
  * because a runner change is never a single-field edit: the task's model,
30884
31944
  * thinking effort and mode are all scoped to its runner, so they are
30885
31945
  * re-derived from the new runner's catalog (see {@link retargetTaskRunner}).
30886
- * That needs discovery, which is why it is not a branch inside `updateTask`.
31946
+ * That needs discovery, which is why `updateTask` hands a runner change here
31947
+ * rather than writing it as a field.
30887
31948
  *
30888
31949
  * The runner is also admitted into `plan.runners` — see {@link admitRunner}.
30889
31950
  */
@@ -30893,11 +31954,13 @@ var PlanEditor = class {
30893
31954
  const task = this.store.get(taskId);
30894
31955
  if (!task) return null;
30895
31956
  if (task.assignedRunner === runner || task.type === "user") return plan;
31957
+ this.refuseLiveRetarget(task);
30896
31958
  const catalog = await this.catalog.runner(runner);
30897
31959
  const from = this.catalog.edit([task.assignedRunner]).runnerModes[task.assignedRunner];
30898
31960
  const changes = retargetTaskRunner(task, runner, this.allowedCatalog(catalog, runner), from);
30899
31961
  if (Object.keys(changes).length === 0) return this.plan();
30900
31962
  this.refuseUnavailableMode(runner, changes.taskMode, catalog.modes);
31963
+ this.refuseLiveRetarget(task);
30901
31964
  return this.commit(() => {
30902
31965
  if (!this.store.update(taskId, this.scheduler.releaseMovedResume(taskId, changes))) return false;
30903
31966
  this.admitRunner(runner, catalog.models);
@@ -30931,6 +31994,9 @@ var PlanEditor = class {
30931
31994
  return true;
30932
31995
  });
30933
31996
  }
31997
+ refuseLiveRetarget(task) {
31998
+ if (this.catalog.edit().liveRunner?.(task.id)) throw new PlanEditError(liveRunnerRefusal(task));
31999
+ }
30934
32000
  /**
30935
32001
  * A mode the host cannot run is refused, with its reason, not swapped for
30936
32002
  * another: the plan is the source of truth for what runs (ADR-0001), and a
@@ -31260,6 +32326,10 @@ var ExecutionCommands = class {
31260
32326
  const all = this.all(tasks);
31261
32327
  const live = all.filter(({ task }) => this.host.running(task.id));
31262
32328
  if (this.host.hasLiveWork()) {
32329
+ if (actor.kind === "user") {
32330
+ const waiting = all.filter(({ task }) => this.host.waitsForApproval(task.id)).length;
32331
+ return { act: async () => ({ message: await this.host.joinRun(waiting) }) };
32332
+ }
31263
32333
  return { refusal: `The plan is already running (${named2(live)} ${live.length === 1 ? "is" : "are"} running). Start one more ready task with run_task, or wait.` };
31264
32334
  }
31265
32335
  const toRun = all.filter(({ task }) => task.type === "ai" && task.status !== "completed" && task.status !== "skipped");
@@ -31685,8 +32755,11 @@ function mergeStopped(result) {
31685
32755
  switch (result.outcome) {
31686
32756
  case "merged":
31687
32757
  return "it merged";
31688
- case "blocked":
31689
- return "the user's checkout has changes that overlap what it would merge";
32758
+ case "blocked": {
32759
+ const off = result.blocked.filter((b) => b.reason === "wrong-branch");
32760
+ if (off.length === 0) return "the user's checkout has changes that overlap what it would merge";
32761
+ return off.map((b) => `${b.repo === "." ? "the checkout" : b.repo} is ${b.actual ? `on ${b.actual}` : b.rebasing ? "in the middle of a rebase" : "on a detached HEAD"}, not ${b.expected}, which the run started on`).join("; ");
32762
+ }
31690
32763
  case "conflict":
31691
32764
  return `it conflicts in ${result.repo}${result.files?.length ? `: ${result.files.slice(0, 5).join(", ")}` : ""}`;
31692
32765
  case "failed":
@@ -31811,11 +32884,17 @@ function liveAiService(config, workspaceRoot, mcpServer) {
31811
32884
  function isPlannerApproval(request) {
31812
32885
  return !isRunnerApproval(request);
31813
32886
  }
32887
+ function writtenTasks(before, after) {
32888
+ const said2 = ({ subtasks: _subtasks, status: _status, ...task }) => JSON.stringify(task);
32889
+ const prior = new Map(flattenTasks(before).map((t) => [t.id, said2(t)]));
32890
+ return flattenTasks(after).filter((t) => prior.get(t.id) !== said2(t)).map((t) => t.id);
32891
+ }
31814
32892
  function createSession(deps) {
31815
32893
  const pinnedAiService = deps.aiService;
31816
32894
  const mcpServer = deps.mcpServer ?? sharedMcpServer();
31817
32895
  const aiService = pinnedAiService ? () => pinnedAiService : liveAiService(deps.config, deps.workspaceRoot, mcpServer);
31818
32896
  const store = new PlanStore();
32897
+ const catalogWindow = (source, model) => deps.modelResolver.contextWindowFor?.(source, model);
31819
32898
  const taskLogs = new TaskLogRecorder({
31820
32899
  broadcast: deps.broadcast,
31821
32900
  // Read through the Session, whose id and workspace are the ones it saves
@@ -31849,7 +32928,7 @@ function createSession(deps) {
31849
32928
  const orchestrator = TaskOrchestrator.compose({
31850
32929
  config: deps.config,
31851
32930
  notifications: deps.notifications,
31852
- runner: taskLogs.wrap(runnerApprovals.wrap(deps.runner)),
32931
+ runner: taskLogs.wrap(runnerApprovals.wrap(withContextWindows(deps.runner, catalogWindow))),
31853
32932
  store,
31854
32933
  output: deps.taskOutput,
31855
32934
  isolation: deps.isolation,
@@ -31857,10 +32936,11 @@ function createSession(deps) {
31857
32936
  workspaceRoot: deps.workspaceRoot,
31858
32937
  skillsAt: (roots) => (deps.skillsService ?? new SkillsService(roots)).forRoot(roots),
31859
32938
  previousAttemptFromLog: (taskId) => lastAttemptDigest(session.taskLogLocation, taskId),
32939
+ lastLoggedAttempt: (taskId) => session.taskLogAttempts(taskId).at(-1) ?? 0,
31860
32940
  runSettings: () => session.planState?.runSettings,
31861
32941
  isApproved: (taskId) => session.isApproved(taskId)
31862
32942
  });
31863
- const usage = new PlannerUsageLedger();
32943
+ const usage = new PlannerUsageLedger(catalogWindow);
31864
32944
  const events = new SessionEventRelay({
31865
32945
  broadcast: deps.broadcast,
31866
32946
  onNotice: deps.onNotice,
@@ -31909,7 +32989,8 @@ function createSession(deps) {
31909
32989
  modelResolver: deps.modelResolver,
31910
32990
  settings: deps.settings,
31911
32991
  // Read through the Session, which holds the plan; the catalog is built first.
31912
- planRunners: () => session.planState?.runners ?? []
32992
+ planRunners: () => session.planState?.runners ?? [],
32993
+ liveRunner: (taskId) => orchestrator.getAttempt(taskId) !== void 0
31913
32994
  });
31914
32995
  const session = new Session({
31915
32996
  config: deps.config,
@@ -31931,6 +33012,7 @@ function createSession(deps) {
31931
33012
  usage,
31932
33013
  approvals,
31933
33014
  approvalPolicy,
33015
+ taskLogs,
31934
33016
  // Every planner's web reads, carried here or natively, are decided by the
31935
33017
  // one policy, which asks through the same channel as paths and commands.
31936
33018
  // The response cache and search provider are process-wide, so parallel
@@ -31981,6 +33063,7 @@ var Session = class {
31981
33063
  fsAdapter;
31982
33064
  approvals;
31983
33065
  approvalPolicy;
33066
+ taskLogs;
31984
33067
  fetcher;
31985
33068
  settingsFn;
31986
33069
  save;
@@ -32016,7 +33099,7 @@ var Session = class {
32016
33099
  turnId: () => this.conversation.currentTurnId,
32017
33100
  recordSkillLoad: (skill) => this.conversation.recordPlannerSkill(skill),
32018
33101
  liveCatalog: () => this.liveCatalog(),
32019
- coerce: (tasks) => coerceAssignments(tasks, this.catalog.allowlist(), this.catalog.models()),
33102
+ coerce: (tasks) => coerceAssignments(tasks, this.catalog.allowlist(), this.catalog.models(), this.store.planTasks),
32020
33103
  submitPlan: (tasks) => this.submitPlanFromTool(tasks),
32021
33104
  editPlan: (ops) => this.editPlanFromTool(ops),
32022
33105
  tasks: () => this.store.planTasks,
@@ -32061,6 +33144,7 @@ var Session = class {
32061
33144
  this.usage = parts.usage;
32062
33145
  this.approvals = parts.approvals;
32063
33146
  this.approvalPolicy = parts.approvalPolicy;
33147
+ this.taskLogs = parts.taskLogs;
32064
33148
  this.fetcher = parts.fetcher;
32065
33149
  this.skillsService = parts.skillsService;
32066
33150
  this.save = parts.saveSession;
@@ -32070,7 +33154,8 @@ var Session = class {
32070
33154
  aiService: () => this.aiService(),
32071
33155
  onProgress: (p) => this.events.progress(p),
32072
33156
  opening: (runners) => this.conversationOpening(runners),
32073
- catalog: () => this.catalog.queryCatalog(this.plan?.runners ?? []),
33157
+ catalog: () => this.catalog.current(),
33158
+ assignmentScope: () => this.catalog.scope(),
32074
33159
  autorun: () => this.userRunSettings().autorun,
32075
33160
  takeRunUpdates: () => this.untoldRunUpdates.splice(0),
32076
33161
  tasks: () => this.store.planTasks,
@@ -32090,13 +33175,18 @@ var Session = class {
32090
33175
  turnAborted: () => this.approvals.clear(isPlannerApproval),
32091
33176
  // After the turn has announced its end, so the next one never starts
32092
33177
  // ahead of it on a surface.
32093
- turnReleased: () => queueMicrotask(() => this.turns.pump())
33178
+ turnReleased: () => queueMicrotask(() => this.turns.pump()),
33179
+ usageLimit: (said2) => this.turns.usageLimit(said2)
32094
33180
  });
32095
33181
  this.turns = new PlanTurns({
32096
33182
  plan: () => this.closed ? null : this.plan,
32097
33183
  conversationBusy: () => this.conversation.isTurnInFlight,
32098
33184
  mayWake: () => this.supervision.mayWake(),
32099
33185
  persist: () => this.persist({ background: true }),
33186
+ announce: () => {
33187
+ this.persist({ background: true });
33188
+ this.events.planGenerated(this.plan, this.goal);
33189
+ },
32100
33190
  interrupt: () => {
32101
33191
  this.conversation.stopTurn("force_send");
32102
33192
  },
@@ -32113,7 +33203,8 @@ var Session = class {
32113
33203
  },
32114
33204
  notice: (message) => this.notice("error", message),
32115
33205
  plannerDetecting: () => installedPlannerAnswered() ? null : installedPlannerDetected(),
32116
- assertPlannerReady: () => this.assertPlannerReady()
33206
+ assertPlannerReady: () => this.assertPlannerReady(),
33207
+ plannerRunner: () => runnerForProvider(this.config.aiProvider)
32117
33208
  });
32118
33209
  this.supervision = new RunSupervision({
32119
33210
  plan: () => this.plan,
@@ -32131,7 +33222,6 @@ var Session = class {
32131
33222
  },
32132
33223
  deliver: (digest) => this.turns.enqueue({ source: "supervisor", text: digest }),
32133
33224
  pump: () => this.turns.pump(),
32134
- plannerRunner: () => runnerForProvider(this.config.aiProvider),
32135
33225
  restartPlanner: () => this.aiService().restart?.(),
32136
33226
  abandonTurn: () => this.conversation.abandonHungTurn(),
32137
33227
  notice: (level, message) => this.notice(level, message)
@@ -32213,6 +33303,7 @@ var Session = class {
32213
33303
  tasks: () => this.plan ? this.store.planTasks : null,
32214
33304
  planApproved: () => this.plan?.approved === true,
32215
33305
  taskApproved: (taskId) => this.isApproved(taskId),
33306
+ waitsForApproval: (taskId) => this.waitsForApproval(taskId),
32216
33307
  approve: (taskIds) => this.approve(taskIds),
32217
33308
  autonomous: () => this.config.autonomousMode,
32218
33309
  autorun: () => this.userRunSettings().autorun,
@@ -32234,6 +33325,7 @@ var Session = class {
32234
33325
  blockedBy: (taskId) => this.orchestrator.getBlockedBy(taskId),
32235
33326
  waitingRequests: (taskId) => this.approvals.outstanding().filter(({ request }) => isRunnerApproval(request) && request.taskId === taskId).map(({ id, request }) => ({ id, subject: request.subject })),
32236
33327
  runPlan: (actor) => this.runWholePlan(runOrigin(actor)),
33328
+ joinRun: (approved) => this.joinRun(approved),
32237
33329
  startTask: (taskId, actor) => this.startOneTask(taskId, runOrigin(actor)),
32238
33330
  stopTask: (taskId) => this.cancelTask(taskId),
32239
33331
  stopRun: () => this.stopExecution(),
@@ -32262,8 +33354,8 @@ var Session = class {
32262
33354
  * one: nothing it would start is approved until the user executes it.
32263
33355
  */
32264
33356
  heldEdit(taskId) {
32265
- const held = this.conversation.pendingOps();
32266
- if (!held || held.length === 0 || !this.plan) return false;
33357
+ const held2 = this.conversation.pendingOps();
33358
+ if (!held2 || held2.length === 0 || !this.plan) return false;
32267
33359
  if (taskId === void 0) return true;
32268
33360
  const before = this.store.get(taskId);
32269
33361
  const after = this.asEdited(taskId);
@@ -32276,10 +33368,10 @@ var Session = class {
32276
33368
  /** {@link editedTasks}, with the id each of the held batch's handles names in them. */
32277
33369
  editedPlan() {
32278
33370
  const unedited = { tasks: this.store.planTasks, handles: /* @__PURE__ */ new Map() };
32279
- const held = this.conversation.pendingOps();
32280
- if (!held || held.length === 0 || !this.plan) return unedited;
33371
+ const held2 = this.conversation.pendingOps();
33372
+ if (!held2 || held2.length === 0 || !this.plan) return unedited;
32281
33373
  const runners = this.editRunners();
32282
- const result = applyTaskOps(this.store.planTasks, held, runners, this.catalog.edit(runners));
33374
+ const result = applyTaskOps(this.store.planTasks, held2, runners, this.catalog.edit(runners));
32283
33375
  return result.ok ? { tasks: result.tasks, handles: result.handles ?? /* @__PURE__ */ new Map() } : unedited;
32284
33376
  }
32285
33377
  /** The task as the open turn's held edit leaves it; undefined when that edit removes it. */
@@ -32303,9 +33395,9 @@ var Session = class {
32303
33395
  * rules, then, for the supervisor, the oscillation guard. `held` are the
32304
33396
  * ops the turn already holds, which `ops` join; errors are numbered by `ops`.
32305
33397
  */
32306
- checkPlannerOps(ops, held = []) {
33398
+ checkPlannerOps(ops, held2 = []) {
32307
33399
  const runners = this.editRunners();
32308
- const result = applyTaskOps(this.store.planTasks, ops, runners, this.catalog.edit(runners), held);
33400
+ const result = applyTaskOps(this.store.planTasks, ops, runners, this.catalog.edit(runners), held2);
32309
33401
  if (!result.ok || !this.supervisorEditing()) return result;
32310
33402
  const trip = this.supervision.checkEdit(this.store.planTasks, result.tasks);
32311
33403
  if (!trip) return result;
@@ -32475,6 +33567,9 @@ var Session = class {
32475
33567
  onUsageLimitWait: ({ taskId, runner, resumeAt, cancelled }) => {
32476
33568
  if (!cancelled) this.supervision.observe({ kind: "usage_limit", taskId, runner, resumeAt });
32477
33569
  },
33570
+ onModelSwitched: (switched) => {
33571
+ void this.followRunnerModel(switched);
33572
+ },
32478
33573
  onTaskIdle: ({ taskId, idleSince }) => {
32479
33574
  this.supervision.observe({ kind: "task_idle", taskId, detail: `no output since ${idleSince}` });
32480
33575
  },
@@ -32595,10 +33690,12 @@ var Session = class {
32595
33690
  * boundary rewrites `in_progress` to `pending` — nothing is running when a
32596
33691
  * session comes off a file — so a surface that re-reads the plan mid-run must
32597
33692
  * come here instead, or every task it is watching reads as never started.
33693
+ * An archived snapshot carries the task as it is now: a retry or continue
33694
+ * keeps the archive while the live task runs on and spends more.
32598
33695
  */
32599
33696
  get currentPlanState() {
32600
33697
  if (!this.plan) return null;
32601
- const executionLog = this.store.getExecutionLog();
33698
+ const executionLog = this.store.getExecutionLog().map((s) => ({ ...s, ...this.store.get(s.id) }));
32602
33699
  const logged = new Set(executionLog.map((s) => s.id));
32603
33700
  const pendingTasks = this.store.planTasks.filter((t) => !logged.has(t.id));
32604
33701
  const record = approvalRecordOf(this.plan);
@@ -32609,7 +33706,7 @@ var Session = class {
32609
33706
  phase: "executing",
32610
33707
  history: [],
32611
33708
  message: "",
32612
- executionLog: [...executionLog],
33709
+ executionLog,
32613
33710
  pendingTasks,
32614
33711
  goal: this.goal,
32615
33712
  runners: this.plan.runners,
@@ -32779,16 +33876,20 @@ var Session = class {
32779
33876
  opened = this.openTurn(() => this.conversation.start(this.resolveSkillInvocation(goal), () => this.conversationOpening(chosenRunners), options?.signal));
32780
33877
  } catch (err) {
32781
33878
  if (this.opening === pending) this.opening = null;
32782
- for (const held2 of pending.held ?? []) held2.reject(err);
33879
+ for (const held3 of pending.held ?? []) held3.reject(err);
32783
33880
  throw err;
32784
33881
  }
32785
- const held = pending.held ?? [];
33882
+ const held2 = pending.held ?? [];
32786
33883
  pending.held = null;
32787
- for (const h of held) this.turns.follow(h.input, h.forced).then(h.resolve, h.reject);
33884
+ for (const h of held2) this.turns.follow(h.input, h.forced).then(h.resolve, h.reject);
32788
33885
  try {
32789
33886
  const { landed: landed2, effects } = await opened;
32790
33887
  this.autorunSubmitted(effects);
32791
33888
  return landed2;
33889
+ } catch (err) {
33890
+ if (!(err instanceof PlannerUsageLimitError) || !this.plan) throw err;
33891
+ this.turns.waitOnLimit({ source: "user", ...this.resolveSkillInvocation(goal) });
33892
+ return this.plan;
32792
33893
  } finally {
32793
33894
  if (this.opening === pending) this.opening = null;
32794
33895
  }
@@ -32803,10 +33904,10 @@ var Session = class {
32803
33904
  behindOpening(input, forced = false) {
32804
33905
  const pending = this.opening;
32805
33906
  if (!pending) return null;
32806
- const held = pending.held;
32807
- if (!held) return this.turns.follow(input, forced);
33907
+ const held2 = pending.held;
33908
+ if (!held2) return this.turns.follow(input, forced);
32808
33909
  return new Promise((resolve9, reject) => {
32809
- held.push({ input, forced, resolve: resolve9, reject });
33910
+ held2.push({ input, forced, resolve: resolve9, reject });
32810
33911
  });
32811
33912
  }
32812
33913
  /**
@@ -32905,12 +34006,12 @@ var Session = class {
32905
34006
  await this.liveCatalog();
32906
34007
  if (this.conversation.currentTurnId !== turn) return refuse(PLANNER_TURN_ENDED);
32907
34008
  if (!this.plan || this.store.planTasks.length === 0) return refuse("There is no plan to edit yet: submit one with submit_plan.");
32908
- const held = this.conversation.pendingOps();
32909
- if (!held) return refuse("A whole plan was submitted in this reply and replaces the plan when it ends: make the edit in your next reply.");
32910
- const result = this.checkPlannerOps(ops, held);
34009
+ const held2 = this.conversation.pendingOps();
34010
+ if (!held2) return refuse("A whole plan was submitted in this reply and replaces the plan when it ends: make the edit in your next reply.");
34011
+ const result = this.checkPlannerOps(ops, held2);
32911
34012
  if (!result.ok) return { ok: false, errors: result.errors };
32912
- if (!this.conversation.submit({ kind: "task_ops", ops: [...held, ...ops] })) return refuse("No planning turn is open to take the edit.");
32913
- const { coerced } = coerceAssignments(result.tasks, this.catalog.allowlist(), this.catalog.models());
34013
+ if (!this.conversation.submit({ kind: "task_ops", ops: [...held2, ...ops] })) return refuse("No planning turn is open to take the edit.");
34014
+ const { coerced } = coerceAssignments(result.tasks, this.catalog.allowlist(), this.catalog.models(), this.store.planTasks);
32914
34015
  return { ok: true, summary: result.summary, coerced: coerced.map(({ taskId: _id, ...rest }) => rest) };
32915
34016
  }
32916
34017
  /** Whether the planner conversation is live (started and not yet committed to a plan). */
@@ -32947,9 +34048,6 @@ var Session = class {
32947
34048
  autonomousDefault: this.config.autonomousMode,
32948
34049
  autorun: this.userRunSettings().autorun,
32949
34050
  isolatedExecution: await this.runs.plannerLayout(),
32950
- // The planner's own model window, when a cached catalog knows it, so the
32951
- // usage line can show context fill (#49). Unknown stays absent.
32952
- contextWindow: this.modelResolver.contextWindowFor?.(this.config.orchestratorModel),
32953
34051
  fs: this.fsAdapter,
32954
34052
  fetcher: this.fetcher,
32955
34053
  approval: this.approvalPolicy,
@@ -32976,7 +34074,7 @@ var Session = class {
32976
34074
  for (const runner of runnersOf(tasks)) this.editor.admitRunner(runner, this.catalog.known(runner));
32977
34075
  const runners = this.plan.runners;
32978
34076
  const before = this.store.planTasks;
32979
- let coerced = coerceAssignments(tasks, this.catalog.allowlist(), this.catalog.models()).tasks;
34077
+ let coerced = coerceAssignments(tasks, this.catalog.allowlist(), this.catalog.models(), before).tasks;
32980
34078
  if (how === "commit") coerced = keepExecutionState(before, carryPlannedModes(before, coerced, this.catalog.edit(runners)));
32981
34079
  const bySupervisor = this.supervisorEditing();
32982
34080
  const { tasks: stamped, added } = stampOrigin(before, coerced, bySupervisor);
@@ -32989,6 +34087,7 @@ var Session = class {
32989
34087
  this.store.resetForRun({ keepScheduledResumes: true });
32990
34088
  }
32991
34089
  if (bySupervisor) this.approve(added);
34090
+ else if (this.conversation.currentWakeUp === void 0 && this.userRunSettings().autorun) this.approve(writtenTasks(before, coerced));
32992
34091
  return coerced.length;
32993
34092
  }
32994
34093
  preparePlannerTaskUpdates(tasks) {
@@ -33023,6 +34122,11 @@ var Session = class {
33023
34122
  if (changes.mergeAtGatesAutomatically) this.orchestrator.runSettingsChanged();
33024
34123
  }
33025
34124
  async executePlan() {
34125
+ if (this.orchestrator.hasLiveWork) {
34126
+ const outcome = await this.execute({ command: "run_plan" }, { kind: "user" });
34127
+ if (!outcome.ok) throw new TaskControlError(outcome.message);
34128
+ return;
34129
+ }
33026
34130
  this.idlePlan();
33027
34131
  this.approve(flattenTasks(this.store.planTasks).map((t) => t.id));
33028
34132
  await this.runWholePlan("user");
@@ -33044,6 +34148,25 @@ var Session = class {
33044
34148
  await this.orchestrator.approveReview(origin);
33045
34149
  });
33046
34150
  }
34151
+ /**
34152
+ * Execute while a run is under way (#95): the seam has approved every task,
34153
+ * as executing does, and the run takes them in. A run the scheduler was not
34154
+ * driving, one task started on its own, becomes the plan's run, as Execute
34155
+ * would make it.
34156
+ */
34157
+ async joinRun(approved) {
34158
+ const plan = this.plan;
34159
+ const scheduling = this.orchestrator.isScheduling;
34160
+ await this.withSave(async () => {
34161
+ plan.approved = true;
34162
+ if (scheduling) await this.orchestrator.tick();
34163
+ else if (this.orchestrator.isReviewApproved) await this.orchestrator.start("user");
34164
+ else await this.orchestrator.approveReview("user");
34165
+ });
34166
+ const line = approved === 0 ? scheduling ? "The plan is already running, and no task waits for your approval." : "The plan is running: each task starts once its dependencies finish." : approved === 1 ? "Approved 1 task; it starts once its dependencies finish." : `Approved ${approved} tasks; each starts once its dependencies finish.`;
34167
+ this.notice("info", line);
34168
+ return line;
34169
+ }
33047
34170
  async approveReview() {
33048
34171
  if (!this.plan) throw new NoPlanError("review");
33049
34172
  const plan = this.plan;
@@ -33052,9 +34175,12 @@ var Session = class {
33052
34175
  await this.withSave(() => this.orchestrator.approveReview("user"));
33053
34176
  return plan;
33054
34177
  }
34178
+ /** Answers the line saying the task waits for its dependencies, when it was queued behind them rather than started. */
33055
34179
  async forceStartTask(taskId) {
33056
34180
  this.approveTask(taskId);
33057
- await this.startOneTask(taskId, "user");
34181
+ if (!this.plan) return;
34182
+ this.requireTask(taskId);
34183
+ return this.withSave(() => this.orchestrator.forceStartTask(taskId, "user"));
33058
34184
  }
33059
34185
  async startOneTask(taskId, origin) {
33060
34186
  if (!this.plan) return;
@@ -33111,10 +34237,51 @@ var Session = class {
33111
34237
  async resumeNow(taskId) {
33112
34238
  await this.withSave(() => this.orchestrator.resumeNow(taskId, "user"));
33113
34239
  }
34240
+ /**
34241
+ * Keep what waits on the planner's usage limit from being sent at the reset:
34242
+ * it goes with the user's next message instead. False when nothing waits on
34243
+ * a reset.
34244
+ */
34245
+ cancelPlannerResend() {
34246
+ return this.turns.cancelResend();
34247
+ }
33114
34248
  /** Keep a task paused on a usage limit from resuming on its own; see {@link TaskOrchestrator.cancelScheduledResume}. */
33115
34249
  cancelScheduledResume(taskId) {
33116
34250
  return this.saved(() => this.orchestrator.cancelScheduledResume(taskId));
33117
34251
  }
34252
+ /**
34253
+ * The plan follows a model switch the user made in the runner (#106), and
34254
+ * the task's log says what the plan holds once that settled: a write-back
34255
+ * not made — no model command asked for the change, or the edit failed — is
34256
+ * stated, never shown as made. Once the switch's attempt has ended, the plan
34257
+ * may be another one, so nothing of it is said.
34258
+ */
34259
+ async followRunnerModel({ taskId, sessionId, from, model, effort, asked, attemptEnded }) {
34260
+ let kept = asked ? void 0 : "no model command in the chat asked for it";
34261
+ const live = () => this.orchestrator.activeSessionMap.get(taskId) === sessionId;
34262
+ const ended = () => this.taskLogs.note(sessionId, taskId, { type: "model_switched", from, to: model, kept: kept ?? "its attempt had already ended" });
34263
+ if (attemptEnded) return ended();
34264
+ if (asked) {
34265
+ try {
34266
+ if (!await this.editor.followRunnerModel(taskId, model, effort, live) && !live()) return ended();
34267
+ } catch (err) {
34268
+ kept = `the plan could not follow it: ${err instanceof Error ? err.message : String(err)}`;
34269
+ this.notice("warn", `The plan could not follow the model switch of task ${this.store.get(taskId)?.order ?? taskId}: ${err instanceof Error ? err.message : String(err)}`);
34270
+ }
34271
+ }
34272
+ const assigned = this.store.get(taskId)?.assignedModel;
34273
+ if (!kept && assigned?.modelId !== model.model) kept = "the plan did not take the edit";
34274
+ this.taskLogs.note(sessionId, taskId, {
34275
+ type: "model_switched",
34276
+ from,
34277
+ to: model,
34278
+ plan: {
34279
+ ...assigned ? { model: { model: assigned.modelId, label: assigned.modelLabel } } : {},
34280
+ ...assigned?.thinkingEffort ? { effort: assigned.thinkingEffort } : {}
34281
+ },
34282
+ ...kept ? { kept } : {}
34283
+ });
34284
+ }
33118
34285
  /** Said through both channels: a host shows one or the other, never both (the daemon's toasts are no-ops). */
33119
34286
  notice(level, message) {
33120
34287
  this.notifications[level](message);
@@ -33186,8 +34353,8 @@ var Session = class {
33186
34353
  if (!this.plan) return EMPTY_RUN;
33187
34354
  const planned = reduceRun(EMPTY_RUN, { type: "plan_snapshot", plan: surfacePlan({ ...this.plan, tasks: this.store.snapshot() }) });
33188
34355
  const view = reduceRun(planned, this.events.statusMessage());
33189
- const held = this.sentHeld ? reduceRun(view, { type: "plan_edit_held", held: this.sentHeld }) : view;
33190
- return this.sentHandoff ? reduceRun(held, { type: "isolation_handoff", ...this.sentHandoff }) : held;
34356
+ const held2 = this.sentHeld ? reduceRun(view, { type: "plan_edit_held", held: this.sentHeld }) : view;
34357
+ return this.sentHandoff ? reduceRun(held2, { type: "isolation_handoff", ...this.sentHandoff }) : held2;
33191
34358
  }
33192
34359
  /** The ancestors a task waits on at its merge gate (ADR-0020); empty when it waits for no Merge all. */
33193
34360
  mergeGate(taskId) {
@@ -33346,6 +34513,7 @@ var Session = class {
33346
34513
  if (adopting) this.dropPlanScope();
33347
34514
  this.plan = plan;
33348
34515
  if (adopting) this.supervision.restore(plan.supervision);
34516
+ if (adopting) this.turns.restoreResend();
33349
34517
  this.goal = goal;
33350
34518
  this.workspace = workspace;
33351
34519
  plan.approved ??= plan.status !== "draft" && plan.status !== "rejected";
@@ -33588,6 +34756,7 @@ function syncTranscript(view, { plan, turnId }) {
33588
34756
  if (isNew && entry.kind === "wake_up") next = takePendingInputs(next, void 0, [entry.content]).view;
33589
34757
  if (isNew && entry.kind === "plan_generated") next = markPlan(next, entry.content, i === committed ? turnId : void 0);
33590
34758
  if (isNew && entry.kind === "system") next = append(next, (id) => settledMessage(id, "system", entry.content));
34759
+ if (isNew && entry.kind === "error") next = append(next, (id) => settledMessage(id, "error", entry.content));
33591
34760
  if (isNew && entry.kind === "compaction") next = markCompaction(next, entry.content);
33592
34761
  if (latest === void 0 || entry.timestamp > latest) latest = entry.timestamp;
33593
34762
  }
@@ -33668,6 +34837,8 @@ function roleOf(entry) {
33668
34837
  return "wake_up";
33669
34838
  case "supervisor":
33670
34839
  return "supervisor";
34840
+ case "error":
34841
+ return "error";
33671
34842
  default:
33672
34843
  return entry.role === "user" ? "user" : "planner";
33673
34844
  }
@@ -33932,13 +35103,18 @@ function blockedBy(task, tasks) {
33932
35103
  if (!tasks?.length || !NOT_STARTED.has(task.status)) return [];
33933
35104
  return blockingFailures(asBlocking(tasks)).get(task.id) ?? [];
33934
35105
  }
35106
+ function flagOf(task, scope) {
35107
+ const issue = scope && task.assignedRunner !== void 0 ? assignmentIssue({ ...task, assignedRunner: task.assignedRunner }, scope) : null;
35108
+ return issue ? { assignmentFlag: assignmentIssueText(issue), assignmentConsequence: assignmentConsequence(task) } : { assignmentFlag: null, assignmentConsequence: null };
35109
+ }
33935
35110
  function taskRowView(task, context = {}) {
33936
35111
  const parent = context.parent ?? void 0;
33937
35112
  const isolation = task.isolation && task.isolation.state !== "none" ? task.isolation : null;
33938
- const latestUsage = task.attemptUsage?.at(-1);
35113
+ const spend = taskUsageSummary(task);
35114
+ const latestAttempt = task.attemptUsage?.at(-1);
33939
35115
  const blocked = blockedBy(task, context.tasks);
33940
35116
  return {
33941
- usage: latestUsage ? { ...usageLine(latestUsage), attempt: latestUsage.attempt, wallTimeMs: latestUsage.wallTimeMs, byModel: latestUsage.byModel } : task.type === "ai" && ["completed", "failed", "in_progress", "awaiting_user"].includes(task.status) ? { totals: {}, attempt: 0, byModel: [] } : null,
35117
+ usage: latestAttempt ? { totals: spend.totals, ...spend.contextFill ? { contextFill: spend.contextFill } : {}, attempt: latestAttempt.attempt } : task.type === "ai" && ["completed", "failed", "in_progress", "awaiting_user"].includes(task.status) ? { totals: {}, attempt: 0 } : null,
33942
35118
  orderLabel: taskOrderLabel(task, parent),
33943
35119
  kind: task.type === "user" ? "user" : task.ops ? "ops" : "ai",
33944
35120
  status: blocked.length > 0 ? "blocked" : taskStatusKind(task),
@@ -33947,6 +35123,7 @@ function taskRowView(task, context = {}) {
33947
35123
  resumes: resumeLabel(task, context.now),
33948
35124
  approvals: approvalLabel(task.awaitingApproval),
33949
35125
  waitsForApproval: task.waitsForApproval && NOT_STARTED.has(task.status) ? WAITS_FOR_APPROVAL_LABEL : null,
35126
+ ...flagOf(task, context.runnerScope),
33950
35127
  autonomous: task.type === "ai" && context.modes?.find((m) => m.id === task.taskMode)?.autonomous === true,
33951
35128
  repos: namedRepos(isolation?.repos),
33952
35129
  conflict: isolation?.state === "conflict" ? { repo: namedRepo(isolation.conflictRepo), files: filesOf(isolation.conflictFiles) } : null,
@@ -34152,6 +35329,7 @@ var StructuredSession = class extends AbstractRunnerSession {
34152
35329
  messageCount = 0;
34153
35330
  interrupting = null;
34154
35331
  lastSessionId = null;
35332
+ lastSessionCost = null;
34155
35333
  structuredEmitter = new import_events2.EventEmitter();
34156
35334
  permissionCount = 0;
34157
35335
  /**
@@ -34356,6 +35534,9 @@ var StructuredSession = class extends AbstractRunnerSession {
34356
35534
  nativeSessionId() {
34357
35535
  return this.adapter?.nativeSessionId() ?? this.lastSessionId;
34358
35536
  }
35537
+ sessionCost() {
35538
+ return this.adapter?.sessionCost?.() ?? this.lastSessionCost;
35539
+ }
34359
35540
  answerPermission(id, decision) {
34360
35541
  const open = this.permissions.get(id);
34361
35542
  if (!open) return false;
@@ -34403,6 +35584,7 @@ var StructuredSession = class extends AbstractRunnerSession {
34403
35584
  */
34404
35585
  async restartInterrupted(turn) {
34405
35586
  const resumeSessionId = this.nativeSessionId() ?? this.startOptions.resumeSessionId;
35587
+ const resumeCost = this.sessionCost() ?? this.startOptions.resumeCost;
34406
35588
  this.generation += 1;
34407
35589
  turn.abort.abort();
34408
35590
  if (this.adapter) {
@@ -34412,7 +35594,7 @@ var StructuredSession = class extends AbstractRunnerSession {
34412
35594
  for (const message of this.queue) this.requeue(message);
34413
35595
  try {
34414
35596
  this.revokeTools();
34415
- await this.startAttached({ ...this.startOptions, resumeSessionId });
35597
+ await this.startAttached({ ...this.startOptions, resumeSessionId, resumeCost });
34416
35598
  } catch (err) {
34417
35599
  this.text.line(`Could not restart ${this.launch.runner} after the interrupt: ${err instanceof Error ? err.message : String(err)}`);
34418
35600
  this.withdrawPermissions();
@@ -34439,6 +35621,7 @@ var StructuredSession = class extends AbstractRunnerSession {
34439
35621
  return;
34440
35622
  }
34441
35623
  this.lastSessionId = adapter.nativeSessionId() ?? this.lastSessionId;
35624
+ this.lastSessionCost = adapter.sessionCost?.() ?? this.lastSessionCost;
34442
35625
  this.withdrawPermissions();
34443
35626
  this.baseHandleExit(code);
34444
35627
  });
@@ -34476,6 +35659,7 @@ var StructuredSession = class extends AbstractRunnerSession {
34476
35659
  this.handleEvent({ type: "error", message: err instanceof Error ? err.message : String(err) });
34477
35660
  }).then(() => {
34478
35661
  this.lastSessionId = adapter.nativeSessionId() ?? this.lastSessionId;
35662
+ this.lastSessionCost = adapter.sessionCost?.() ?? this.lastSessionCost;
34479
35663
  if (generation2 === this.generation) this.endTurn(turn, turn.reason);
34480
35664
  });
34481
35665
  this.offerNext();
@@ -34602,6 +35786,11 @@ var StructuredSession = class extends AbstractRunnerSession {
34602
35786
  case "error":
34603
35787
  this.text.failure(event.message);
34604
35788
  break;
35789
+ // The plan follows the switch (#106), so a restart after an interrupt
35790
+ // runs what the plan now says rather than what this attempt started on.
35791
+ case "model_switched":
35792
+ this.startOptions = { ...this.startOptions, model: event.to.model };
35793
+ break;
34605
35794
  default:
34606
35795
  break;
34607
35796
  }
@@ -34682,7 +35871,8 @@ var StructuredRunner = class extends AbstractRunner {
34682
35871
  model: opts.modelId,
34683
35872
  mode: opts.mode ?? "default",
34684
35873
  flags: resolveTaskRunnerFlags(manifest, { mode: opts.mode ?? "default", model: opts.modelId, thinkingEffort: opts.thinkingEffort }),
34685
- resumeSessionId: opts.resumeSessionId
35874
+ resumeSessionId: opts.resumeSessionId,
35875
+ resumeCost: opts.resumeCost
34686
35876
  },
34687
35877
  interruptGraceMs: this.interruptGraceMs,
34688
35878
  tools: { server: this.mcp, scope: { sessionId: opts.planSessionId ?? "", taskId: opts.taskId, attempt: opts.attempt ?? 1 } },
@@ -34985,6 +36175,7 @@ var PlannerModelMemory = class {
34985
36175
  PlannerNotReadyError,
34986
36176
  PlannerTurnDiscardedError,
34987
36177
  PlannerTurnStoppedError,
36178
+ PlannerUsageLimitError,
34988
36179
  REFUSED_COMMANDS,
34989
36180
  RUN_SETTINGS_DEFAULTS,
34990
36181
  RunSettingsError,
@@ -35041,6 +36232,8 @@ var PlannerModelMemory = class {
35041
36232
  approvalScopes,
35042
36233
  assertWorkspaceExists,
35043
36234
  assertWorkspaceIsProject,
36235
+ assignmentIssue,
36236
+ assignmentIssueText,
35044
36237
  augmentedPath,
35045
36238
  automergeSummary,
35046
36239
  autonomyLevelLabel,
@@ -35057,6 +36250,7 @@ var PlannerModelMemory = class {
35057
36250
  canSetDependencies,
35058
36251
  canSplitTask,
35059
36252
  capConflictFiles,
36253
+ checkoutRule,
35060
36254
  classifyCommand,
35061
36255
  classifyOutcome,
35062
36256
  clearAugmentedPathCache,
@@ -35070,6 +36264,7 @@ var PlannerModelMemory = class {
35070
36264
  commandAllowedBy,
35071
36265
  configuredProviders,
35072
36266
  continuability,
36267
+ countedSessionCost,
35073
36268
  createAiService,
35074
36269
  createEmptyPlan,
35075
36270
  createSession,
@@ -35133,6 +36328,7 @@ var PlannerModelMemory = class {
35133
36328
  isMeasured,
35134
36329
  isOpenAiProvider,
35135
36330
  isPlannerBackend,
36331
+ isRepoGroup,
35136
36332
  isRunnerApproval,
35137
36333
  isSupervisorLevel,
35138
36334
  isTaskRunning,
@@ -35155,6 +36351,7 @@ var PlannerModelMemory = class {
35155
36351
  markdownHtml,
35156
36352
  markdownTokens,
35157
36353
  mcpClientConfig,
36354
+ mergeTarget,
35158
36355
  migrateOldConfigDir,
35159
36356
  migratePlanIsolation,
35160
36357
  migratePlanState,
@@ -35204,6 +36401,7 @@ var PlannerModelMemory = class {
35204
36401
  removedPluginNotice,
35205
36402
  renderTaskQueryAnswer,
35206
36403
  replayTaskLog,
36404
+ reposWithWork,
35207
36405
  researchShellWarning,
35208
36406
  researchToolsPath,
35209
36407
  resetInstalledPlanner,
@@ -35217,6 +36415,7 @@ var PlannerModelMemory = class {
35217
36415
  resolveWithin,
35218
36416
  resumeLabel,
35219
36417
  resumeTime,
36418
+ resumedSessionWindow,
35220
36419
  runSettingsOf,
35221
36420
  runnerForProvider,
35222
36421
  runnerModesFrom,
@@ -35257,6 +36456,7 @@ var PlannerModelMemory = class {
35257
36456
  taskRowView,
35258
36457
  taskStartedNotice,
35259
36458
  taskStatusKind,
36459
+ taskUsageSummary,
35260
36460
  textHasTaskOps,
35261
36461
  textHasTaskQuery,
35262
36462
  titledRefs,