@ordewell/core 0.10.3 → 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-DCd-rkeH.d.ts → IFileSystem-ClBdp59f.d.ts} +1 -1
  2. package/dist/{IFileSystem-B8Nkgg4e.d.mts → IFileSystem-Db34maxC.d.mts} +1 -1
  3. package/dist/{IRunner-CXdYyIZo.d.mts → IRunner-Crkt3tHD.d.ts} +64 -3
  4. package/dist/{IRunner-QBdYcatb.d.ts → IRunner-lMQIa5Wc.d.mts} +64 -3
  5. package/dist/{ModeResolver-geqjKkLY.d.mts → ModeResolver-B4hhABNf.d.mts} +1 -1
  6. package/dist/{ModeResolver-BOU90Nn-.d.ts → ModeResolver-CUR5jCSU.d.ts} +1 -1
  7. package/dist/{Task-CRVIqSfy.d.mts → Task-D4QHjdGv.d.mts} +93 -11
  8. package/dist/{Task-CRVIqSfy.d.ts → Task-D4QHjdGv.d.ts} +93 -11
  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 +307 -66
  19. package/dist/index.d.ts +307 -66
  20. package/dist/index.js +1688 -402
  21. package/dist/index.js.map +1 -1
  22. package/dist/index.mjs +1567 -388
  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-oC8bO4pN.d.ts → plan-utils-BVJCzNvm.d.ts} +204 -19
  29. package/dist/{plan-utils-KU1Rse82.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.mjs CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  looksLikePlanAttempt,
3
3
  parsePlanJson,
4
4
  validatePlanTasks
5
- } from "./chunk-645FALZX.mjs";
5
+ } from "./chunk-F3KIX7WR.mjs";
6
6
  import {
7
7
  ACT_RISK,
8
8
  AUTORUN_RISK,
@@ -19,6 +19,9 @@ import {
19
19
  addUsage,
20
20
  addUsageTotals,
21
21
  aheadOfDraft,
22
+ assignmentConsequence,
23
+ assignmentIssue,
24
+ assignmentIssueText,
22
25
  automergeSummary,
23
26
  autorunSummary,
24
27
  blockingFailures,
@@ -26,6 +29,7 @@ import {
26
29
  capConflictFiles,
27
30
  classifyOutcome,
28
31
  continuability,
32
+ countedSessionCost,
29
33
  dependentsNotice,
30
34
  diffRows,
31
35
  diffStat,
@@ -60,11 +64,13 @@ import {
60
64
  reduceRun,
61
65
  reduceTaskLog,
62
66
  replayTaskLog,
67
+ resumedSessionWindow,
63
68
  runnerToolSubject,
64
69
  sameMessage,
65
70
  serializePlan,
66
71
  serializeTask,
67
72
  serializeTaskStatus,
73
+ sessionOwner,
68
74
  skillLoadNotice,
69
75
  startAttemptUsage,
70
76
  stopTurn,
@@ -78,13 +84,14 @@ import {
78
84
  taskRowActions,
79
85
  taskRowView,
80
86
  taskStartedNotice,
87
+ taskUsageSummary,
81
88
  titledRefs,
82
89
  toolHeadline,
83
90
  truncateCheckpointSummary,
84
91
  unsendAll,
85
92
  unsendLatest,
86
93
  usageLine
87
- } from "./chunk-FH6M5QZG.mjs";
94
+ } from "./chunk-6LEA6WAM.mjs";
88
95
  import {
89
96
  NOT_STARTED,
90
97
  PLAN_ENVELOPE_KEY,
@@ -111,6 +118,7 @@ import {
111
118
  extractObjectsWithKey,
112
119
  filterModelsForPrompt,
113
120
  isTaskRunning,
121
+ liveRunnerRefusal,
114
122
  opensWithJsonObject,
115
123
  parseAutonomyLevel,
116
124
  parseTaskOpsJson,
@@ -130,7 +138,7 @@ import {
130
138
  unavailableModeReason,
131
139
  unavailableTaskModeMessage,
132
140
  validateTaskEdit
133
- } from "./chunk-A57ITQE7.mjs";
141
+ } from "./chunk-4QLFM73A.mjs";
134
142
  import {
135
143
  SELF_REPO,
136
144
  absorbRemoval,
@@ -144,7 +152,7 @@ import {
144
152
  noRemoval,
145
153
  repoRootOf,
146
154
  taskIsolationOf
147
- } from "./chunk-OYUSADFM.mjs";
155
+ } from "./chunk-NJGID6BG.mjs";
148
156
  import {
149
157
  DEFAULT_RUNNERS,
150
158
  addTaskToPlan,
@@ -176,7 +184,7 @@ import {
176
184
  updateTaskInPlan,
177
185
  waitsForApproval,
178
186
  waitsForReset
179
- } from "./chunk-SM6BV6XS.mjs";
187
+ } from "./chunk-RT4NQ2BZ.mjs";
180
188
 
181
189
  // src/models/TaskLog.ts
182
190
  var HEAD_LINES = 60;
@@ -278,6 +286,9 @@ function toTaskLogEvent(event) {
278
286
  };
279
287
  case "command_output":
280
288
  return { type: "command_output", text: event.text };
289
+ // Logged once the plan's write-back settles: see `TaskLogRecorder.note`.
290
+ case "model_switched":
291
+ case "model_drifted":
281
292
  case "background_agent":
282
293
  return null;
283
294
  }
@@ -4729,8 +4740,8 @@ function forms(text) {
4729
4740
  function sensitiveEnvValues(env) {
4730
4741
  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());
4731
4742
  }
4732
- function carriesSecret(texts, held) {
4733
- const secrets = [...held].map((s) => s.trim()).filter((s) => s.length >= MIN_SECRET_LENGTH);
4743
+ function carriesSecret(texts, held2) {
4744
+ const secrets = [...held2].map((s) => s.trim()).filter((s) => s.length >= MIN_SECRET_LENGTH);
4734
4745
  return texts.flatMap(forms).some((text) => secrets.some((secret) => text.includes(secret)) || TOKEN_SHAPES.some((shape) => shape.test(text)) || [...text.matchAll(SK_KEY)].some((m) => looksRandom(m[0].slice(3))));
4735
4746
  }
4736
4747
 
@@ -6406,7 +6417,7 @@ var CODEX_MANIFEST = {
6406
6417
  }
6407
6418
  },
6408
6419
  modelDiscovery: {
6409
- // Codex has no `codex models` subcommand (see docs/adr/0004). Discovery
6420
+ // Codex has no `codex models` subcommand (see ADR-0004). Discovery
6410
6421
  // tries three sources in order, each falling through to the next:
6411
6422
  //
6412
6423
  // 1. `codex app-server` stdio JSON-RPC (`initialize` → `model/list`) —
@@ -6477,6 +6488,10 @@ function parseOpenCodeModels(stdout, manifest) {
6477
6488
  }
6478
6489
  return models;
6479
6490
  }
6491
+ function contextLimit(limit) {
6492
+ const context = limit?.context;
6493
+ return typeof context === "number" && Number.isFinite(context) && context > 0 ? { contextWindow: context } : {};
6494
+ }
6480
6495
  function parseOpenCodeModelsVerbose(stdout, manifest) {
6481
6496
  const normalized2 = stdout.endsWith("\n") ? stdout : stdout + "\n";
6482
6497
  const parts = normalized2.split(/^([A-Za-z0-9._-]+\/[^\s"{}]+)\n/m);
@@ -6492,12 +6507,14 @@ function parseOpenCodeModelsVerbose(stdout, manifest) {
6492
6507
  const jsonText = parts[i + 1].trim();
6493
6508
  if (seen.has(fullId)) continue;
6494
6509
  let variantIds = [];
6510
+ let window = {};
6495
6511
  let modelLabel = prefLabels.get(fullId) || fullId.split("/").pop() || fullId;
6496
6512
  const runnerProvider = fullId.split("/")[0];
6497
6513
  try {
6498
6514
  const data = JSON.parse(jsonText);
6499
6515
  if (data.name) modelLabel = prefLabels.get(fullId) || data.name;
6500
6516
  variantIds = Object.keys(data.variants ?? {});
6517
+ window = contextLimit(data.limit);
6501
6518
  } catch {
6502
6519
  }
6503
6520
  seen.add(fullId);
@@ -6505,12 +6522,15 @@ function parseOpenCodeModelsVerbose(stdout, manifest) {
6505
6522
  modelId: fullId,
6506
6523
  modelLabel,
6507
6524
  runnerProvider,
6508
- variants: variantIds.map((id) => ({ id, label: id.charAt(0).toUpperCase() + id.slice(1) }))
6525
+ variants: variantIds.map((id) => ({ id, label: id.charAt(0).toUpperCase() + id.slice(1) })),
6526
+ ...window
6509
6527
  });
6510
6528
  }
6511
- if (models.length === 0) return [];
6512
- const prefOrder = preferred?.map((p) => p.id) ?? [];
6513
- models.sort((a, b) => {
6529
+ return preferredFirst(models, manifest);
6530
+ }
6531
+ function preferredFirst(models, manifest) {
6532
+ const prefOrder = manifest.modelDiscovery.preferredPatterns?.map((p) => p.id) ?? [];
6533
+ return [...models].sort((a, b) => {
6514
6534
  const ai = prefOrder.indexOf(a.modelId);
6515
6535
  const bi = prefOrder.indexOf(b.modelId);
6516
6536
  if (ai >= 0 && bi >= 0) return ai - bi;
@@ -6518,7 +6538,37 @@ function parseOpenCodeModelsVerbose(stdout, manifest) {
6518
6538
  if (bi >= 0) return 1;
6519
6539
  return a.modelId.localeCompare(b.modelId);
6520
6540
  });
6521
- return models;
6541
+ }
6542
+ function parseOpenCodeModelsApi(stdout, manifest) {
6543
+ let data;
6544
+ try {
6545
+ data = JSON.parse(stdout)?.data;
6546
+ } catch {
6547
+ return [];
6548
+ }
6549
+ if (!Array.isArray(data)) return [];
6550
+ const prefLabels = new Map((manifest.modelDiscovery.preferredPatterns ?? []).map((p) => [p.id, p.label]));
6551
+ const seen = /* @__PURE__ */ new Set();
6552
+ const models = [];
6553
+ for (const entry of data) {
6554
+ if (!entry || typeof entry.providerID !== "string" || typeof entry.modelID !== "string") continue;
6555
+ if (entry.enabled === false) continue;
6556
+ const fullId = `${entry.providerID}/${entry.modelID}`;
6557
+ if (seen.has(fullId)) continue;
6558
+ seen.add(fullId);
6559
+ const variantIds = Array.isArray(entry.variants) ? entry.variants.flatMap((v) => {
6560
+ const id = v?.id;
6561
+ return typeof id === "string" && id ? [id] : [];
6562
+ }) : [];
6563
+ models.push({
6564
+ modelId: fullId,
6565
+ modelLabel: prefLabels.get(fullId) || typeof entry.name === "string" && entry.name || entry.modelID,
6566
+ runnerProvider: entry.providerID,
6567
+ variants: variantIds.map((id) => ({ id, label: id.charAt(0).toUpperCase() + id.slice(1) })),
6568
+ ...contextLimit(entry.limit)
6569
+ });
6570
+ }
6571
+ return preferredFirst(models, manifest);
6522
6572
  }
6523
6573
 
6524
6574
  // src/plugins/builtin/opencode.manifest.ts
@@ -6534,13 +6584,17 @@ var OPENCODE_MANIFEST = {
6534
6584
  modelDiscovery: {
6535
6585
  method: "command",
6536
6586
  discoveryCommands: [
6587
+ // v1 prints per-model variants under --verbose; v2 rejects the flag (a
6588
+ // fast non-zero exit) and serves the same catalog, variants included, over
6589
+ // `api model.list`. v1 is asked first so it never reaches the `api` form.
6537
6590
  { command: "opencode", args: ["models", "--verbose"], parser: parseOpenCodeModelsVerbose },
6591
+ { command: "opencode", args: ["api", "model.list"], parser: parseOpenCodeModelsApi },
6538
6592
  { command: "opencode", args: ["models"], parser: parseOpenCodeModels }
6539
6593
  ],
6540
6594
  // No fallbackModels: the list must always reflect what `opencode models`
6541
6595
  // actually reports for THIS user (their configured providers only). A
6542
6596
  // hardcoded fallback previously leaked models the user didn't have.
6543
- // No static variants — per-model variants are fetched from opencode models --verbose
6597
+ // No static variants — per-model variants come from the first two commands above
6544
6598
  providerQualifiedIds: true
6545
6599
  },
6546
6600
  contextFile: "AGENTS.md",
@@ -8151,6 +8205,7 @@ var LOG_KINDS = {
8151
8205
  turn_start: true,
8152
8206
  command: true,
8153
8207
  command_output: true,
8208
+ model_switched: true,
8154
8209
  compaction: true,
8155
8210
  runner_loaded: true,
8156
8211
  turn_end: true,
@@ -8186,6 +8241,11 @@ ${s.content}` : ""}`).join("\n");
8186
8241
  return `${event.messageId ? `${event.messageId} ` : ""}/${event.name}${event.args ? ` ${event.args}` : ""}`;
8187
8242
  case "command_output":
8188
8243
  return event.text;
8244
+ case "model_switched": {
8245
+ const { plan } = event;
8246
+ const planned = plan ? `; plan: ${plan.model?.model ?? "no model"}${plan.effort ? ` (${plan.effort})` : ""}` : "";
8247
+ return `${event.from.model} \u2192 ${event.to.model}${planned}${event.kept ? `, kept: ${event.kept}` : ""}`;
8248
+ }
8189
8249
  case "compaction":
8190
8250
  return `${event.trigger}${event.preTokens !== void 0 && event.postTokens !== void 0 ? `, ${event.preTokens} \u2192 ${event.postTokens} tokens` : ""}`;
8191
8251
  case "runner_loaded":
@@ -9745,6 +9805,7 @@ function canUseToolAnswer({ input, suggestions }, decision) {
9745
9805
  ...decision.decision === "allowForTask" && suggestions.length > 0 ? { updatedPermissions: suggestions } : {}
9746
9806
  };
9747
9807
  }
9808
+ var MODEL_COMMAND = "model";
9748
9809
  var SUBAGENT_TOOLS = /* @__PURE__ */ new Set(["Agent", "Task"]);
9749
9810
  function usageRecord(usage, model, subagentId) {
9750
9811
  const record = {
@@ -9806,9 +9867,9 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
9806
9867
  /** The model answering the planner's current message, as its `message_start` named it. */
9807
9868
  plannerModel;
9808
9869
  /**
9809
- * The session's `total_cost_usd` as last reported. Undefined after a resume:
9810
- * the CLI restores the resumed session's running total, and what Ordewell
9811
- * already counted of it is not ours to know here.
9870
+ * The session's `total_cost_usd` as last counted. A resume starts from what
9871
+ * was counted of that session before, and is undefined without it: the CLI
9872
+ * restores the session's running total, which holds turns counted earlier.
9812
9873
  */
9813
9874
  reportedCostUsd = 0;
9814
9875
  reportedModelCostUsd = /* @__PURE__ */ new Map();
@@ -9849,6 +9910,19 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
9849
9910
  terminalOnly = /* @__PURE__ */ new Set();
9850
9911
  /** Whether this process's `runner_loaded` went out: its first `init` sends it. */
9851
9912
  loadedReported = false;
9913
+ /** The models `initialize` listed, which turn the ids the CLI reports back into the ones a plan holds. */
9914
+ listedModels = [];
9915
+ /** The id the task's main thread last reported answering on. */
9916
+ mainModel;
9917
+ /** What the plan should say the task runs: its spawn's model, then each switch followed. */
9918
+ followedModel;
9919
+ /**
9920
+ * Model commands sent through the chat whose switch has not been seen yet,
9921
+ * oldest first, each with the turns still to end before its own has. Several
9922
+ * can be pending: a command's switch is first reported by the next turn's
9923
+ * `init`, which can be another `/model`'s.
9924
+ */
9925
+ modelCommands = [];
9852
9926
  spawnSpec(opts, workspace) {
9853
9927
  if (opts.kind === "task") return this.taskSpawnSpec(opts);
9854
9928
  const args = [
@@ -9874,12 +9948,23 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
9874
9948
  if (opts.effort && opts.effort !== "adaptive") args.push("--effort", opts.effort);
9875
9949
  if (opts.resumeSessionId) {
9876
9950
  args.push("--resume", opts.resumeSessionId);
9877
- this.reportedCostUsd = void 0;
9951
+ this.countFrom(opts.resumeSessionId, opts.resumeCost);
9878
9952
  this.pendingResume = opts.resumeSessionId;
9879
9953
  }
9880
9954
  this.configDir = workspace.CLAUDE_CONFIG_DIR ?? process.env.CLAUDE_CONFIG_DIR ?? path13.join(os6.homedir(), ".claude");
9881
9955
  return { command: "claude", args, env: NO_AUTO_MEMORY };
9882
9956
  }
9957
+ sessionCost() {
9958
+ const sessionId = this.sessionId ?? this.pendingResume;
9959
+ if (!sessionId || this.reportedCostUsd === void 0) return null;
9960
+ return { sessionId, usd: this.reportedCostUsd, modelUsd: Object.fromEntries(this.reportedModelCostUsd) };
9961
+ }
9962
+ /** A count kept for another session says nothing of this one's. */
9963
+ countFrom(sessionId, counted) {
9964
+ const known = counted?.sessionId === sessionId ? counted : void 0;
9965
+ this.reportedCostUsd = known?.usd;
9966
+ this.reportedModelCostUsd = new Map(Object.entries(known?.modelUsd ?? {}));
9967
+ }
9883
9968
  savedOutput(abs) {
9884
9969
  return this.configDir !== null && this.sessionId !== null && inSavedOutput(abs, this.configDir, this.sessionId);
9885
9970
  }
@@ -9894,6 +9979,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
9894
9979
  */
9895
9980
  taskSpawnSpec(opts) {
9896
9981
  this.requestedMode = opts.flags.permissionMode;
9982
+ this.followedModel = opts.model;
9897
9983
  const args = [
9898
9984
  ...TASK_PROTOCOL_ARGS,
9899
9985
  "--permission-prompt-tool",
@@ -9907,7 +9993,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
9907
9993
  if (opts.model) args.push("--model", opts.model);
9908
9994
  if (opts.resumeSessionId) {
9909
9995
  args.push("--resume", opts.resumeSessionId);
9910
- this.reportedCostUsd = void 0;
9996
+ this.countFrom(opts.resumeSessionId, opts.resumeCost);
9911
9997
  this.pendingResume = opts.resumeSessionId;
9912
9998
  }
9913
9999
  if (opts.mcp) args.push("--append-system-prompt", TASK_COMPLETION_CONTRACT, ...this.ordewellServerArgs(opts.mcp, "task"));
@@ -9949,6 +10035,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
9949
10035
  if (opts.kind !== "task") return;
9950
10036
  const initialized = (await this.control({ subtype: "initialize" }, MODEL_PIN_TIMEOUT_MS))?.response;
9951
10037
  this.listedCommands = initialized?.commands ?? [];
10038
+ this.listedModels = initialized?.models ?? [];
9952
10039
  if (opts.flags.permissionMode !== "plan" || !opts.model) return;
9953
10040
  const resolved = initialized?.models?.find((m) => m.value === opts.model)?.resolvedModel;
9954
10041
  if (resolved && resolved !== opts.model) await this.control({ subtype: "set_model", model: resolved }, MODEL_PIN_TIMEOUT_MS);
@@ -10002,6 +10089,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
10002
10089
  if (this.role !== "task" || !this.process || this.interruptRequested || this.pendingResume) return false;
10003
10090
  const uuid = randomUUID();
10004
10091
  this.steers.set(uuid, id);
10092
+ this.noteModelCommand(text, 2);
10005
10093
  this.writeLine({ type: "user", uuid, message: { role: "user", content: [{ type: "text", text }] } });
10006
10094
  return true;
10007
10095
  }
@@ -10053,12 +10141,23 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
10053
10141
  }
10054
10142
  turnPayload(message) {
10055
10143
  this.openTurnState();
10144
+ this.noteModelCommand(message, 1);
10056
10145
  return `${JSON.stringify({
10057
10146
  type: "user",
10058
10147
  message: { role: "user", content: [{ type: "text", text: message }] }
10059
10148
  })}
10060
10149
  `;
10061
10150
  }
10151
+ /**
10152
+ * The CLI runs a message that starts with a slash command as that command,
10153
+ * whether the surface sent it as one or as plain text, so the message
10154
+ * itself says a model switch was asked for: every way into the chat counts.
10155
+ */
10156
+ noteModelCommand(text, turnsLeft) {
10157
+ const [, name, args = ""] = /^\/(\S+)\s*([\s\S]*)$/.exec(text) ?? [];
10158
+ if (this.role !== "task" || !name || (findRunnerCommand(this.commands(), name)?.name ?? name) !== MODEL_COMMAND) return;
10159
+ this.modelCommands.push({ args: args.trim(), turnsLeft });
10160
+ }
10062
10161
  openTurnState() {
10063
10162
  this.turnHasText = false;
10064
10163
  this.interruptRequested = false;
@@ -10170,6 +10269,7 @@ ${block.text}` : block.text });
10170
10269
  this.clearFollowOnTimer();
10171
10270
  if (this.refuseOtherMode(msg, emit)) return;
10172
10271
  this.readLoaded(msg, emit);
10272
+ this.followModel(msg.model, emit);
10173
10273
  }
10174
10274
  if (msg.subtype === "commands_changed" && Array.isArray(msg.commands)) this.listedCommands = msg.commands;
10175
10275
  if (msg.subtype === "compact_boundary") {
@@ -10192,6 +10292,7 @@ ${block.text}` : block.text });
10192
10292
  return;
10193
10293
  case "result":
10194
10294
  this.reportSessionCost(msg, emit);
10295
+ for (const command of this.modelCommands) command.turnsLeft = Math.max(0, command.turnsLeft - 1);
10195
10296
  if (this.interruptRequested && (msg.is_error || msg.subtype !== "success")) {
10196
10297
  this.interruptRequested = false;
10197
10298
  this.closeTurn({ type: "turn_end", interrupted: true }, emit);
@@ -10211,6 +10312,55 @@ ${block.text}` : block.text });
10211
10312
  return;
10212
10313
  }
10213
10314
  }
10315
+ /**
10316
+ * A task's main thread reported the model it answers on. A change is the
10317
+ * user's own switch when a model command went out before it: Ordewell never
10318
+ * passes `--fallback-model`, so nothing else should move it. A change with
10319
+ * no command before it is reported as drift, and the plan does not follow
10320
+ * it. A command whose turn ended with the model unchanged switched nothing,
10321
+ * and is let go.
10322
+ *
10323
+ * Effort is never reported (recorded on 2.1.302, `task-effort-command`):
10324
+ * `init` names the model alone, its `per_turn_effort_active` says effort is
10325
+ * on, not which; `/effort low` only prints that it set it; and `/model`
10326
+ * takes no effort (`/model haiku high` is "not found"). So a switch carries
10327
+ * no effort and the plan's stays as planned.
10328
+ */
10329
+ followModel(reported, emit) {
10330
+ if (this.role !== "task" || !reported || reported === "<synthetic>") return;
10331
+ const previous = this.mainModel;
10332
+ this.mainModel = reported;
10333
+ if (previous === void 0 || previous === reported) {
10334
+ this.modelCommands = this.modelCommands.filter((c) => c.turnsLeft > 0);
10335
+ return;
10336
+ }
10337
+ const matched = this.modelCommands.findIndex((c) => this.runsAs(c.args, reported));
10338
+ const command = this.modelCommands[Math.max(matched, 0)];
10339
+ this.modelCommands = this.modelCommands.slice(Math.max(matched, 0) + 1);
10340
+ const from = this.listedModel(previous, this.followedModel);
10341
+ if (!command) {
10342
+ emit({ type: "model_drifted", from, to: this.listedModel(reported, void 0) });
10343
+ return;
10344
+ }
10345
+ const to = this.listedModel(reported, command.args);
10346
+ this.followedModel = to.model;
10347
+ emit({ type: "model_switched", from, to });
10348
+ }
10349
+ /** Whether the model value `asked` runs as the id `resolved`, by what `initialize` lists. */
10350
+ runsAs(asked, resolved) {
10351
+ return asked === resolved || this.listedModels.some((m) => m.value === asked && m.resolvedModel === resolved);
10352
+ }
10353
+ /**
10354
+ * The value `initialize` lists for the id a model runs as. Several can share
10355
+ * one (`sonnet`, `default`); the one asked for wins, and `default` only as a
10356
+ * last resort, since it names whatever the CLI's default becomes.
10357
+ */
10358
+ listedModel(resolved, asked) {
10359
+ const listed3 = this.listedModels.filter((m) => m.value && (m.resolvedModel === resolved || m.value === resolved));
10360
+ const pick = listed3.find((m) => m.value === asked && asked !== "default") ?? listed3.find((m) => m.value !== "default") ?? listed3[0];
10361
+ if (!pick?.value) return { model: resolved };
10362
+ return { model: pick.value, ...pick.displayName ? { label: pick.displayName } : {} };
10363
+ }
10214
10364
  /**
10215
10365
  * `--permission-mode auto` on a model or account without auto mode is not
10216
10366
  * refused: the CLI says nothing and starts in `default`, then asks about every
@@ -10294,8 +10444,10 @@ ${block.text}` : block.text });
10294
10444
  * {@link AgentEvent}), so nothing here has to reconcile with it.
10295
10445
  */
10296
10446
  handleStreamEvent(event, emit) {
10297
- if (event.type === "message_start") this.plannerModel = event.message?.model;
10298
- else if (event.type === "message_delta" && event.usage) emit({ type: "usage", record: usageRecord(event.usage, this.plannerModel) });
10447
+ if (event.type === "message_start") {
10448
+ this.plannerModel = event.message?.model;
10449
+ this.followModel(this.plannerModel, emit);
10450
+ } else if (event.type === "message_delta" && event.usage) emit({ type: "usage", record: usageRecord(event.usage, this.plannerModel) });
10299
10451
  else if (event.type === "content_block_start" && event.content_block?.type === "text") {
10300
10452
  this.pendingBreak = this.turnHasText;
10301
10453
  } else if (event.type === "content_block_delta" && event.delta?.type === "text_delta" && event.delta.text) {
@@ -10310,7 +10462,8 @@ ${block.text}` : block.text });
10310
10462
  * What only the result line knows: the cost, which covers every call the
10311
10463
  * session made — subagents included, since none of their lines carries one —
10312
10464
  * and the planner model's window. `total_cost_usd` is a running total, so a
10313
- * turn reports its growth. The first turn after a resume only sets the
10465
+ * turn reports its growth. After a resume it grows from what was counted of
10466
+ * the session before; with no count kept, the first turn only sets the
10314
10467
  * baseline: its total includes turns counted before, and a turn's own share
10315
10468
  * cannot be told apart from them.
10316
10469
  */
@@ -10890,6 +11043,97 @@ var CodexRpc = class {
10890
11043
  }
10891
11044
  };
10892
11045
 
11046
+ // src/utils/wallTime.ts
11047
+ var MONTHS = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
11048
+ function lastMatch(re, text) {
11049
+ let last = null;
11050
+ for (const m of text.matchAll(re)) last = m;
11051
+ return last;
11052
+ }
11053
+ function hour24(hour, meridiem) {
11054
+ const h = Number(hour) % 12;
11055
+ return meridiem.toLowerCase() === "pm" ? h + 12 : h;
11056
+ }
11057
+ function month(name) {
11058
+ if (!name) return void 0;
11059
+ const at = MONTHS.indexOf(name.toLowerCase());
11060
+ return at < 0 ? void 0 : at;
11061
+ }
11062
+ function placeWallTime(wall, zone, now) {
11063
+ const today = wallFields(now, zone);
11064
+ const year = wall.year ?? today.year;
11065
+ const monthIndex = wall.month ?? today.month;
11066
+ const day = wall.day ?? today.day;
11067
+ const at = fromWall(year, monthIndex, day, wall.hour, wall.minute, zone);
11068
+ if (wall.day !== void 0 || at.getTime() >= now.getTime() - HOUR_MS) return at;
11069
+ return fromWall(year, monthIndex, day + 1, wall.hour, wall.minute, zone);
11070
+ }
11071
+ var HOUR_MS = 60 * 6e4;
11072
+ function wallFields(instant, zone) {
11073
+ if (!zone) {
11074
+ return { year: instant.getFullYear(), month: instant.getMonth(), day: instant.getDate(), hour: instant.getHours(), minute: instant.getMinutes(), second: instant.getSeconds() };
11075
+ }
11076
+ const parts = new Intl.DateTimeFormat("en-US", {
11077
+ timeZone: zone,
11078
+ hourCycle: "h23",
11079
+ year: "numeric",
11080
+ month: "numeric",
11081
+ day: "numeric",
11082
+ hour: "numeric",
11083
+ minute: "numeric",
11084
+ second: "numeric"
11085
+ }).formatToParts(instant);
11086
+ const field = (type) => Number(parts.find((p) => p.type === type)?.value ?? 0);
11087
+ return { year: field("year"), month: field("month") - 1, day: field("day"), hour: field("hour"), minute: field("minute"), second: field("second") };
11088
+ }
11089
+ function zoneOffsetMs(instant, zone) {
11090
+ const f = wallFields(new Date(instant), zone);
11091
+ return Date.UTC(f.year, f.month, f.day, f.hour, f.minute, f.second) - Math.floor(instant / 1e3) * 1e3;
11092
+ }
11093
+ function fromWall(year, monthIndex, day, hour, minute, zone) {
11094
+ if (!zone) return new Date(year, monthIndex, day, hour, minute);
11095
+ const asUtc = Date.UTC(year, monthIndex, day, hour, minute);
11096
+ let instant = asUtc - zoneOffsetMs(asUtc, zone);
11097
+ instant = asUtc - zoneOffsetMs(instant, zone);
11098
+ return new Date(instant);
11099
+ }
11100
+
11101
+ // src/services/harness/codexUsageLimit.ts
11102
+ 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;
11103
+ function codexUsageLimitReset(text, now) {
11104
+ const m = lastMatch(CODEX_RESET_RE, text);
11105
+ if (!m) return null;
11106
+ const [, mon, day, year, hour, minute, meridiem] = m;
11107
+ const wall = { hour: hour24(hour, meridiem), minute: Number(minute) };
11108
+ if (mon) {
11109
+ const at = month(mon);
11110
+ if (at === void 0) return null;
11111
+ Object.assign(wall, { month: at, day: Number(day), year: Number(year) });
11112
+ }
11113
+ return placeWallTime(wall, void 0, now);
11114
+ }
11115
+ function mergeCodexRateLimits(known, params) {
11116
+ const snapshot = params?.rateLimits;
11117
+ if (!snapshot || typeof snapshot !== "object") return known;
11118
+ const id = snapshot.limitId ?? "";
11119
+ const before = known.get(id);
11120
+ const next = new Map(known);
11121
+ next.set(id, { limitId: id, primary: snapshot.primary ?? before?.primary, secondary: snapshot.secondary ?? before?.secondary });
11122
+ return next;
11123
+ }
11124
+ function isCodexUsageLimit(error) {
11125
+ const info = error?.codexErrorInfo;
11126
+ if (info === void 0 || info === null || info === "other") return void 0;
11127
+ if (info === "usageLimitExceeded") return true;
11128
+ return typeof info === "object" && "usageLimitExceeded" in info;
11129
+ }
11130
+ function codexUsageLimitSignal(error, limits, now) {
11131
+ const limited = isCodexUsageLimit(error);
11132
+ if (limited !== true) return limited;
11133
+ 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());
11134
+ return resets.length > 0 ? { resetsAt: new Date(Math.max(...resets)).toISOString() } : {};
11135
+ }
11136
+
10893
11137
  // src/services/harness/CodexAdapter.ts
10894
11138
  var HANDSHAKE_TIMEOUT_MS = 3e4;
10895
11139
  var MCP_ATTACH_TIMEOUT_MS2 = 1e4;
@@ -11033,6 +11277,11 @@ var CodexAdapter = class extends StdioAgentAdapter {
11033
11277
  turn = null;
11034
11278
  /** The last turn this adapter ended — so its second end signal, landing late, ends nothing. */
11035
11279
  endedTurnId = null;
11280
+ /**
11281
+ * The account's limit windows as Codex last reported them. Only read when a
11282
+ * turn fails on the usage limit; an update on its own acts on nothing.
11283
+ */
11284
+ rateLimits = /* @__PURE__ */ new Map();
11036
11285
  /** An interrupt was asked for during this turn, so however it ends, it was cut short. */
11037
11286
  interruptRequested = false;
11038
11287
  /**
@@ -11428,6 +11677,9 @@ ${this.exitMessage()}`);
11428
11677
  case "thread/tokenUsage/updated":
11429
11678
  this.emitUsage(msg.params, emit);
11430
11679
  return;
11680
+ case "account/rateLimits/updated":
11681
+ this.rateLimits = mergeCodexRateLimits(this.rateLimits, msg.params);
11682
+ return;
11431
11683
  case "turn/started": {
11432
11684
  if (this.subagentOf(msg.params?.threadId)) return;
11433
11685
  this.turnHasText = false;
@@ -11468,7 +11720,7 @@ ${this.exitMessage()}`);
11468
11720
  const failure = msg.params;
11469
11721
  if (failure?.willRetry) return;
11470
11722
  this.closeTurn(emit);
11471
- emit({ type: "error", message: failure?.error?.message || "Codex ended the turn with an error." });
11723
+ emit(this.turnFailed(failure?.error));
11472
11724
  return;
11473
11725
  }
11474
11726
  case "turn/completed": {
@@ -11479,7 +11731,7 @@ ${this.exitMessage()}`);
11479
11731
  if (this.interruptRequested || turn?.status === "interrupted") {
11480
11732
  emit({ type: "turn_end", interrupted: true });
11481
11733
  } else if (turn?.status === "failed") {
11482
- emit({ type: "error", message: turn.error?.message || "Codex ended the turn with an error." });
11734
+ emit(this.turnFailed(turn.error));
11483
11735
  } else {
11484
11736
  emit({ type: "turn_end" });
11485
11737
  }
@@ -11491,6 +11743,11 @@ ${this.exitMessage()}`);
11491
11743
  return;
11492
11744
  }
11493
11745
  }
11746
+ turnFailed(error) {
11747
+ const message = error?.message || "Codex ended the turn with an error.";
11748
+ const usageLimit = codexUsageLimitSignal(error, this.rateLimits, /* @__PURE__ */ new Date());
11749
+ return usageLimit === void 0 ? { type: "error", message } : { type: "error", message, usageLimit };
11750
+ }
11494
11751
  turnStarted(id) {
11495
11752
  const turn = this.turn;
11496
11753
  if (!turn || turn.id || typeof id !== "string") return;
@@ -11948,9 +12205,9 @@ function recordUnusable(servers, approval) {
11948
12205
  for (const server4 of servers) approval?.record?.(startRequest(server4), false, "unsupported");
11949
12206
  }
11950
12207
  var NOT_APPROVED = "not approved for planning";
11951
- function heldServersNote(held, unreadable = []) {
12208
+ function heldServersNote(held2, unreadable = []) {
11952
12209
  const lines = [
11953
- ...held.map(({ server: server4, reason }) => `- ${server4.name} (${server4.source}): ${reason}`),
12210
+ ...held2.map(({ server: server4, reason }) => `- ${server4.name} (${server4.source}): ${reason}`),
11954
12211
  ...unreadable.map((file) => `- ${file.source}: Ordewell could not read it (${file.reason})`)
11955
12212
  ];
11956
12213
  if (lines.length === 0) return "";
@@ -12087,8 +12344,8 @@ var PendingSteers = class {
12087
12344
  * False when it was already delivered, so a late failure does not recall it.
12088
12345
  */
12089
12346
  forget(id) {
12090
- for (const [messageId, held] of this.pending) {
12091
- if (held !== id) continue;
12347
+ for (const [messageId, held2] of this.pending) {
12348
+ if (held2 !== id) continue;
12092
12349
  this.pending.delete(messageId);
12093
12350
  return true;
12094
12351
  }
@@ -12166,9 +12423,9 @@ var ChildSessions = class {
12166
12423
  adopt(child, callId) {
12167
12424
  if (this.owners.get(child)) return null;
12168
12425
  this.owners.set(child, callId);
12169
- const held = this.held.get(child) ?? [];
12426
+ const held2 = this.held.get(child) ?? [];
12170
12427
  this.held.delete(child);
12171
- return held;
12428
+ return held2;
12172
12429
  }
12173
12430
  owns(callId) {
12174
12431
  return [...this.owners.values()].includes(callId);
@@ -12692,11 +12949,11 @@ var OpenCodeV2 = class {
12692
12949
  adoptChild(data, state, onEvent, turn) {
12693
12950
  const child = data.metadata?.sessionID;
12694
12951
  const callId = data.id;
12695
- const held = child && callId ? state.children.adopt(child, callId) : null;
12696
- if (!held || !callId) return;
12952
+ const held2 = child && callId ? state.children.adopt(child, callId) : null;
12953
+ if (!held2 || !callId) return;
12697
12954
  const model = state.stepModels.get(data.assistantMessageID ?? "");
12698
12955
  onEvent({ type: "subagent_started", subagentId: callId, brief: state.briefs.get(callId) ?? "", ...model ? { model } : {} });
12699
- for (const frame of held) this.onFrame(frame, state, turn, onEvent);
12956
+ for (const frame of held2) this.onFrame(frame, state, turn, onEvent);
12700
12957
  }
12701
12958
  finishChild(callId, success, output, state, onEvent) {
12702
12959
  if (!state.children.owns(callId)) return;
@@ -13408,13 +13665,13 @@ var OpenCodeAdapter = class {
13408
13665
  trackSubagent(part, callId, turn, onEvent) {
13409
13666
  const state = part.state;
13410
13667
  const child = state?.metadata?.sessionId;
13411
- const held = child ? turn.children.adopt(child, callId) : null;
13412
- if (held) {
13668
+ const held2 = child ? turn.children.adopt(child, callId) : null;
13669
+ if (held2) {
13413
13670
  const input = state?.input ?? {};
13414
13671
  const brief2 = typeof input.description === "string" ? input.description : typeof input.prompt === "string" ? input.prompt : "";
13415
13672
  const model = flatModelId(state?.metadata?.model?.providerID, state?.metadata?.model?.modelID);
13416
13673
  onEvent({ type: "subagent_started", subagentId: callId, brief: brief2, ...model ? { model } : {} });
13417
- for (const frame of held) this.onFrame(frame, turn, onEvent);
13674
+ for (const frame of held2) this.onFrame(frame, turn, onEvent);
13418
13675
  }
13419
13676
  const status = state?.status;
13420
13677
  if (status === "completed" || status === "error") {
@@ -13644,61 +13901,6 @@ var OPENCODE_REPO_CONFIG = {
13644
13901
  }
13645
13902
  };
13646
13903
 
13647
- // src/utils/wallTime.ts
13648
- var MONTHS = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
13649
- function lastMatch(re, text) {
13650
- let last = null;
13651
- for (const m of text.matchAll(re)) last = m;
13652
- return last;
13653
- }
13654
- function hour24(hour, meridiem) {
13655
- const h = Number(hour) % 12;
13656
- return meridiem.toLowerCase() === "pm" ? h + 12 : h;
13657
- }
13658
- function month(name) {
13659
- if (!name) return void 0;
13660
- const at = MONTHS.indexOf(name.toLowerCase());
13661
- return at < 0 ? void 0 : at;
13662
- }
13663
- function placeWallTime(wall, zone, now) {
13664
- const today = wallFields(now, zone);
13665
- const year = wall.year ?? today.year;
13666
- const monthIndex = wall.month ?? today.month;
13667
- const day = wall.day ?? today.day;
13668
- const at = fromWall(year, monthIndex, day, wall.hour, wall.minute, zone);
13669
- if (wall.day !== void 0 || at.getTime() >= now.getTime() - HOUR_MS) return at;
13670
- return fromWall(year, monthIndex, day + 1, wall.hour, wall.minute, zone);
13671
- }
13672
- var HOUR_MS = 60 * 6e4;
13673
- function wallFields(instant, zone) {
13674
- if (!zone) {
13675
- return { year: instant.getFullYear(), month: instant.getMonth(), day: instant.getDate(), hour: instant.getHours(), minute: instant.getMinutes(), second: instant.getSeconds() };
13676
- }
13677
- const parts = new Intl.DateTimeFormat("en-US", {
13678
- timeZone: zone,
13679
- hourCycle: "h23",
13680
- year: "numeric",
13681
- month: "numeric",
13682
- day: "numeric",
13683
- hour: "numeric",
13684
- minute: "numeric",
13685
- second: "numeric"
13686
- }).formatToParts(instant);
13687
- const field = (type) => Number(parts.find((p) => p.type === type)?.value ?? 0);
13688
- return { year: field("year"), month: field("month") - 1, day: field("day"), hour: field("hour"), minute: field("minute"), second: field("second") };
13689
- }
13690
- function zoneOffsetMs(instant, zone) {
13691
- const f = wallFields(new Date(instant), zone);
13692
- return Date.UTC(f.year, f.month, f.day, f.hour, f.minute, f.second) - Math.floor(instant / 1e3) * 1e3;
13693
- }
13694
- function fromWall(year, monthIndex, day, hour, minute, zone) {
13695
- if (!zone) return new Date(year, monthIndex, day, hour, minute);
13696
- const asUtc = Date.UTC(year, monthIndex, day, hour, minute);
13697
- let instant = asUtc - zoneOffsetMs(asUtc, zone);
13698
- instant = asUtc - zoneOffsetMs(instant, zone);
13699
- return new Date(instant);
13700
- }
13701
-
13702
13904
  // src/services/harness/claudeUsageLimit.ts
13703
13905
  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;
13704
13906
  function claudeUsageLimitReset(text, now) {
@@ -13714,21 +13916,6 @@ function claudeUsageLimitReset(text, now) {
13714
13916
  return placeWallTime(wall, zone, now);
13715
13917
  }
13716
13918
 
13717
- // src/services/harness/codexUsageLimit.ts
13718
- 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;
13719
- function codexUsageLimitReset(text, now) {
13720
- const m = lastMatch(CODEX_RESET_RE, text);
13721
- if (!m) return null;
13722
- const [, mon, day, year, hour, minute, meridiem] = m;
13723
- const wall = { hour: hour24(hour, meridiem), minute: Number(minute) };
13724
- if (mon) {
13725
- const at = month(mon);
13726
- if (at === void 0) return null;
13727
- Object.assign(wall, { month: at, day: Number(day), year: Number(year) });
13728
- }
13729
- return placeWallTime(wall, void 0, now);
13730
- }
13731
-
13732
13919
  // src/services/harness/connectors.ts
13733
13920
  var CONNECTORS = {
13734
13921
  "claude-code": { create: (deps) => new ClaudeCodeAdapter(deps), ordewellTools: CLAUDE_ORDEWELL, repoConfig: CLAUDE_REPO_CONFIG, usageLimitReset: claudeUsageLimitReset },
@@ -14059,6 +14246,7 @@ function parseCodexModelsCache(content) {
14059
14246
  }
14060
14247
  }
14061
14248
  var GEMINI_MODELS_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
14249
+ var NEGATIVE_TTL_MS = 6e4;
14062
14250
  function geminiDiscoveryCacheKey(apiKey) {
14063
14251
  const dir = globalDataDir();
14064
14252
  const hash = apiKey.slice(-8);
@@ -14126,7 +14314,8 @@ async function discoverGeminiModels(apiKey, baseUrl, fetchImpl) {
14126
14314
  modelId,
14127
14315
  modelLabel: m.displayName || modelId,
14128
14316
  runnerProvider: "gemini",
14129
- variants: []
14317
+ variants: [],
14318
+ ...typeof m.inputTokenLimit === "number" && m.inputTokenLimit > 0 ? { contextWindow: m.inputTokenLimit } : {}
14130
14319
  });
14131
14320
  }
14132
14321
  }
@@ -14188,26 +14377,33 @@ async function discoverFromApi(manifest, config, fetchImpl) {
14188
14377
  return null;
14189
14378
  }
14190
14379
  }
14380
+ function wasKilled(err) {
14381
+ return !!err && typeof err === "object" && err.killed === true;
14382
+ }
14191
14383
  async function discoverFromCommand(manifest, execImpl) {
14192
14384
  const discovery = manifest.modelDiscovery;
14193
14385
  const commands = discovery.discoveryCommands ?? [
14194
14386
  { command: discovery.command, args: discovery.args || [], parser: discovery.parser }
14195
14387
  ];
14196
14388
  const runCommands = async () => {
14389
+ let timedOut2 = false;
14197
14390
  for (const cmd of commands) {
14198
14391
  try {
14199
14392
  const { stdout } = await execImpl(`${cmd.command} ${cmd.args.join(" ")}`, { timeout: 45e3 });
14200
14393
  const models = parseModelOutput(stdout, cmd.parser, manifest);
14201
- if (models.length > 0) return applyVariants(mergeCanonicalAliases(models, manifest), manifest);
14202
- } catch {
14394
+ if (models.length > 0) return { models: applyVariants(mergeCanonicalAliases(models, manifest), manifest), timedOut: timedOut2 };
14395
+ } catch (err) {
14396
+ if (wasKilled(err)) timedOut2 = true;
14203
14397
  }
14204
14398
  }
14205
- return null;
14399
+ return { models: null, timedOut: timedOut2 };
14206
14400
  };
14207
14401
  const first = await runCommands();
14208
- if (first) return { models: first, fromFallback: false };
14209
- const second = await runCommands();
14210
- if (second) return { models: second, fromFallback: false };
14402
+ if (first.models) return { models: first.models, fromFallback: false };
14403
+ if (first.timedOut) {
14404
+ const second = await runCommands();
14405
+ if (second.models) return { models: second.models, fromFallback: false };
14406
+ }
14211
14407
  return { models: applyVariants(buildFallbackModels(manifest), manifest), fromFallback: true };
14212
14408
  }
14213
14409
  function mergeCanonicalAliases(discovered, manifest) {
@@ -14244,16 +14440,33 @@ function parseLineByLine(stdout) {
14244
14440
  }
14245
14441
  var ModelDiscovery = class {
14246
14442
  cache = /* @__PURE__ */ new Map();
14443
+ /**
14444
+ * Degraded results (a fallback list, or empty) remembered for
14445
+ * {@link NEGATIVE_TTL_MS}. They are deliberately not cached — a transient
14446
+ * failure must heal — but re-running a failing CLI on every lookup is the
14447
+ * other extreme.
14448
+ */
14449
+ misses = /* @__PURE__ */ new Map();
14450
+ /**
14451
+ * Discoveries still running, so a second lookup of the same runner joins the
14452
+ * first instead of spawning the CLI again. A slow CLI is the case this
14453
+ * exists for: a planner switch and a runner-list refresh land inside one
14454
+ * another's wait, and two `opencode models` calls race each other for the
14455
+ * same background service.
14456
+ */
14457
+ inflight = /* @__PURE__ */ new Map();
14247
14458
  registry;
14248
14459
  execImpl;
14249
14460
  fetchImpl;
14250
14461
  appServerClient;
14251
14462
  readFileImpl;
14252
- constructor(registry, execImpl = defaultExec2, fetchImpl, appServerClient = defaultAppServerClient, readFileImpl) {
14463
+ now;
14464
+ constructor(registry, execImpl = defaultExec2, fetchImpl, appServerClient = defaultAppServerClient, readFileImpl, now = Date.now) {
14253
14465
  this.registry = registry;
14254
14466
  this.execImpl = execImpl;
14255
14467
  this.fetchImpl = fetchImpl;
14256
14468
  this.appServerClient = appServerClient;
14469
+ this.now = now;
14257
14470
  this.readFileImpl = readFileImpl ?? ((filePath) => {
14258
14471
  try {
14259
14472
  return fsSync3.readFileSync(filePath.replace(/^~/, osMod.homedir()), "utf8");
@@ -14297,10 +14510,20 @@ var ModelDiscovery = class {
14297
14510
  const runnerLabel = manifest.displayName || runner;
14298
14511
  return models.map((m) => ({ ...m, runnerId: runner, runnerLabel }));
14299
14512
  }
14300
- async discoverRaw(runner) {
14301
- if (this.cache.has(runner)) {
14302
- return this.cache.get(runner);
14303
- }
14513
+ discoverRaw(runner) {
14514
+ const cached2 = this.cache.get(runner);
14515
+ if (cached2) return Promise.resolve(cached2);
14516
+ const remembered = this.misses.get(runner);
14517
+ if (remembered && this.now() - remembered.at < NEGATIVE_TTL_MS) return Promise.resolve(remembered.models);
14518
+ const running = this.inflight.get(runner);
14519
+ if (running) return running;
14520
+ const started = this.discoverUncached(runner).finally(() => {
14521
+ if (this.inflight.get(runner) === started) this.inflight.delete(runner);
14522
+ });
14523
+ this.inflight.set(runner, started);
14524
+ return started;
14525
+ }
14526
+ async discoverUncached(runner) {
14304
14527
  const manifest = this.registry.getManifest(runner);
14305
14528
  if (!manifest) {
14306
14529
  return [];
@@ -14332,10 +14555,13 @@ var ModelDiscovery = class {
14332
14555
  models = buildFallbackModels(manifest);
14333
14556
  }
14334
14557
  if (cacheable && models.length > 0) this.cache.set(runner, models);
14558
+ else this.misses.set(runner, { models, at: this.now() });
14335
14559
  return models;
14336
14560
  }
14337
14561
  clear() {
14338
14562
  this.cache.clear();
14563
+ this.misses.clear();
14564
+ this.inflight.clear();
14339
14565
  }
14340
14566
  /** Whatever `discover()` has already cached for `runner` — never triggers discovery. */
14341
14567
  getCached(runner) {
@@ -14497,6 +14723,7 @@ function parseMaxParallel(value) {
14497
14723
  // src/services/SettingsService.ts
14498
14724
  import * as fs10 from "fs";
14499
14725
  import * as path25 from "path";
14726
+ import { randomUUID as randomUUID3 } from "crypto";
14500
14727
 
14501
14728
  // src/services/LoopGuards.ts
14502
14729
  var DEFAULT_SUPERVISOR_RETRY_LIMIT = 2;
@@ -14763,23 +14990,34 @@ function createWakePolicy(options) {
14763
14990
  // src/services/runnerExit.ts
14764
14991
  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;
14765
14992
  var USAGE_LIMIT_TAIL = 4096;
14766
- function classifyRunnerStop(output) {
14767
- return USAGE_LIMIT_RE.test(output.slice(-USAGE_LIMIT_TAIL)) ? "usage-limit" : "stopped";
14993
+ function classifyRunnerStop(said2) {
14994
+ if (typeof said2 !== "string" && said2.usageLimit !== void 0) return said2.usageLimit ? "usage-limit" : "stopped";
14995
+ return USAGE_LIMIT_RE.test(wordsOf(said2).slice(-USAGE_LIMIT_TAIL)) ? "usage-limit" : "stopped";
14996
+ }
14997
+ function wordsOf(said2) {
14998
+ return typeof said2 === "string" ? said2 : said2.message;
14768
14999
  }
14769
15000
  function stopsRunner(reason) {
14770
15001
  return reason !== "stop" && reason !== "load";
14771
15002
  }
14772
- function readUsageLimitReset(runner, output, now = /* @__PURE__ */ new Date()) {
15003
+ function readUsageLimitReset(runner, said2, now = /* @__PURE__ */ new Date()) {
15004
+ const reset = (typeof said2 === "string" ? null : reportedReset(said2)) ?? readReset(runner, wordsOf(said2), now);
15005
+ if (!reset || reset.getTime() < now.getTime() - DAY_MS) return null;
15006
+ return reset;
15007
+ }
15008
+ function reportedReset(failure) {
15009
+ const at = failure.usageLimit ? failure.usageLimit.resetsAt : void 0;
15010
+ const reset = at === void 0 ? null : new Date(at);
15011
+ return reset && !Number.isNaN(reset.getTime()) ? reset : null;
15012
+ }
15013
+ function readReset(runner, output, now) {
14773
15014
  const read2 = connectorFor(runner)?.usageLimitReset;
14774
15015
  if (!read2) return null;
14775
- let reset;
14776
15016
  try {
14777
- reset = read2(output.slice(-USAGE_LIMIT_TAIL), now);
15017
+ return read2(output.slice(-USAGE_LIMIT_TAIL), now);
14778
15018
  } catch {
14779
15019
  return null;
14780
15020
  }
14781
- if (!reset || reset.getTime() < now.getTime() - DAY_MS) return null;
14782
- return reset;
14783
15021
  }
14784
15022
  var DAY_MS = 24 * 60 * 6e4;
14785
15023
 
@@ -14883,15 +15121,15 @@ var ReplySplitter = class {
14883
15121
  push(segmentId, text) {
14884
15122
  const state = this.segments.get(segmentId);
14885
15123
  if (state === "text" || state === "plan") return { route: state, text };
14886
- const held = (state?.held ?? "") + text;
14887
- const opensWithObject = opensWithJsonObject(held);
15124
+ const held2 = (state?.held ?? "") + text;
15125
+ const opensWithObject = opensWithJsonObject(held2);
14888
15126
  if (opensWithObject === void 0) {
14889
- this.segments.set(segmentId, { held });
15127
+ this.segments.set(segmentId, { held: held2 });
14890
15128
  return null;
14891
15129
  }
14892
15130
  const route = opensWithObject ? "plan" : "text";
14893
15131
  this.segments.set(segmentId, route);
14894
- return { route, text: held };
15132
+ return { route, text: held2 };
14895
15133
  }
14896
15134
  };
14897
15135
  var TurnStream = class {
@@ -15046,6 +15284,7 @@ function plannerTranscript(history) {
15046
15284
  const loadsOf = /* @__PURE__ */ new Map();
15047
15285
  let owner = -1;
15048
15286
  for (const entry of history) {
15287
+ if (entry.kind === "error") continue;
15049
15288
  if (entry.kind === "skill_load" && entry.skill) {
15050
15289
  const loads = entry.skill.invokedBy === "user" ? loadsOf.get(owner) : void 0;
15051
15290
  if (loads) loads.push(entry.skill);
@@ -16383,6 +16622,45 @@ function markOf(was, now) {
16383
16622
  return editedFields(was, now).length > 0 || was.status !== now.status ? "changed" : void 0;
16384
16623
  }
16385
16624
 
16625
+ // src/services/RunnerChange.ts
16626
+ var sameSet = (a, b) => a === void 0 || b === void 0 ? a === b : a.length === b.length && a.every((id) => b.includes(id));
16627
+ function scopeChange(told, now) {
16628
+ const switchedOn = now.enabledRunners.filter((r) => !told.enabledRunners.includes(r));
16629
+ const switchedOff = told.enabledRunners.filter((r) => !now.enabledRunners.includes(r));
16630
+ const models = now.models;
16631
+ const allowlists = [.../* @__PURE__ */ new Set([...Object.keys(told.allowlist), ...Object.keys(now.allowlist)])].map((runner) => ({
16632
+ runner,
16633
+ before: effectiveAllowlist(told.allowlist[runner], runner, models),
16634
+ after: effectiveAllowlist(now.allowlist[runner], runner, models)
16635
+ })).filter((c) => !sameSet(c.before, c.after));
16636
+ return switchedOn.length || switchedOff.length || allowlists.length ? { switchedOn, switchedOff, allowlists } : null;
16637
+ }
16638
+ function flaggedTasks(rows, scope) {
16639
+ return rows.flatMap(({ ref, task }) => {
16640
+ const issue = assignmentIssue(task, scope);
16641
+ return issue ? [{ ref, title: task.title, task, text: assignmentIssueText(issue), status: task.status, awaitingReason: task.awaitingReason, resumeAt: task.resumeAt }] : [];
16642
+ });
16643
+ }
16644
+ function allowlistLine({ runner, before, after }) {
16645
+ if (!after) return `${runner} is no longer limited: any of its models may be assigned.`;
16646
+ if (!before) return `${runner} is now limited to: ${after.join(", ")}.`;
16647
+ const added = after.filter((id) => !before.includes(id));
16648
+ const removed = before.filter((id) => !after.includes(id));
16649
+ return `${runner} models: ${[added.length ? `now allowed ${added.join(", ")}` : "", removed.length ? `no longer allowed ${removed.join(", ")}` : ""].filter(Boolean).join("; ")}.`;
16650
+ }
16651
+ function runnersChangedBlock(change, flagged, tools) {
16652
+ return [
16653
+ "<runners_changed>",
16654
+ "The user changed which runners and models you may assign since your last turn.",
16655
+ change.switchedOn.length ? `Runners switched on: ${change.switchedOn.join(", ")}.` : "",
16656
+ change.switchedOff.length ? `Runners switched off: ${change.switchedOff.join(", ")}.` : "",
16657
+ ...change.allowlists.map(allowlistLine),
16658
+ flagged.length ? ["These tasks are now outside that:", ...flagged.map((f) => `- ${f.ref} "${f.title}" (${f.text}) \u2014 ${assignmentConsequence(f)}`)].join("\n") : "",
16659
+ `${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." : ""}`,
16660
+ "</runners_changed>"
16661
+ ].filter(Boolean).join("\n");
16662
+ }
16663
+
16386
16664
  // src/services/PlannerConversation.ts
16387
16665
  var CATALOG_MODEL_CAP = 100;
16388
16666
  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.";
@@ -16447,6 +16725,17 @@ var PlannerTurnFailedError = class extends Error {
16447
16725
  this.name = "PlannerTurnFailedError";
16448
16726
  }
16449
16727
  };
16728
+ var PlannerUsageLimitError = class extends PlannerTurnFailedError {
16729
+ resendAt;
16730
+ /** Why nothing is sent again on its own, when nothing is. */
16731
+ held;
16732
+ constructor(limit) {
16733
+ super(limit.text);
16734
+ this.name = "PlannerUsageLimitError";
16735
+ this.resendAt = limit.resendAt;
16736
+ if (limit.held) this.held = limit.held;
16737
+ }
16738
+ };
16450
16739
  var PlannerTurnStoppedError = class extends Error {
16451
16740
  constructor(options) {
16452
16741
  super("The planner turn was stopped.", options);
@@ -16472,6 +16761,8 @@ var PlannerConversation = class {
16472
16761
  savedInBackground = 0;
16473
16762
  /** What the planner was last told of autorun: by its opening prompt, or a turn's block since. */
16474
16763
  toldAutorun = false;
16764
+ /** The runners and allowlist the planner was last told of: its opening prompt, or a turn's block since. Null until a conversation opens. */
16765
+ toldScope = null;
16475
16766
  live = null;
16476
16767
  openTurn = null;
16477
16768
  get transcript() {
@@ -16587,8 +16878,8 @@ var PlannerConversation = class {
16587
16878
  * from the moment it is handed in until it lands, is refused, or the turn
16588
16879
  * ends: the backend's reply claims it, but it has not landed by then.
16589
16880
  */
16590
- showHeld(turn, held) {
16591
- const result = held?.kind === "task_ops" ? this.host.validateOps(held.ops) : void 0;
16881
+ showHeld(turn, held2) {
16882
+ const result = held2?.kind === "task_ops" ? this.host.validateOps(held2.ops) : void 0;
16592
16883
  if (!result?.ok) {
16593
16884
  this.clearHeld(turn);
16594
16885
  return;
@@ -16607,9 +16898,9 @@ var PlannerConversation = class {
16607
16898
  * a whole plan, which an edit cannot be layered on.
16608
16899
  */
16609
16900
  pendingOps() {
16610
- const held = this.openTurn?.submitted;
16611
- if (!held) return [];
16612
- return held.kind === "task_ops" ? held.ops : null;
16901
+ const held2 = this.openTurn?.submitted;
16902
+ if (!held2) return [];
16903
+ return held2.kind === "task_ops" ? held2.ops : null;
16613
16904
  }
16614
16905
  /**
16615
16906
  * A read the planner makes through a tool. It spends the same per-turn budget
@@ -16649,7 +16940,8 @@ var PlannerConversation = class {
16649
16940
  history: plan.conversationHistory,
16650
16941
  researchLog: plan.researchLog,
16651
16942
  persisted: this.persisted,
16652
- savedInBackground: this.savedInBackground
16943
+ savedInBackground: this.savedInBackground,
16944
+ toldScope: this.toldScope
16653
16945
  };
16654
16946
  }
16655
16947
  /**
@@ -16663,7 +16955,9 @@ var PlannerConversation = class {
16663
16955
  * the undo is saved in turn, or a reload would bring the writes back.
16664
16956
  */
16665
16957
  restore(snapshot) {
16666
- if (this.host.plan() !== snapshot.plan || this.persisted !== snapshot.persisted) return false;
16958
+ if (this.host.plan() !== snapshot.plan) return false;
16959
+ this.toldScope = snapshot.toldScope;
16960
+ if (this.persisted !== snapshot.persisted) return false;
16667
16961
  const undo = () => {
16668
16962
  const actions = (snapshot.plan.conversationHistory ?? []).filter((entry) => entry.actor === "Ordewell setting" && !(snapshot.history ?? []).includes(entry));
16669
16963
  snapshot.plan.conversationHistory = actions.length > 0 ? [...snapshot.history ?? [], ...actions] : snapshot.history;
@@ -16698,8 +16992,9 @@ var PlannerConversation = class {
16698
16992
  * at it.
16699
16993
  */
16700
16994
  rewindTargets() {
16995
+ const opener = this.transcript.findIndex((m) => m.kind !== "error");
16701
16996
  return this.transcript.flatMap((m, index) => {
16702
- if (!isUserMessage(m) || index === 0) return [];
16997
+ if (!isUserMessage(m) || index === opener) return [];
16703
16998
  const line = m.content.split("\n")[0];
16704
16999
  const preview = line.length > REWIND_PREVIEW_WIDTH ? `${line.slice(0, REWIND_PREVIEW_WIDTH - 1)}\u2026` : line;
16705
17000
  return [{ index, preview, content: m.content, timestamp: m.timestamp }];
@@ -16826,8 +17121,10 @@ var PlannerConversation = class {
16826
17121
  return this.userTurn("supersede", goal, signal, async (userTurn) => {
16827
17122
  const opening = await prepare();
16828
17123
  this.assertCurrent(userTurn);
17124
+ userTurn.checkpoint = this.snapshot();
16829
17125
  this.recordUser(goal, (/* @__PURE__ */ new Date()).toISOString());
16830
17126
  this.toldAutorun = opening.autorun ?? false;
17127
+ this.toldScope = this.host.assignmentScope();
16831
17128
  const turn = await this.host.aiService().startConversation({
16832
17129
  ...opening,
16833
17130
  // The system prompt's goal is the text as typed; skill bodies ride the
@@ -16837,7 +17134,9 @@ var PlannerConversation = class {
16837
17134
  onProgress: userTurn.stream.sink(),
16838
17135
  signal: userTurn.signal
16839
17136
  });
16840
- return this.settle(await this.drainTaskQueries(turn, userTurn), userTurn);
17137
+ const settleable = await this.drainTaskQueries(turn, userTurn);
17138
+ this.assertNotLimited(settleable, userTurn);
17139
+ return this.settle(settleable, userTurn);
16841
17140
  });
16842
17141
  }
16843
17142
  /**
@@ -16870,13 +17169,16 @@ var PlannerConversation = class {
16870
17169
  // anything the turn appends as it runs goes after this point.
16871
17170
  at: priorHistory.length
16872
17171
  };
16873
- const contextBlock = [this.catalogBlock(), this.planContextBlock(), this.autorunBlock(), this.runUpdatesBlock()].filter(Boolean).join("\n\n");
17172
+ const toldBefore = this.toldScope;
17173
+ const contextBlock = [this.catalogBlock(), this.planContextBlock(), this.autorunBlock(), this.runnersChangedBlock(), this.runUpdatesBlock()].filter(Boolean).join("\n\n");
16874
17174
  const said2 = fromUser ? plannerMessage(message.text, message.skills) : "";
16875
17175
  const outgoing = [contextBlock, ...wakeUps, userTurn.wakeUp?.brief, said2].filter(Boolean).join("\n\n");
16876
17176
  const ai = this.host.aiService();
16877
17177
  try {
16878
17178
  const turn = this.canContinueLive() ? await ai.continueConversation(outgoing, userTurn.stream.sink(), signal) : await this.resume(outgoing, priorHistory, signal, userTurn.stream.sink());
16879
17179
  const settleable = await this.drainTaskQueries(turn, userTurn);
17180
+ if (settleable.kind === "message" && (settleable.failed || userTurn.signal.aborted)) this.toldScope = toldBefore;
17181
+ this.assertNotLimited(settleable, userTurn);
16880
17182
  if (userTurn.wakeUp) this.assertWakeUpAnswered(settleable, userTurn);
16881
17183
  this.assertCurrent(userTurn, settleable);
16882
17184
  return await this.settle(settleable, userTurn);
@@ -16895,6 +17197,34 @@ var PlannerConversation = class {
16895
17197
  if (userTurn.signal.aborted) throw new PlannerTurnStoppedError();
16896
17198
  if (turn.kind === "message" && turn.failed) throw new PlannerTurnFailedError(turn.text);
16897
17199
  }
17200
+ /**
17201
+ * A turn that ended on the planner's usage limit lands nothing. What it
17202
+ * wrote is taken back, its failure is kept in its place, and the limit's
17203
+ * wait is armed in the same save. Only a turn Ordewell started gets here,
17204
+ * so a limit is never acted on for a planner nobody asked anything of.
17205
+ */
17206
+ assertNotLimited(turn, userTurn) {
17207
+ if (turn.kind !== "message" || !turn.failed || userTurn.signal.aborted) return;
17208
+ const said2 = { message: turn.text, usageLimit: turn.usageLimit };
17209
+ if (classifyRunnerStop(said2) !== "usage-limit") return;
17210
+ this.assertCurrent(userTurn);
17211
+ if (userTurn.checkpoint) this.restore(userTurn.checkpoint);
17212
+ const limit = this.host.usageLimit(said2);
17213
+ this.host.mutate(() => {
17214
+ this.recordFailure(limit.text);
17215
+ return true;
17216
+ }, () => this.host.broadcastPlan());
17217
+ throw new PlannerUsageLimitError(limit);
17218
+ }
17219
+ /**
17220
+ * A failure kept in the transcript, drawn as one on reload too, and never
17221
+ * replayed to the planner. Stamped after the newest entry: surfaces take
17222
+ * only entries newer than the last they synced.
17223
+ */
17224
+ recordFailure(text) {
17225
+ const newest = this.transcript.reduce((latest, e) => Math.max(latest, Date.parse(e.timestamp) || 0), 0);
17226
+ this.append("assistant", text, { kind: "error", timestamp: new Date(Math.max(Date.now(), newest + 1)).toISOString() });
17227
+ }
16898
17228
  assertIdle(operation) {
16899
17229
  if (this.isTurnInFlight) throw new ConversationBusyError(operation);
16900
17230
  }
@@ -17036,6 +17366,7 @@ var PlannerConversation = class {
17036
17366
  const goal = opener || this.host.goal() || message;
17037
17367
  this.reset();
17038
17368
  this.toldAutorun = opening.autorun ?? false;
17369
+ this.toldScope = this.host.assignmentScope();
17039
17370
  return this.host.aiService().startConversation({
17040
17371
  ...opening,
17041
17372
  runners,
@@ -17274,6 +17605,24 @@ The plan is unchanged. Rephrase the request, or adjust the tasks manually.`,
17274
17605
  this.toldAutorun = now;
17275
17606
  return now ? AUTORUN_ON : AUTORUN_OFF;
17276
17607
  }
17608
+ /**
17609
+ * The user's runner or allowlist change, when the planner was not told yet —
17610
+ * the same once-per-change rule as autorun, for API and harness planners
17611
+ * alike. Tasks the change leaves outside the scope are named, not touched.
17612
+ *
17613
+ * A reloaded session is not told: {@link resume} takes the scope as it is
17614
+ * then, as it does the autorun rule, since the catalog the first turn carries
17615
+ * and `list_runners` already show the runners as they are now.
17616
+ */
17617
+ runnersChangedBlock() {
17618
+ const now = this.host.assignmentScope();
17619
+ const told = this.toldScope;
17620
+ this.toldScope = now;
17621
+ const change = told && scopeChange(told, now);
17622
+ if (!change) return null;
17623
+ const flagged = flaggedTasks(flattenTasksWithParents(this.host.tasks()), now);
17624
+ return runnersChangedBlock(change, flagged, this.host.aiService().plannerToolsAttached?.() ?? false);
17625
+ }
17277
17626
  runUpdatesBlock() {
17278
17627
  const updates = this.host.takeRunUpdates();
17279
17628
  return updates.length > 0 ? ["Run update:", ...updates.map((update) => `- ${update}`)].join("\n") : null;
@@ -17354,7 +17703,6 @@ var WAKE_DEBOUNCE_MS = 5e3;
17354
17703
  var WAKE_UP_TURN_TIMEOUT_MS = 10 * 6e4;
17355
17704
  var HUNG_TURN_GRACE_MS = 3e4;
17356
17705
  var DEFAULT_WAKE_UP_READ_BUDGET = 12;
17357
- var RESET_MARGIN_MS = 6e4;
17358
17706
  var DETAIL_CHARS = 240;
17359
17707
  var WakeUpTimeoutError = class extends Error {
17360
17708
  constructor(timeoutMs) {
@@ -17380,8 +17728,6 @@ var Supervisor = class {
17380
17728
  /** The run's end was reported: the scheduler can announce it more than once. */
17381
17729
  finished = false;
17382
17730
  runEndPending = false;
17383
- /** While the planner's usage limit holds wake-ups back: when it lets them go. */
17384
- limited = null;
17385
17731
  /** When each wake-up since the last pass was delivered, for the wake-up rate guard. */
17386
17732
  wakeUps = [];
17387
17733
  debounceMs;
@@ -17402,9 +17748,9 @@ var Supervisor = class {
17402
17748
  get awaitsRunEnd() {
17403
17749
  return this.runEndPending;
17404
17750
  }
17405
- /** Whether wake-ups alone may open a turn now: the supervisor watches, and no usage limit holds it. */
17751
+ /** 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. */
17406
17752
  mayWake() {
17407
- return this.watching && this.limited === null;
17753
+ return this.watching;
17408
17754
  }
17409
17755
  /**
17410
17756
  * What a wake-up turn is held to under the run's level. The brief is the
@@ -17471,8 +17817,6 @@ var Supervisor = class {
17471
17817
  /** The plan these events were about is gone: nothing gathered or waited on carries over. */
17472
17818
  reset() {
17473
17819
  this.disarm();
17474
- if (this.limited) clearTimeout(this.limited.timer);
17475
- this.limited = null;
17476
17820
  this.failures = 0;
17477
17821
  this.finished = false;
17478
17822
  this.runEndPending = false;
@@ -17523,9 +17867,13 @@ var Supervisor = class {
17523
17867
  }
17524
17868
  failed(err) {
17525
17869
  if (err instanceof PlannerTurnStoppedError || err instanceof PlannerTurnDiscardedError) return;
17870
+ if (err instanceof PlannerUsageLimitError) {
17871
+ if (err.resendAt === null) this.giveUp(`the planner hit its usage limit and ${err.held ?? "did not say when it resets"}`);
17872
+ return;
17873
+ }
17526
17874
  const reason = err instanceof Error ? err.message : String(err);
17527
- if (!(err instanceof WakeUpTimeoutError) && classifyRunnerStop(reason) === "usage-limit") {
17528
- this.waitForReset(reason);
17875
+ if (!(err instanceof WakeUpTimeoutError) && !(err instanceof PlannerTurnFailedError) && classifyRunnerStop(reason) === "usage-limit") {
17876
+ this.giveUp("the planner hit its usage limit and did not say when it resets");
17529
17877
  return;
17530
17878
  }
17531
17879
  this.failures++;
@@ -17536,22 +17884,6 @@ var Supervisor = class {
17536
17884
  }
17537
17885
  this.giveUp(`its wake-up turn failed twice (${brief(reason)})`);
17538
17886
  }
17539
- waitForReset(said2) {
17540
- const runner = this.host.plannerRunner();
17541
- const reset = runner ? readUsageLimitReset(runner, said2, new Date(this.now())) : null;
17542
- if (!reset) {
17543
- this.giveUp("the planner hit its usage limit and did not say when it resets");
17544
- return;
17545
- }
17546
- if (this.limited) clearTimeout(this.limited.timer);
17547
- const until = reset.getTime() + RESET_MARGIN_MS;
17548
- const timer = setTimeout(() => {
17549
- this.limited = null;
17550
- this.host.pump();
17551
- }, Math.max(0, until - this.now()));
17552
- this.limited = { until, timer };
17553
- 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.`);
17554
- }
17555
17887
  /** The run goes on without a supervisor, and nothing that happened is lost (see {@link standDown}). */
17556
17888
  giveUp(reason) {
17557
17889
  this.host.goUnsupervised(reason);
@@ -17565,8 +17897,8 @@ var Supervisor = class {
17565
17897
  standDown() {
17566
17898
  this.disarm();
17567
17899
  this.runEndPending = false;
17568
- const held = this.policy.poll(Number.POSITIVE_INFINITY);
17569
- if (held) this.deliver(held);
17900
+ const held2 = this.policy.poll(Number.POSITIVE_INFINITY);
17901
+ if (held2) this.deliver(held2);
17570
17902
  }
17571
17903
  arm() {
17572
17904
  const due = this.policy.dueAt();
@@ -17757,78 +18089,224 @@ function runSettingsOf(settings) {
17757
18089
  }
17758
18090
  return out;
17759
18091
  }
18092
+ var SAVE_TRIES = 5;
18093
+ var LOCK_STALE_MS = 2e3;
18094
+ var LOCK_TIMEOUT_MS = 1e4;
18095
+ var LOCK_POLL_MS = 5;
18096
+ var LOCK_POLL_MAX_MS = 100;
18097
+ function sleepSync(ms) {
18098
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
18099
+ }
18100
+ function withFileLock(file, swap) {
18101
+ const lock = `${file}.lock`;
18102
+ const token = `${process.pid}:${randomUUID3()}`;
18103
+ const deadline = Date.now() + LOCK_TIMEOUT_MS;
18104
+ let last = null;
18105
+ let poll = LOCK_POLL_MS;
18106
+ for (; ; ) {
18107
+ try {
18108
+ const fd = fs10.openSync(lock, "wx");
18109
+ try {
18110
+ fs10.writeSync(fd, token);
18111
+ } finally {
18112
+ fs10.closeSync(fd);
18113
+ }
18114
+ break;
18115
+ } catch (err) {
18116
+ if (err.code !== "EEXIST") throw err;
18117
+ }
18118
+ if (Date.now() >= deadline) {
18119
+ 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.`);
18120
+ }
18121
+ let holder;
18122
+ let mtimeMs;
18123
+ try {
18124
+ holder = fs10.readFileSync(lock, "utf8");
18125
+ mtimeMs = fs10.statSync(lock).mtimeMs;
18126
+ } catch (err) {
18127
+ if (gone(err)) {
18128
+ last = null;
18129
+ continue;
18130
+ }
18131
+ throw err;
18132
+ }
18133
+ const stale = Date.now() - mtimeMs > LOCK_STALE_MS;
18134
+ if (last && last.token === holder && last.mtimeMs === mtimeMs && stale) {
18135
+ unlinkLock(lock);
18136
+ last = null;
18137
+ continue;
18138
+ }
18139
+ last = stale ? { token: holder, mtimeMs } : null;
18140
+ sleepSync(poll);
18141
+ poll = Math.min(poll * 2, LOCK_POLL_MAX_MS);
18142
+ }
18143
+ try {
18144
+ return swap();
18145
+ } finally {
18146
+ releaseLock(lock, token);
18147
+ }
18148
+ }
18149
+ function gone(err) {
18150
+ return err.code === "ENOENT";
18151
+ }
18152
+ function releaseLock(lock, token) {
18153
+ let holder;
18154
+ try {
18155
+ holder = fs10.readFileSync(lock, "utf8");
18156
+ } catch (err) {
18157
+ if (gone(err)) return;
18158
+ throw err;
18159
+ }
18160
+ if (holder === token) unlinkLock(lock);
18161
+ }
18162
+ function unlinkLock(lock) {
18163
+ try {
18164
+ fs10.unlinkSync(lock);
18165
+ } catch (err) {
18166
+ if (!gone(err)) throw err;
18167
+ }
18168
+ }
17760
18169
  var SettingsService = class {
17761
18170
  filePath;
17762
18171
  cache = null;
17763
- cachedMtimeMs = null;
18172
+ cachedStamp = null;
17764
18173
  constructor(filePath = getSettingsPath()) {
17765
18174
  this.filePath = filePath;
17766
18175
  }
17767
18176
  // Several processes share the settings file (VS Code extension host, web
17768
- // server, CLI), so the cache is keyed on the file's mtime: a write from any
18177
+ // server, CLI), so the cache is keyed on the file's identity: a write from any
17769
18178
  // other process invalidates it. A stale cache here previously meant an
17770
18179
  // allowlist set in one surface never reached another's planner.
17771
18180
  getAll() {
17772
- const mtime = this.fileMtimeMs();
17773
- if (this.cache && mtime === this.cachedMtimeMs) return this.cache;
18181
+ const stamp = this.fileStamp();
18182
+ if (this.cache && stamp === this.cachedStamp) return this.cache;
17774
18183
  this.cache = this.load();
17775
- this.cachedMtimeMs = mtime;
18184
+ this.cachedStamp = stamp;
17776
18185
  return this.cache;
17777
18186
  }
17778
- fileMtimeMs() {
18187
+ /**
18188
+ * Every write replaces the file, so its inode changes with it; the mtime
18189
+ * alone cannot tell two writes apart on a filesystem whose timestamps are
18190
+ * coarse, and a write that shares the last one's tick stayed unseen.
18191
+ */
18192
+ fileStamp(file = this.filePath) {
17779
18193
  try {
17780
- return fs10.statSync(this.filePath).mtimeMs;
18194
+ const stat = fs10.statSync(file);
18195
+ return `${stat.ino}:${stat.mtimeMs}:${stat.size}`;
17781
18196
  } catch {
17782
18197
  return null;
17783
18198
  }
17784
18199
  }
18200
+ /**
18201
+ * What a change is applied to: the file as it is now, never a copy held since
18202
+ * an earlier read. Another process may have saved in between, and writing
18203
+ * the whole file back would silently undo it.
18204
+ */
18205
+ current() {
18206
+ this.cache = null;
18207
+ return this.getAll();
18208
+ }
18209
+ /**
18210
+ * Calls `listener` when another process (or this one) saves the file. The
18211
+ * directory is watched, not the file: a write that swaps the file in leaves
18212
+ * a watch on the old one pointing at nothing.
18213
+ */
18214
+ watch(listener) {
18215
+ const dir = path25.dirname(this.filePath);
18216
+ const name = path25.basename(this.filePath);
18217
+ let seen = this.fileStamp();
18218
+ let timer;
18219
+ let watcher;
18220
+ try {
18221
+ fs10.mkdirSync(dir, { recursive: true });
18222
+ watcher = fs10.watch(dir, (_event, changed) => {
18223
+ if (changed && changed !== name) return;
18224
+ clearTimeout(timer);
18225
+ timer = setTimeout(() => {
18226
+ const stamp = this.fileStamp();
18227
+ if (stamp === seen) return;
18228
+ seen = stamp;
18229
+ listener();
18230
+ }, 50);
18231
+ });
18232
+ } catch {
18233
+ return { dispose() {
18234
+ } };
18235
+ }
18236
+ return { dispose: () => {
18237
+ clearTimeout(timer);
18238
+ watcher.close();
18239
+ } };
18240
+ }
17785
18241
  getModelAllowlist(runner) {
17786
18242
  return this.getAll().modelAllowlist?.[runner];
17787
18243
  }
17788
18244
  setModelAllowlist(runner, ids) {
17789
- this.getAll();
17790
- if (ids === void 0) {
17791
- if (this.cache.modelAllowlist) {
17792
- delete this.cache.modelAllowlist[runner];
17793
- if (Object.keys(this.cache.modelAllowlist).length === 0) {
17794
- this.cache.modelAllowlist = void 0;
18245
+ this.save(() => {
18246
+ if (ids === void 0) {
18247
+ if (this.cache.modelAllowlist) {
18248
+ delete this.cache.modelAllowlist[runner];
18249
+ if (Object.keys(this.cache.modelAllowlist).length === 0) {
18250
+ this.cache.modelAllowlist = void 0;
18251
+ }
17795
18252
  }
18253
+ } else {
18254
+ if (!this.cache.modelAllowlist) {
18255
+ this.cache.modelAllowlist = {};
18256
+ }
18257
+ this.cache.modelAllowlist[runner] = ids;
17796
18258
  }
17797
- } else {
17798
- if (!this.cache.modelAllowlist) {
17799
- this.cache.modelAllowlist = {};
17800
- }
17801
- this.cache.modelAllowlist[runner] = ids;
17802
- }
17803
- this.persist();
18259
+ });
17804
18260
  }
17805
18261
  getPlannerModel(provider) {
17806
18262
  return this.getAll().plannerModels?.[provider];
17807
18263
  }
17808
18264
  setPlannerModel(provider, entry) {
17809
- this.getAll();
17810
- if (entry === void 0) {
17811
- if (this.cache.plannerModels) {
17812
- delete this.cache.plannerModels[provider];
17813
- if (Object.keys(this.cache.plannerModels).length === 0) {
17814
- this.cache.plannerModels = void 0;
18265
+ this.save(() => {
18266
+ if (entry === void 0) {
18267
+ if (this.cache.plannerModels) {
18268
+ delete this.cache.plannerModels[provider];
18269
+ if (Object.keys(this.cache.plannerModels).length === 0) {
18270
+ this.cache.plannerModels = void 0;
18271
+ }
17815
18272
  }
18273
+ } else {
18274
+ if (!this.cache.plannerModels) {
18275
+ this.cache.plannerModels = {};
18276
+ }
18277
+ this.cache.plannerModels[provider] = entry.effort ? { model: entry.model, effort: entry.effort } : { model: entry.model };
17816
18278
  }
17817
- } else {
17818
- if (!this.cache.plannerModels) {
17819
- this.cache.plannerModels = {};
17820
- }
17821
- this.cache.plannerModels[provider] = entry.effort ? { model: entry.model, effort: entry.effort } : { model: entry.model };
17822
- }
17823
- this.persist();
18279
+ });
17824
18280
  }
17825
18281
  getEnabledRunners() {
17826
18282
  return this.getAll().enabledRunners;
17827
18283
  }
17828
18284
  setEnabledRunners(ids) {
17829
- this.getAll();
17830
- this.cache.enabledRunners = [...ids];
17831
- this.persist();
18285
+ this.save(() => {
18286
+ this.cache.enabledRunners = [...ids];
18287
+ });
18288
+ }
18289
+ /**
18290
+ * Change the list from what the file holds at this moment: the change is
18291
+ * handed the saved list (undefined when the user never chose) and its result
18292
+ * is saved in the same call. A list worked out from an earlier read would
18293
+ * save over what another surface set since. It may be handed the list again,
18294
+ * when another process saved before this one could (see {@link save}).
18295
+ */
18296
+ updateEnabledRunners(change) {
18297
+ return this.save(() => {
18298
+ const next = [...change(this.cache.enabledRunners)];
18299
+ this.cache.enabledRunners = next;
18300
+ return next;
18301
+ });
18302
+ }
18303
+ /** Save `ids` as the user's choice unless one is saved already, judged on the file as it is now. */
18304
+ setEnabledRunnersIfUnset(ids) {
18305
+ return this.save(() => {
18306
+ if (this.cache.enabledRunners !== void 0) return false;
18307
+ this.cache.enabledRunners = [...ids];
18308
+ return true;
18309
+ });
17832
18310
  }
17833
18311
  /**
17834
18312
  * Full or Guarded (ADR-0001), as saved here (ADR-0030). A value set in the
@@ -17846,9 +18324,9 @@ var SettingsService = class {
17846
18324
  return env !== void 0 && env.trim() !== "";
17847
18325
  }
17848
18326
  setAutonomousMode(enabled) {
17849
- this.getAll();
17850
- this.cache.autonomousMode = enabled;
17851
- this.persist();
18327
+ this.save(() => {
18328
+ this.cache.autonomousMode = enabled;
18329
+ });
17852
18330
  }
17853
18331
  getRunSettings() {
17854
18332
  return runSettingsOf(this.getAll());
@@ -17860,11 +18338,11 @@ var SettingsService = class {
17860
18338
  throw new RunSettingsError(RUN_SETTING_CHECKS[key].refusal);
17861
18339
  }
17862
18340
  }
17863
- this.getAll();
17864
- for (const key of RUN_SETTING_KEYS) {
17865
- if (changes[key] !== void 0) this.cache[key] = changes[key];
17866
- }
17867
- this.persist();
18341
+ this.save(() => {
18342
+ for (const key of RUN_SETTING_KEYS) {
18343
+ if (changes[key] !== void 0) this.cache[key] = changes[key];
18344
+ }
18345
+ });
17868
18346
  return this.getRunSettings();
17869
18347
  }
17870
18348
  load() {
@@ -17893,13 +18371,43 @@ var SettingsService = class {
17893
18371
  }
17894
18372
  return {};
17895
18373
  }
17896
- persist() {
18374
+ /**
18375
+ * Makes `change` to the file as it is now, and saves it. Another process can
18376
+ * save between that read and this write; the file is looked at again just
18377
+ * before ours replaces it, and when it moved the change is made afresh from
18378
+ * what that process saved, so neither save undoes the other. A file that
18379
+ * keeps moving is written over after {@link SAVE_TRIES} tries rather than
18380
+ * never. The look and the swap are one step under the file's lock
18381
+ * ({@link withFileLock}). A change that changes nothing writes nothing.
18382
+ */
18383
+ save(change) {
18384
+ for (let tries = 1; ; tries++) {
18385
+ const before = JSON.stringify(this.current());
18386
+ const read2 = this.cachedStamp;
18387
+ const result = change();
18388
+ if (JSON.stringify(this.cache) === before || this.persist(tries < SAVE_TRIES ? read2 : void 0)) return result;
18389
+ }
18390
+ }
18391
+ /** False, saving nothing, when the file is no longer the one stamped `expected`. */
18392
+ persist(expected) {
17897
18393
  const dir = path25.dirname(this.filePath);
17898
18394
  if (!fs10.existsSync(dir)) {
17899
18395
  fs10.mkdirSync(dir, { recursive: true });
17900
18396
  }
17901
- fs10.writeFileSync(this.filePath, JSON.stringify(this.cache, null, 2));
17902
- this.cachedMtimeMs = this.fileMtimeMs();
18397
+ const staging = `${this.filePath}.${process.pid}.tmp`;
18398
+ fs10.writeFileSync(staging, JSON.stringify(this.cache, null, 2));
18399
+ const stamp = this.fileStamp(staging);
18400
+ const swapped = withFileLock(this.filePath, () => {
18401
+ if (expected !== void 0 && this.fileStamp() !== expected) return false;
18402
+ fs10.renameSync(staging, this.filePath);
18403
+ return true;
18404
+ });
18405
+ if (!swapped) {
18406
+ fs10.rmSync(staging, { force: true });
18407
+ return false;
18408
+ }
18409
+ this.cachedStamp = stamp;
18410
+ return true;
17903
18411
  }
17904
18412
  };
17905
18413
 
@@ -18383,7 +18891,7 @@ var EnvConfig = class extends BaseConfig {
18383
18891
  };
18384
18892
 
18385
18893
  // src/services/mergeResultNotice.ts
18386
- function blockNotice({ repo, reason, files }) {
18894
+ function blockNotice({ repo, reason, files, expected, actual, rebasing }) {
18387
18895
  switch (reason) {
18388
18896
  case "merge-in-progress":
18389
18897
  return `${repo} has a merge in progress`;
@@ -18393,10 +18901,22 @@ function blockNotice({ repo, reason, files }) {
18393
18901
  return `${repo} has uncommitted changes to ${files.join(", ")}`;
18394
18902
  case "partial-landing":
18395
18903
  return `${repo} holds part of a task whose landing could not be rolled back`;
18904
+ case "wrong-branch":
18905
+ 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}`;
18396
18906
  case "git-error":
18397
18907
  return `git could not check ${repo}`;
18398
18908
  }
18399
18909
  }
18910
+ function branchFix(blocked) {
18911
+ if (blocked.length === 0 || blocked.some((b) => b.reason !== "wrong-branch")) return "";
18912
+ const rebasing = blocked.filter((b) => b.rebasing);
18913
+ if (rebasing.length > 0) {
18914
+ const where = rebasing.map((b) => b.repo === SELF_REPO ? "your checkout" : b.repo).join(", ");
18915
+ 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.`;
18916
+ }
18917
+ const started = [...new Set(blocked.map((b) => b.expected))];
18918
+ 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.";
18919
+ }
18400
18920
  function landedNotice(landed2) {
18401
18921
  if (landed2.length === 0) return "Nothing was merged.";
18402
18922
  return landed2.length === 1 ? `${landed2[0]} was merged already and stays merged.` : `${landed2.join(", ")} were merged already and stay merged.`;
@@ -18413,7 +18933,7 @@ function describeMergeResult(result, branch, group, repaired = []) {
18413
18933
  return { level: "info", message: repaired.length > 0 ? `${base} ${repairedNotice(repaired)}` : base };
18414
18934
  }
18415
18935
  case "blocked":
18416
- return { level: "warn", message: `Merged nothing, so every tree is as it was: ${result.blocked.map(blockNotice).join("; ")}.` };
18936
+ return { level: "warn", message: `Merged nothing, so every tree is as it was: ${result.blocked.map(blockNotice).join("; ")}.${branchFix(result.blocked)}` };
18417
18937
  case "conflict":
18418
18938
  return {
18419
18939
  level: "warn",
@@ -18777,7 +19297,7 @@ var GitWorktreeIsolation = class {
18777
19297
  path: repoPath,
18778
19298
  root,
18779
19299
  baseRef,
18780
- ...branch ? { baseBranch: branch } : {},
19300
+ ...branch ? { baseBranch: branch } : { startedDetached: true },
18781
19301
  integrationBranch: integrationBranchFor(run.id)
18782
19302
  };
18783
19303
  if (!(await this.tryGit(root, ["branch", repo.integrationBranch, baseRef])).ok) return null;
@@ -19130,7 +19650,9 @@ var GitWorktreeIsolation = class {
19130
19650
  async mergeAll(run) {
19131
19651
  const partial = await this.settleLanding(run);
19132
19652
  if (partial.length > 0) return { outcome: "blocked", blocked: partial.map((repo) => ({ repo, reason: "partial-landing", files: [] })) };
19653
+ const misplaced = await this.offBaseBranch(run.repos);
19133
19654
  const repos = await this.reposWithWork(run);
19655
+ if (misplaced.length > 0) return { outcome: "blocked", blocked: misplaced };
19134
19656
  if (run.repos.length === 1 || !await this.canPreflight()) return this.mergeInTurn(repos);
19135
19657
  const blocked = [];
19136
19658
  for (const repo of repos) {
@@ -19148,6 +19670,27 @@ var GitWorktreeIsolation = class {
19148
19670
  }
19149
19671
  return withWork;
19150
19672
  }
19673
+ /**
19674
+ * A merge goes into whatever is checked out, which is only what the user
19675
+ * meant if it is the branch the run started on. A run that started on a
19676
+ * detached HEAD named no branch, so anything is as good as it was.
19677
+ */
19678
+ async offBaseBranch(repos) {
19679
+ const off = [];
19680
+ for (const repo of repos) {
19681
+ if (!repo.baseBranch) continue;
19682
+ const current = (await this.tryGit(repo.root, ["symbolic-ref", "--short", "-q", "HEAD"])).stdout.trim();
19683
+ if (current === repo.baseBranch) continue;
19684
+ off.push({
19685
+ repo: repo.path,
19686
+ reason: "wrong-branch",
19687
+ files: [],
19688
+ expected: repo.baseBranch,
19689
+ ...current ? { actual: current } : await this.rebaseInProgress(repo.root) ? { rebasing: true } : {}
19690
+ });
19691
+ }
19692
+ return off;
19693
+ }
19151
19694
  /** `git merge-tree --write-tree`, which merges without touching a tree, arrived in git 2.38. */
19152
19695
  async canPreflight() {
19153
19696
  const version = await this.tryGit(void 0, ["--version"]);
@@ -19635,6 +20178,14 @@ var GitWorktreeIsolation = class {
19635
20178
  if ((await this.tryGit(cwd, ["rev-parse", "-q", "--verify", "MERGE_HEAD"])).ok) return true;
19636
20179
  return (await this.tryGit(cwd, ["ls-files", "-u"])).stdout.trim() !== "";
19637
20180
  }
20181
+ /** A rebase leaves HEAD detached, so it is told from a checkout the user detached on purpose. */
20182
+ async rebaseInProgress(cwd) {
20183
+ for (const dir of ["rebase-merge", "rebase-apply"]) {
20184
+ const found = await this.tryGit(cwd, ["rev-parse", "--git-path", dir]);
20185
+ if (found.ok && fs13.existsSync(path28.resolve(cwd, found.stdout.trim()))) return true;
20186
+ }
20187
+ return false;
20188
+ }
19638
20189
  async abortMerge(cwd) {
19639
20190
  if (!(await this.tryGit(cwd, ["merge", "--abort"])).ok) await this.tryGit(cwd, ["reset", "--hard"]);
19640
20191
  }
@@ -19655,6 +20206,29 @@ function createWorktreeIsolation(deps) {
19655
20206
  return new GitWorktreeIsolation(deps);
19656
20207
  }
19657
20208
 
20209
+ // src/services/mergeWording.ts
20210
+ function isRepoGroup(handoff) {
20211
+ return handoff.repos.some((r) => r.path !== ".");
20212
+ }
20213
+ function reposWithWork(handoff) {
20214
+ const withWork = handoff.repos.filter((r) => r.landed.length > 0);
20215
+ return (withWork.length > 0 ? withWork : handoff.repos).map((r) => r.path);
20216
+ }
20217
+ function mergeTarget(handoff) {
20218
+ if (!isRepoGroup(handoff)) return handoff.repos[0]?.baseBranch ?? "the branch you have checked out";
20219
+ const targets = handoff.repos.filter((r) => reposWithWork(handoff).includes(r.path));
20220
+ const branches = new Set(targets.map((r) => r.baseBranch));
20221
+ const [only] = branches;
20222
+ if (branches.size === 1 && only) return `${only} in each of ${targets.map((r) => r.path).join(", ")}`;
20223
+ return `each of ${targets.map((r) => `${r.path} (${r.baseBranch ?? "as checked out"})`).join(", ")}`;
20224
+ }
20225
+ function checkoutRule(handoff) {
20226
+ const named3 = handoff.repos.filter((r) => r.baseBranch);
20227
+ if (named3.length === 0) return "";
20228
+ if (!isRepoGroup(handoff)) return ` Nothing is merged unless ${named3[0].baseBranch} is checked out.`;
20229
+ return " Nothing is merged unless each repository is on the branch it started on.";
20230
+ }
20231
+
19658
20232
  // src/interfaces/ILogger.ts
19659
20233
  var ConsoleLogger = class {
19660
20234
  warn(scope, message, err) {
@@ -19707,7 +20281,10 @@ async function settleReply(opts) {
19707
20281
  opts.onProgress({ type: "interrupted" });
19708
20282
  return { done: turn };
19709
20283
  }
19710
- if (attempt.failure !== void 0) return { done: { ...message(attempt.failure), failed: true } };
20284
+ if (attempt.failure !== void 0) {
20285
+ const { usageLimit } = attempt;
20286
+ return { done: { ...message(attempt.failure), failed: true, ...usageLimit !== void 0 && { usageLimit } } };
20287
+ }
19711
20288
  if (!attempt.text.trim()) return { done: message(emptyReplyReport(deniedStep(attempt))) };
19712
20289
  const reply2 = opts.classify ? classifyPlannerReply(attempt.text, opts.classify) : { kind: "prose" };
19713
20290
  switch (reply2.kind) {
@@ -20638,7 +21215,7 @@ var CONVERSATION_TOOL_DEFINITIONS = toGeminiToolDeclarations(true);
20638
21215
  function tokenCount(value) {
20639
21216
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
20640
21217
  }
20641
- function usageRecordFrom(usage) {
21218
+ function usageRecordFrom(usage, model) {
20642
21219
  if (!usage) return void 0;
20643
21220
  const inputTokens = tokenCount(usage.promptTokenCount);
20644
21221
  const answerTokens = tokenCount(usage.candidatesTokenCount);
@@ -20646,6 +21223,7 @@ function usageRecordFrom(usage) {
20646
21223
  const reportedOutput = [answerTokens, thoughtTokens].filter((t) => t !== void 0);
20647
21224
  const record = {
20648
21225
  source: "google",
21226
+ model,
20649
21227
  ...inputTokens !== void 0 ? { inputTokens } : {},
20650
21228
  ...reportedOutput.length > 0 ? { outputTokens: reportedOutput.reduce((a, b) => a + b, 0) } : {},
20651
21229
  ...tokenCount(usage.cachedContentTokenCount) !== void 0 ? { cachedInputTokens: tokenCount(usage.cachedContentTokenCount) } : {}
@@ -20654,15 +21232,31 @@ function usageRecordFrom(usage) {
20654
21232
  return anyReported ? record : void 0;
20655
21233
  }
20656
21234
  var GeminiResearchChat = class {
20657
- constructor(chat, hooks = {}) {
20658
- this.chat = chat;
21235
+ constructor(getModel, open, hooks = {}) {
21236
+ this.getModel = getModel;
21237
+ this.open = open;
20659
21238
  this.hooks = hooks;
21239
+ this.model = getModel();
21240
+ this.chat = open(this.model);
20660
21241
  }
20661
- chat;
21242
+ getModel;
21243
+ open;
20662
21244
  hooks;
21245
+ chat;
21246
+ /** The model `chat` calls, named on its usage records. */
21247
+ model;
21248
+ /** The SDK binds a chat to one model, so a switch reopens it on the new one over the history so far. */
21249
+ async current() {
21250
+ const model = this.getModel();
21251
+ if (model !== this.model) {
21252
+ this.chat = this.open(model, await this.chat.getHistory());
21253
+ this.model = model;
21254
+ }
21255
+ return this.chat;
21256
+ }
20663
21257
  async sendMessage(text, signal) {
20664
21258
  if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
20665
- const result = await this.chat.sendMessageStream(text);
21259
+ const result = await (await this.current()).sendMessageStream(text);
20666
21260
  return this.consume(result.stream);
20667
21261
  }
20668
21262
  async sendToolResults(results, signal) {
@@ -20673,7 +21267,7 @@ var GeminiResearchChat = class {
20673
21267
  response: { output: r.output, truncated: r.truncated, totalChars: r.totalChars }
20674
21268
  }
20675
21269
  }));
20676
- const result = await this.chat.sendMessageStream(funcResponses);
21270
+ const result = await (await this.current()).sendMessageStream(funcResponses);
20677
21271
  return this.consume(result.stream);
20678
21272
  }
20679
21273
  /**
@@ -20713,7 +21307,7 @@ var GeminiResearchChat = class {
20713
21307
  }
20714
21308
  }
20715
21309
  }
20716
- const usageRecord3 = usageRecordFrom(usage);
21310
+ const usageRecord3 = usageRecordFrom(usage, this.model);
20717
21311
  if (usageRecord3) this.hooks.onUsage?.(usageRecord3);
20718
21312
  return {
20719
21313
  text,
@@ -20729,6 +21323,8 @@ var GeminiService = class extends BaseAiService {
20729
21323
  genAI = null;
20730
21324
  genAIKey = "";
20731
21325
  model = null;
21326
+ /** The id `model` was made for; the SDK's own `model.model` is its `models/…` resource path. */
21327
+ modelId = "";
20732
21328
  constructor(config) {
20733
21329
  super(config);
20734
21330
  }
@@ -20742,13 +21338,18 @@ var GeminiService = class extends BaseAiService {
20742
21338
  }
20743
21339
  return true;
20744
21340
  }
20745
- getPlanningModel() {
21341
+ /** The planner model picked for this session, as the API names it. */
21342
+ plannerModel() {
21343
+ return normalizeGeminiModel(this.config.orchestratorModel || this.config.planningModel);
21344
+ }
21345
+ getPlanningModel(model = this.plannerModel()) {
20746
21346
  this.init();
20747
- if (!this.model || this.model.model !== this.config.planningModel) {
21347
+ if (!this.model || this.modelId !== model) {
20748
21348
  this.model = this.genAI.getGenerativeModel({
20749
- model: this.config.planningModel,
21349
+ model,
20750
21350
  generationConfig: { temperature: 0.3, topP: 0.95, maxOutputTokens: 16384 }
20751
21351
  });
21352
+ this.modelId = model;
20752
21353
  }
20753
21354
  return this.model;
20754
21355
  }
@@ -20763,7 +21364,6 @@ var GeminiService = class extends BaseAiService {
20763
21364
  }
20764
21365
  async startConversation(req) {
20765
21366
  this.ensureInit();
20766
- const genModel = this.getPlanningModel();
20767
21367
  const contextStr = await BaseAiService.collectResearchContext(req.fs, req.runners);
20768
21368
  const systemPrompt = buildConversationSystemPrompt(
20769
21369
  req.goal,
@@ -20792,13 +21392,14 @@ ${m.content}` });
20792
21392
  if ((req.priorHistory?.length ?? 0) > 0 && seededHistory[seededHistory.length - 1].role === "user") {
20793
21393
  seededHistory.push({ role: "model", parts: [{ text: "Understood." }] });
20794
21394
  }
20795
- const chat = genModel.startChat({
20796
- history: seededHistory,
20797
- tools: [{ functionDeclarations: req.plannerTools ? CONVERSATION_TOOL_DEFINITIONS : TOOL_DEFINITIONS }],
21395
+ const tools = [{ functionDeclarations: req.plannerTools ? CONVERSATION_TOOL_DEFINITIONS : TOOL_DEFINITIONS }];
21396
+ const open = (model, history = seededHistory) => this.getPlanningModel(model).startChat({
21397
+ history,
21398
+ tools,
20798
21399
  generationConfig: { temperature: 0.3, topP: 0.95, maxOutputTokens: 16384 }
20799
21400
  });
20800
21401
  let currentProgress = req.onProgress;
20801
- const researchChat = new GeminiResearchChat(chat, {
21402
+ const researchChat = new GeminiResearchChat(() => this.plannerModel(), open, {
20802
21403
  onReasoning: (text, segmentId) => currentProgress({ type: "thinking", text, segmentId }),
20803
21404
  onContent: (text, segmentId) => currentProgress({ type: "text_delta", text, segmentId }),
20804
21405
  onUsage: (record) => currentProgress({ type: "usage", record })
@@ -20871,18 +21472,17 @@ ${repairHint}` }] : parts;
20871
21472
  async researchAndPlan(userDescription, runners, modelsByRunner, fs18, onProgress, fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
20872
21473
  this.ensureInit();
20873
21474
  const { autonomousDefault } = modes;
20874
- const genModel = this.getPlanningModel();
20875
21475
  const contextStr = await BaseAiService.collectResearchContext(fs18, runners);
20876
21476
  const systemPrompt = buildResearchPrompt(userDescription, contextStr, modelsByRunner, runners, runnerModes, modes);
20877
21477
  const firstMessage = `${buildResearchToolsPrompt()}
20878
21478
 
20879
21479
  Explore the workspace to understand the codebase, then generate the plan.`;
20880
- const chat = genModel.startChat({
20881
- history: [{ role: "user", parts: [{ text: systemPrompt }] }],
21480
+ const open = (model, history = [{ role: "user", parts: [{ text: systemPrompt }] }]) => this.getPlanningModel(model).startChat({
21481
+ history,
20882
21482
  tools: [{ functionDeclarations: TOOL_DEFINITIONS }],
20883
21483
  generationConfig: { temperature: 0.3, topP: 0.95, maxOutputTokens: 16384 }
20884
21484
  });
20885
- const researchChat = new GeminiResearchChat(chat, {
21485
+ const researchChat = new GeminiResearchChat(() => this.plannerModel(), open, {
20886
21486
  onUsage: (record) => onProgress({ type: "usage", record })
20887
21487
  });
20888
21488
  const result = await this.runResearchLoop(researchChat, firstMessage, fs18, onProgress, runners, void 0, fetcher, userDescription, runnerModes, autonomousDefault, signal);
@@ -20918,7 +21518,7 @@ import OpenAI from "openai";
20918
21518
  import { v4 as uuidv44 } from "uuid";
20919
21519
  var KEYLESS_PLACEHOLDER = "not-needed";
20920
21520
  var OpenAiResearchChat = class {
20921
- constructor(messages, getClient, getModel, tools, onReasoning, onContent, getSource = () => "openai", onUsage, contextWindow) {
21521
+ constructor(messages, getClient, getModel, tools, onReasoning, onContent, getSource = () => "openai", onUsage) {
20922
21522
  this.messages = messages;
20923
21523
  this.getClient = getClient;
20924
21524
  this.getModel = getModel;
@@ -20927,7 +21527,6 @@ var OpenAiResearchChat = class {
20927
21527
  this.onContent = onContent;
20928
21528
  this.getSource = getSource;
20929
21529
  this.onUsage = onUsage;
20930
- this.contextWindow = contextWindow;
20931
21530
  }
20932
21531
  messages;
20933
21532
  getClient;
@@ -20937,7 +21536,6 @@ var OpenAiResearchChat = class {
20937
21536
  onContent;
20938
21537
  getSource;
20939
21538
  onUsage;
20940
- contextWindow;
20941
21539
  async sendMessage(text, signal) {
20942
21540
  this.answerAbandonedCalls();
20943
21541
  this.messages.push({ role: "user", content: text });
@@ -21052,7 +21650,6 @@ var OpenAiResearchChat = class {
21052
21650
  record.reportedCost = { amount: reported.cost, currency: "USD" };
21053
21651
  hasMeasure = true;
21054
21652
  }
21055
- if (this.contextWindow && this.contextWindow > 0) record.contextWindow = this.contextWindow;
21056
21653
  return hasMeasure ? record : void 0;
21057
21654
  }
21058
21655
  };
@@ -21179,8 +21776,7 @@ ${buildResearchToolsPrompt()}` }
21179
21776
  (delta, segmentId) => currentProgress({ type: "thinking", text: delta, segmentId }),
21180
21777
  (delta, segmentId) => currentProgress({ type: "text_delta", text: delta, segmentId }),
21181
21778
  () => this.config.aiProvider,
21182
- (record) => currentProgress({ type: "usage", record }),
21183
- req.contextWindow
21779
+ (record) => currentProgress({ type: "usage", record })
21184
21780
  );
21185
21781
  const ctx = {
21186
21782
  chat,
@@ -21742,6 +22338,10 @@ var VerdictEngine = class {
21742
22338
  this.generations.clear();
21743
22339
  }
21744
22340
  };
22341
+ var EXIT_CHECK = "exit_code";
22342
+ function endedByExit(verdict) {
22343
+ return verdict.checks.some((check2) => check2.name === EXIT_CHECK);
22344
+ }
21745
22345
  function exitVerdict(exitCode) {
21746
22346
  const normalized2 = exitCode == null ? 0 : exitCode;
21747
22347
  const exitOk = normalized2 === 0;
@@ -21750,7 +22350,7 @@ function exitVerdict(exitCode) {
21750
22350
  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.`,
21751
22351
  checks: [
21752
22352
  { name: "task_complete", passed: false, skipped: false, detail: "the runner exited before calling task_complete" },
21753
- { name: "exit_code", passed: exitOk, skipped: false, detail: exitOk ? "the runner exited cleanly (code 0)" : `the runner exited with code ${normalized2}` }
22353
+ { name: EXIT_CHECK, passed: exitOk, skipped: false, detail: exitOk ? "the runner exited cleanly (code 0)" : `the runner exited with code ${normalized2}` }
21754
22354
  ],
21755
22355
  decidedAt: (/* @__PURE__ */ new Date()).toISOString()
21756
22356
  };
@@ -21949,6 +22549,7 @@ var PlanStore = class {
21949
22549
  this.rebuild();
21950
22550
  for (const task of this._allTasks) {
21951
22551
  if (task.status === "failed") task.status = "pending";
22552
+ if (task.runnerSessionId && !task.runnerSessionOwner) task.runnerSessionOwner = sessionOwner(task) ?? task.assignedRunner;
21952
22553
  }
21953
22554
  this.validateAssignedRunners();
21954
22555
  }
@@ -22078,9 +22679,17 @@ var PlanStore = class {
22078
22679
  const task = this._taskMap.get(id);
22079
22680
  if (task) task.outputSummary = summary;
22080
22681
  }
22081
- setTaskRunnerSessionId(id, runnerSessionId) {
22682
+ /** The owner is saved with the session it belongs to, and dropped with it. */
22683
+ setTaskRunnerSessionId(id, runnerSessionId, owner) {
22684
+ const task = this._taskMap.get(id);
22685
+ if (!task) return;
22686
+ task.runnerSessionId = runnerSessionId;
22687
+ if (runnerSessionId && owner) task.runnerSessionOwner = owner;
22688
+ else delete task.runnerSessionOwner;
22689
+ }
22690
+ setCarriedFrom(id, runner) {
22082
22691
  const task = this._taskMap.get(id);
22083
- if (task) task.runnerSessionId = runnerSessionId;
22692
+ if (task) task.carriedFrom = runner;
22084
22693
  }
22085
22694
  clearCarriedFrom(id) {
22086
22695
  const task = this._taskMap.get(id);
@@ -22094,9 +22703,9 @@ var PlanStore = class {
22094
22703
  const task = this._taskMap.get(id);
22095
22704
  if (task?.attemptUsage) task.attemptUsage = task.attemptUsage.map((usage) => usage.attempt === attempt ? addAttemptUsage(usage, record, now) : usage);
22096
22705
  }
22097
- endAttemptUsage(id, attempt, now) {
22706
+ endAttemptUsage(id, attempt, now, runnerSessionCost, sessionId) {
22098
22707
  const task = this._taskMap.get(id);
22099
- if (task?.attemptUsage) task.attemptUsage = task.attemptUsage.map((usage) => usage.attempt === attempt ? endAttemptUsage(usage, now) : usage);
22708
+ if (task?.attemptUsage) task.attemptUsage = task.attemptUsage.map((usage) => usage.attempt === attempt ? endAttemptUsage(usage, now, runnerSessionCost, sessionId) : usage);
22100
22709
  }
22101
22710
  setTaskAttemptSkills(id, skills) {
22102
22711
  const task = this._taskMap.get(id);
@@ -22954,6 +23563,8 @@ function conflictResolverTask(task, run, plan) {
22954
23563
  }
22955
23564
 
22956
23565
  // src/services/UsageLimitWaits.ts
23566
+ var RESUME_MARGIN_MS = 6e4;
23567
+ var RELAPSE_MS = 5 * 6e4;
22957
23568
  var UsageLimitWaits = class {
22958
23569
  constructor(onDue) {
22959
23570
  this.onDue = onDue;
@@ -23076,6 +23687,9 @@ function selectReadyTasks(input) {
23076
23687
  gated
23077
23688
  };
23078
23689
  }
23690
+ function isBlocked(task, store) {
23691
+ return blockedIn(task, blockingFailures(store.allTasks));
23692
+ }
23079
23693
  var blockedIn = (task, blocked) => task.status === "blocked" || blocked.has(task.id);
23080
23694
  function dependencyMet(depId, store, runs) {
23081
23695
  return waitingBehind([depId], store, runs).length === 0;
@@ -23205,6 +23819,12 @@ var TaskOrchestrator = class _TaskOrchestrator {
23205
23819
  * task whose spawn always throws.
23206
23820
  */
23207
23821
  onHold = /* @__PURE__ */ new Set();
23822
+ /**
23823
+ * Force starts queued behind dependencies the scheduler will not start them
23824
+ * after: outside a run it drives, or past a failed dependency, which it
23825
+ * never forces. Each starts once those still under way have finished.
23826
+ */
23827
+ queuedStarts = /* @__PURE__ */ new Set();
23208
23828
  /**
23209
23829
  * Tasks Mark complete is landing after it ended their attempt: in progress
23210
23830
  * with no attempt for as long as that takes, and not orphans for it.
@@ -23226,6 +23846,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
23226
23846
  observers = [];
23227
23847
  skillsAt;
23228
23848
  previousAttemptFromLog;
23849
+ lastLoggedAttempt;
23229
23850
  runSettings;
23230
23851
  isApproved;
23231
23852
  constructor(deps) {
@@ -23241,6 +23862,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
23241
23862
  this.workspaceEnv = deps.workspaceEnv;
23242
23863
  this.skillsAt = deps.skillsAt;
23243
23864
  this.previousAttemptFromLog = deps.previousAttemptFromLog;
23865
+ this.lastLoggedAttempt = deps.lastLoggedAttempt;
23244
23866
  this.runSettings = deps.runSettings;
23245
23867
  this.isApproved = deps.isApproved;
23246
23868
  this.store.onMutate = () => this.emit("onTaskChanged");
@@ -23305,6 +23927,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
23305
23927
  workspaceEnv: options.workspaceEnv ?? ((cwd) => resolveWorkspaceEnv(cwd)),
23306
23928
  skillsAt: options.skillsAt ?? ((roots) => new SkillsService(roots)),
23307
23929
  previousAttemptFromLog: options.previousAttemptFromLog ?? (() => null),
23930
+ lastLoggedAttempt: options.lastLoggedAttempt ?? (() => 0),
23308
23931
  runSettings: options.runSettings ?? (() => void 0),
23309
23932
  isApproved: options.isApproved ?? (() => true)
23310
23933
  });
@@ -23561,10 +24184,10 @@ var TaskOrchestrator = class _TaskOrchestrator {
23561
24184
  */
23562
24185
  holdStarts() {
23563
24186
  this.startHolds++;
23564
- let held = true;
24187
+ let held2 = true;
23565
24188
  return () => {
23566
- if (!held) return;
23567
- held = false;
24189
+ if (!held2) return;
24190
+ held2 = false;
23568
24191
  this.startHolds--;
23569
24192
  };
23570
24193
  }
@@ -23575,24 +24198,25 @@ var TaskOrchestrator = class _TaskOrchestrator {
23575
24198
  */
23576
24199
  admitStart() {
23577
24200
  const generation2 = this.stopGeneration;
23578
- let held = true;
24201
+ let held2 = true;
23579
24202
  this.admissions++;
23580
24203
  return {
23581
24204
  current: () => generation2 === this.stopGeneration,
23582
24205
  release: () => {
23583
- if (!held) return;
23584
- held = false;
24206
+ if (!held2) return;
24207
+ held2 = false;
23585
24208
  this.admissions--;
23586
24209
  }
23587
24210
  };
23588
24211
  }
23589
24212
  /**
23590
24213
  * Something can still start that nobody has to ask for again: a start
23591
- * admitted and not yet claimed, or a task that resumes on its own at its
23592
- * usage limit's reset. A Stop is owed to it, whether or not anything runs.
24214
+ * admitted and not yet claimed, a force start queued behind its
24215
+ * dependencies, or a task that resumes on its own at its usage limit's
24216
+ * reset. A Stop is owed to it, whether or not anything runs.
23593
24217
  */
23594
24218
  get startsPending() {
23595
- return this.admissions > 0 || this.store.allTasks.some(waitsForReset);
24219
+ return this.admissions > 0 || this.queuedStarts.size > 0 || this.store.allTasks.some(waitsForReset);
23596
24220
  }
23597
24221
  /** Run `work`, which awaits before it claims, under an admission it holds throughout; it is handed the generation it was admitted under. */
23598
24222
  async whileAdmitted(work) {
@@ -23691,6 +24315,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
23691
24315
  this.retryCounts.clear();
23692
24316
  this.spawnCounts.clear();
23693
24317
  this.onHold.clear();
24318
+ this.queuedStarts.clear();
23694
24319
  this.recoverOrphans();
23695
24320
  this.rearmLimitWaits();
23696
24321
  }
@@ -23778,6 +24403,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
23778
24403
  }
23779
24404
  this.runs.interrupt();
23780
24405
  this.onHold.clear();
24406
+ if (reason === "user") this.queuedStarts.clear();
23781
24407
  for (const t of this.store.allTasks.filter(waitsForReset)) this.cancelScheduledResume(t.id);
23782
24408
  this.sayStopped(stopped);
23783
24409
  this.recoverOrphans();
@@ -23877,6 +24503,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
23877
24503
  const attempt = this.attempts.get(taskId);
23878
24504
  if (!task || !attempt) return;
23879
24505
  attempt.decided = true;
24506
+ const stoppedMidTurn = endedByExit(verdict) ? saidInFlight(attempt) : null;
23880
24507
  console.error(`[TaskOrchestrator] Task ${taskRefOf(this.store.planTasks, task.id) ?? `#${task.order}`} "${task.title}" verdict=${verdict.outcome}`);
23881
24508
  console.error(`[TaskOrchestrator] Runner: ${task.assignedRunner}, Model: ${task.assignedModel?.modelId ?? "default"}`);
23882
24509
  console.error(`[TaskOrchestrator] Prompt preview: ${(task.prompt ?? "").slice(0, 200)}`);
@@ -23898,8 +24525,8 @@ ${summary || "(empty: no output captured)"}`);
23898
24525
  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.`);
23899
24526
  } else if (landing) {
23900
24527
  await this.applyLanding(task, landing, () => this.notifications.info(`Task "${task.title}" completed.`));
23901
- } else if (classifyRunnerStop(attempt.session?.getOutput() ?? "") === "usage-limit") {
23902
- this.pauseOnLimit(task, attempt, attempt.session?.getOutput() ?? "");
24528
+ } else if (stoppedMidTurn !== null && classifyRunnerStop(stoppedMidTurn) === "usage-limit") {
24529
+ this.pauseOnLimit(task, attempt, stoppedMidTurn);
23903
24530
  if (attempt.worktree) await this.runs.release(taskId, { keep: true });
23904
24531
  } else {
23905
24532
  this.store.markFailed(taskId);
@@ -23914,16 +24541,17 @@ ${summary || "(empty: no output captured)"}`);
23914
24541
  const status = this.store.get(taskId)?.status;
23915
24542
  if (status === "completed" || status === "failed" || status === "awaiting_user") this.emit("onTaskSettled", { taskId });
23916
24543
  this.emit("onTaskChanged");
23917
- if (!this.running) {
23918
- if (this.attempts.size === 0) {
23919
- if (this.store.isFinished()) this.planStatus = "completed";
23920
- this.emit("onTick");
23921
- if (this.settled) await this.closeRun();
23922
- this.emit("onExecutionComplete");
23923
- }
24544
+ if (this.running) {
24545
+ await this.tick();
23924
24546
  return;
23925
24547
  }
23926
- await this.tick();
24548
+ await this.startQueued();
24549
+ if (this.attempts.size === 0) {
24550
+ if (this.store.isFinished()) this.planStatus = "completed";
24551
+ this.emit("onTick");
24552
+ if (this.settled) await this.closeRun();
24553
+ this.emit("onExecutionComplete");
24554
+ }
23927
24555
  }
23928
24556
  /**
23929
24557
  * A repair's verdict never replaces the verdict the task's own work earned,
@@ -24191,11 +24819,17 @@ ${summary || "(empty: no output captured)"}`);
24191
24819
  const resumes = this.awaitsResume;
24192
24820
  this.retryCounts.set(taskId, (this.retryCounts.get(taskId) ?? 0) + 1);
24193
24821
  const ended = this.endAttempt(taskId, "retry");
24822
+ const movedFrom = this.movedFrom(task);
24194
24823
  this.store.retry(taskId);
24824
+ if (movedFrom) this.store.setCarriedFrom(taskId, movedFrom);
24195
24825
  this.store.unblockDependents(taskId);
24196
24826
  this.onHold.delete(taskId);
24197
24827
  this.emit("onTaskChanged");
24198
- await this.runs.release(taskId, { keep: false }, ended?.integration);
24828
+ if (movedFrom) {
24829
+ await ended?.integration;
24830
+ } else {
24831
+ await this.runs.release(taskId, { keep: false }, ended?.integration);
24832
+ }
24199
24833
  if (admittedAt !== this.stopGeneration) return;
24200
24834
  if (resumes) await this.start(origin);
24201
24835
  else await this.tick();
@@ -24246,6 +24880,16 @@ ${summary || "(empty: no output captured)"}`);
24246
24880
  this.emit("onTaskChanged");
24247
24881
  return true;
24248
24882
  }
24883
+ /**
24884
+ * The runner whose attempt left the task's work, when the task has been
24885
+ * moved off it since: a session cannot cross runners, so whatever starts
24886
+ * the task next is a carry-over of that work, the rule a paused task's move
24887
+ * follows (ADR-0018, U1).
24888
+ */
24889
+ movedFrom(task) {
24890
+ const owner = sessionOwner(task);
24891
+ return owner && owner !== this.store.resolveTaskRunner(task) ? owner : void 0;
24892
+ }
24249
24893
  /**
24250
24894
  * Run before writing assignments so cancellation sees the old runner's
24251
24895
  * shared wait. A session cannot cross runners, so the next attempt is a
@@ -24303,7 +24947,7 @@ ${summary || "(empty: no output captured)"}`);
24303
24947
  const summary = this.output.finalText(taskId);
24304
24948
  this.endAttempt(taskId, "usage-limit");
24305
24949
  this.pauseOnLimit(task, attempt, attempt.turnError ?? "");
24306
- this.store.setTaskOutputSummary(taskId, summarizeOutput(attempt.turnError ?? `${attempt.runner} hit its usage limit`, summary));
24950
+ this.store.setTaskOutputSummary(taskId, summarizeOutput(attempt.turnError?.message ?? `${attempt.runner} hit its usage limit`, summary));
24307
24951
  if (attempt.worktree) await this.runs.release(taskId, { keep: true });
24308
24952
  await this.afterVerdict(taskId);
24309
24953
  }
@@ -24402,10 +25046,14 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
24402
25046
  return false;
24403
25047
  }
24404
25048
  /**
24405
- * Manually start a single AI task right now, bypassing dependency/readiness
24406
- * gating (the "force start" affordance on a task card) but never the claim:
24407
- * it still needs the user's approval, a free slot, and no Stop since it was
24408
- * asked for, and throws {@link TaskControlError} saying which it lacks.
25049
+ * Manually start a single AI task right now (the "force start" affordance on
25050
+ * a task card), past a failed dependency (ADR-0034) or a merge gate
25051
+ * (ADR-0020), but never past the claim: it still needs the user's approval,
25052
+ * a free slot, and no Stop since it was asked for, and throws
25053
+ * {@link TaskControlError} saying which it lacks. A dependency still under
25054
+ * way or yet to run is not forced past: the task is released to the
25055
+ * scheduler instead, which starts it once they finish, and the line saying
25056
+ * so is the answer.
24409
25057
  * Reuses the scheduler's own claim so a force-started task gets the same
24410
25058
  * augmented prompt, session tracking, and exit handling — callers must not
24411
25059
  * re-spawn the runner themselves. No-op if the task is unknown, not an AI
@@ -24416,8 +25064,12 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
24416
25064
  if (!task || task.type !== "ai") return;
24417
25065
  if (this.attempts.has(taskId)) return;
24418
25066
  this.refuseOpsDuringMerge(task);
25067
+ const queued2 = this.queuedBehindDependencies(task);
25068
+ if (queued2) return queued2;
24419
25069
  await this.whileAdmitted(async (admittedAt) => {
24420
- if (!await this.openForAdmitted(taskId, origin, () => this.forceStartTask(taskId, origin), admittedAt)) return;
25070
+ if (!await this.openForAdmitted(taskId, origin, async () => {
25071
+ await this.forceStartTask(taskId, origin);
25072
+ }, admittedAt)) return;
24421
25073
  if (this.attempts.has(taskId)) return;
24422
25074
  this.refuseOpsDuringMerge(task);
24423
25075
  this.refuseClaim(task, admittedAt);
@@ -24426,6 +25078,68 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
24426
25078
  await this.claimed(task, admittedAt);
24427
25079
  });
24428
25080
  }
25081
+ /**
25082
+ * A force start of a task whose dependencies are still under way or yet to
25083
+ * run: started now, it would build on work that is not there, so it waits
25084
+ * for them in the run, off any hold, and says so. Dependencies are never
25085
+ * bypassed silently. A blocked task is forced past its failure as asked.
25086
+ */
25087
+ queuedBehindDependencies(task) {
25088
+ const waiting = this.underWayBefore(task);
25089
+ if (waiting.length === 0) return null;
25090
+ this.onHold.delete(task.id);
25091
+ if (!this.running || isBlocked(task, this.store)) this.queuedStarts.add(task.id);
25092
+ const names = waiting.map((id) => `task "${this.store.get(id)?.title ?? id}"`);
25093
+ const listed3 = names.length === 1 ? names[0] : `${names.slice(0, -1).join(", ")} and ${names.at(-1)}`;
25094
+ const idle = this.running || waiting.some((id) => this.attempts.has(id)) ? "" : " Nothing is running now: run the plan to start them.";
25095
+ const queued2 = `Task "${task.title}" is approved and starts once ${listed3} ${names.length === 1 ? "finishes" : "finish"}.${idle}`;
25096
+ this.tell("info", queued2);
25097
+ this.emit("onTaskChanged");
25098
+ return queued2;
25099
+ }
25100
+ /**
25101
+ * A task's unmet dependencies, less the failed or blocked ones a force start
25102
+ * goes past. A blocked one is gone past for its failure only: the work under
25103
+ * way or yet to run that it waits on is still waited for, through it.
25104
+ */
25105
+ underWayBefore(task) {
25106
+ const blocked = blockingFailures(this.store.allTasks);
25107
+ const waiting = /* @__PURE__ */ new Set();
25108
+ const seen = /* @__PURE__ */ new Set();
25109
+ const walk = (of) => {
25110
+ for (const id of unmetDependencies(of, this.store, this.runs)) {
25111
+ if (seen.has(id)) continue;
25112
+ seen.add(id);
25113
+ const dep = this.store.get(id);
25114
+ if (dep?.status === "failed") continue;
25115
+ if (dep && (dep.status === "blocked" || blocked.has(id))) walk(dep);
25116
+ else waiting.add(id);
25117
+ }
25118
+ };
25119
+ walk(task);
25120
+ return [...waiting];
25121
+ }
25122
+ /**
25123
+ * Starts the queued force starts whose dependencies have now finished. One
25124
+ * the scheduler now drives the run for, and nothing blocks, is left to it.
25125
+ */
25126
+ async startQueued() {
25127
+ for (const id of [...this.queuedStarts]) {
25128
+ const task = this.store.get(id);
25129
+ if (!task || this.attempts.has(id) || task.status === "completed" || task.status === "skipped") {
25130
+ this.queuedStarts.delete(id);
25131
+ continue;
25132
+ }
25133
+ if (this.underWayBefore(task).length > 0) continue;
25134
+ this.queuedStarts.delete(id);
25135
+ if (this.running && !isBlocked(task, this.store)) continue;
25136
+ try {
25137
+ await this.forceStartTask(id);
25138
+ } catch (err) {
25139
+ this.tell("warn", `Task "${task.title}" did not start once its dependencies finished: ${err instanceof Error ? err.message : String(err)}`);
25140
+ }
25141
+ }
25142
+ }
24429
25143
  refuseOpsDuringMerge(task) {
24430
25144
  if (this.merging && mergeExcludes(this.nextAttemptKind(task.id))) {
24431
25145
  throw new TaskControlError(`A Merge all is under way: ops task "${task.title}" can start once it has finished.`);
@@ -24490,6 +25204,7 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
24490
25204
  async tick() {
24491
25205
  const admittedAt = this.stopGeneration;
24492
25206
  this.recoverOrphans();
25207
+ await this.startQueued();
24493
25208
  if (!this.running) {
24494
25209
  if (this.attempts.size === 0 && this.runs.isOpen && this.settled) await this.closeRun();
24495
25210
  return;
@@ -24646,7 +25361,7 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
24646
25361
  if (refused3) return { refused: refused3 };
24647
25362
  const attempt = {
24648
25363
  taskId: task.id,
24649
- attempt: Math.max(this.spawnCounts.get(task.id) ?? 0, ...task.attemptUsage?.map((u) => u.attempt) ?? []) + 1,
25364
+ attempt: Math.max(this.spawnCounts.get(task.id) ?? 0, this.lastLoggedAttempt(task.id), ...task.attemptUsage?.map((u) => u.attempt) ?? []) + 1,
24650
25365
  phase: "starting",
24651
25366
  session: null,
24652
25367
  runner: this.store.resolveTaskRunner(task),
@@ -24660,6 +25375,7 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
24660
25375
  skills: [],
24661
25376
  autoResumed: opts.autoResumed === true,
24662
25377
  turnError: null,
25378
+ turnOutputFrom: 0,
24663
25379
  reminded: false
24664
25380
  };
24665
25381
  this.spawnCounts.set(task.id, attempt.attempt);
@@ -24733,6 +25449,8 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
24733
25449
  title: task.title,
24734
25450
  env,
24735
25451
  resumeSessionId: kind.kind === "continuation" ? kind.resumeSessionId : void 0,
25452
+ resumeCost: kind.kind === "continuation" ? countedSessionCost(task.attemptUsage, kind.resumeSessionId) : void 0,
25453
+ resumeWindow: kind.kind === "continuation" ? resumedSessionWindow(task.attemptUsage, attempt.runner, attempt.attempt, kind.resumeSessionId) : void 0,
24736
25454
  attempt: attempt.attempt,
24737
25455
  ...attempt.skills.length > 0 ? { skills: attempt.skills } : {},
24738
25456
  onNotice: (message) => this.tell("warn", `Task "${task.title}": ${message}`)
@@ -24757,8 +25475,21 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
24757
25475
  this.output.attach(task.id, session);
24758
25476
  this.verifier.watch(task, session);
24759
25477
  session.onEvent((event) => {
24760
- if (event.type === "turn_start") attempt.turnError = null;
24761
- else if (event.type === "error") attempt.turnError = event.message;
25478
+ if (event.type === "turn_start") {
25479
+ attempt.turnError = null;
25480
+ attempt.turnOutputFrom = session.getOutput().length;
25481
+ } else if (event.type === "error") attempt.turnError = event;
25482
+ else if (event.type === "model_switched" || event.type === "model_drifted") {
25483
+ this.emit("onModelSwitched", {
25484
+ taskId: task.id,
25485
+ sessionId: session.id,
25486
+ from: event.from,
25487
+ model: event.to,
25488
+ ...event.type === "model_switched" && event.effort ? { effort: event.effort } : {},
25489
+ asked: event.type === "model_switched",
25490
+ attemptEnded: this.attempts.get(task.id) !== attempt
25491
+ });
25492
+ }
24762
25493
  });
24763
25494
  session.onTurnEnd((reason) => this.onTurnEnd(task.id, attempt, session, reason));
24764
25495
  return true;
@@ -24768,6 +25499,16 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
24768
25499
  return false;
24769
25500
  }
24770
25501
  this.endAttempt(task.id, "spawn-failed");
25502
+ const refusal = err instanceof Error ? err.message : String(err);
25503
+ if (attempt.kind.kind !== "repair" && classifyRunnerStop(refusal) === "usage-limit") {
25504
+ this.pauseOnLimit(task, attempt, refusal);
25505
+ this.store.setTaskOutputSummary(task.id, summarizeOutput(refusal, ""));
25506
+ await this.runs.release(task.id, { keep: true });
25507
+ this.emit("onTaskSettled", { taskId: task.id });
25508
+ this.emit("onTaskChanged");
25509
+ await this.tick();
25510
+ return false;
25511
+ }
24771
25512
  if (attempt.kind.kind === "continuation") {
24772
25513
  const refused3 = err instanceof RunnerStartupExit;
24773
25514
  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)}`);
@@ -24887,12 +25628,13 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
24887
25628
  endAttempt(taskId, reason) {
24888
25629
  const attempt = this.attempts.get(taskId);
24889
25630
  this.attempts.delete(taskId);
25631
+ const session = attempt?.session ?? null;
25632
+ const nativeSessionId = session?.nativeSessionId() ?? null;
24890
25633
  if (attempt) {
24891
- this.store.endAttemptUsage(taskId, attempt.attempt, (/* @__PURE__ */ new Date()).toISOString());
25634
+ this.store.endAttemptUsage(taskId, attempt.attempt, (/* @__PURE__ */ new Date()).toISOString(), session?.sessionCost?.() ?? void 0, nativeSessionId ?? void 0);
24892
25635
  this.output.detach(taskId);
24893
25636
  }
24894
- const session = attempt?.session ?? null;
24895
- if (session) this.saveNativeSession(taskId, session.nativeSessionId());
25637
+ if (attempt && session) this.saveNativeSession(taskId, nativeSessionId, attempt.runner);
24896
25638
  if (stopsRunner(reason)) {
24897
25639
  const task = this.store.get(taskId);
24898
25640
  if (task) this.verifier.clear(task);
@@ -24901,8 +25643,8 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
24901
25643
  return attempt;
24902
25644
  }
24903
25645
  /** Kept on the task once the attempt ends, so a continue can resume it after a reload (ADR-0018, K1). */
24904
- saveNativeSession(taskId, nativeSessionId) {
24905
- if (nativeSessionId) this.store.setTaskRunnerSessionId(taskId, nativeSessionId);
25646
+ saveNativeSession(taskId, nativeSessionId, runner) {
25647
+ if (nativeSessionId) this.store.setTaskRunnerSessionId(taskId, nativeSessionId, runner);
24906
25648
  }
24907
25649
  endAllAttempts(reason) {
24908
25650
  const ended = [...this.attempts.values()];
@@ -24912,13 +25654,17 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
24912
25654
  return ended;
24913
25655
  }
24914
25656
  };
24915
- var RESUME_MARGIN_MS = 6e4;
24916
- var RELAPSE_MS = 5 * 6e4;
24917
25657
  var LIMIT_RESET_MESSAGE = "Your usage limit has reset. Carry on with the task from where you stopped.";
24918
25658
  var OPS_RETRY_TAIL_LINES = 60;
24919
25659
  function unresumedMessage(task, why) {
24920
25660
  return `Could not continue task "${task.title}": ${why}. Retry starts it afresh.`;
24921
25661
  }
25662
+ function saidInFlight(attempt) {
25663
+ const session = attempt.session;
25664
+ if (!session || session.turnState() !== "working") return null;
25665
+ if (attempt.turnError?.usageLimit !== void 0) return attempt.turnError;
25666
+ return session.getOutput().slice(attempt.turnOutputFrom);
25667
+ }
24922
25668
 
24923
25669
  // src/services/Planner.ts
24924
25670
  function plannerModesWith(req) {
@@ -25021,15 +25767,15 @@ var ModelResolver = class {
25021
25767
  return toOrchestratorOptions({}, ORCHESTRATOR_SHORTCUTS);
25022
25768
  }
25023
25769
  async modelsForRunners(runners) {
25024
- const out = {};
25025
- for (const runner of runners) {
25770
+ const unique = [...new Set(runners)];
25771
+ const discovered = await Promise.all(unique.map(async (runner) => {
25026
25772
  try {
25027
- out[runner] = await this.discovery.discover(runner);
25773
+ return [runner, await this.discovery.discover(runner)];
25028
25774
  } catch {
25029
- out[runner] = [];
25775
+ return [runner, []];
25030
25776
  }
25031
- }
25032
- return out;
25777
+ }));
25778
+ return Object.fromEntries(discovered);
25033
25779
  }
25034
25780
  async pickerOptions() {
25035
25781
  if (this.cachedPickerOptions) return this.cachedPickerOptions;
@@ -25076,21 +25822,18 @@ var ModelResolver = class {
25076
25822
  return this.cachedPickerOptions ?? [];
25077
25823
  }
25078
25824
  /**
25079
- * The planner model's context window, from whatever catalog is already
25080
- * cached (#49): a vendor model comes from the picker catalog, a harness
25081
- * planner's model from the runner's own discovered models. Reads only —
25082
- * never triggers a fetch or discovery of its own, so an unknown window stays
25083
- * unknown and context fill is omitted rather than guessed.
25825
+ * A model's context window from whatever catalog is already cached (#49),
25826
+ * for a usage record whose reporter stated none: a vendor's model from the
25827
+ * picker catalog (its own `/models` endpoint), a runner's model from that
25828
+ * runner's own discovered listing. `source` is the record's reporter, and
25829
+ * `model` the id it named. Reads only — never triggers a fetch or discovery
25830
+ * of its own, so an unknown window stays unknown and context fill is
25831
+ * omitted rather than guessed.
25084
25832
  */
25085
- contextWindowFor(modelId) {
25086
- if (!modelId) return void 0;
25087
- const picker = this.cachedPickerOptions?.find((o) => o.id === modelId)?.contextWindow;
25088
- if (picker && picker > 0) return picker;
25089
- for (const runner of this.config.enabledRunners ?? []) {
25090
- const found = this.discovery.getCached(runner)?.find((m) => m.modelId === modelId)?.contextWindow;
25091
- if (found && found > 0) return found;
25092
- }
25093
- return void 0;
25833
+ contextWindowFor(source, model) {
25834
+ if (!model) return void 0;
25835
+ 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;
25836
+ return window && window > 0 ? window : void 0;
25094
25837
  }
25095
25838
  invalidate() {
25096
25839
  this.discovery.clear();
@@ -25116,24 +25859,28 @@ var defaultExec3 = async (command, options) => {
25116
25859
  return { stdout: String(stdout) };
25117
25860
  };
25118
25861
  var CHECK_TIMEOUT_MS = 8e3;
25862
+ var NEGATIVE_TTL_MS2 = 6e4;
25119
25863
  var RunnerInstallation = class {
25120
25864
  cache = /* @__PURE__ */ new Map();
25121
25865
  inflight = /* @__PURE__ */ new Map();
25122
25866
  registry;
25123
25867
  execImpl;
25124
- constructor(registry, execImpl = defaultExec3) {
25868
+ now;
25869
+ constructor(registry, execImpl = defaultExec3, now = Date.now) {
25125
25870
  this.registry = registry;
25126
25871
  this.execImpl = execImpl;
25872
+ this.now = now;
25127
25873
  }
25128
25874
  /** True if the runner's CLI responds to `--version`. Cached per process. */
25129
25875
  async isInstalled(runner) {
25130
- if (this.cache.has(runner)) return this.cache.get(runner);
25876
+ const cached2 = this.cache.get(runner);
25877
+ if (cached2 && (cached2.installed || this.now() - cached2.at < NEGATIVE_TTL_MS2)) return cached2.installed;
25131
25878
  const existing = this.inflight.get(runner);
25132
25879
  if (existing) return existing;
25133
25880
  const manifest = this.registry.getManifest(runner);
25134
25881
  const command = manifest?.runner.command;
25135
25882
  if (!command) {
25136
- this.cache.set(runner, false);
25883
+ this.cache.set(runner, { installed: false, at: this.now() });
25137
25884
  return false;
25138
25885
  }
25139
25886
  const probe = (async () => {
@@ -25144,13 +25891,22 @@ var RunnerInstallation = class {
25144
25891
  return false;
25145
25892
  }
25146
25893
  })().then((installed) => {
25147
- this.cache.set(runner, installed);
25894
+ this.cache.set(runner, { installed, at: this.now() });
25148
25895
  this.inflight.delete(runner);
25149
25896
  return installed;
25150
25897
  });
25151
25898
  this.inflight.set(runner, probe);
25152
25899
  return probe;
25153
25900
  }
25901
+ /**
25902
+ * Forget cached "not installed" verdicts so the next probe re-runs them. A
25903
+ * runner-list refresh calls this, so a CLI installed while the host runs is
25904
+ * offered without a restart. An installed verdict stays: it cannot have
25905
+ * changed without the binary going away, and re-probing one costs a spawn.
25906
+ */
25907
+ refresh() {
25908
+ for (const [runner, probe] of this.cache) if (!probe.installed) this.cache.delete(runner);
25909
+ }
25154
25910
  /**
25155
25911
  * Whether this runner can serve as a harness planner (ADR-0009, T9), and why
25156
25912
  * not when it cannot. Surfaces call this to grey out an unusable agent in the
@@ -25581,6 +26337,8 @@ var PlannerSession = class {
25581
26337
  * outlives the session — nothing from one goal may reach the next.
25582
26338
  */
25583
26339
  nativeSessionId = null;
26340
+ /** What was counted of that session's running cost, so a resume counts on from it rather than dropping its first turn. */
26341
+ sessionCost = null;
25584
26342
  /** Set once the first spawn attached. */
25585
26343
  start = null;
25586
26344
  /**
@@ -25771,6 +26529,7 @@ var CliAgentAiService = class {
25771
26529
  // hit its rate limit, or lost its login must say so in the chat rather
25772
26530
  // than leave an empty planner bubble.
25773
26531
  failure: turn.error,
26532
+ usageLimit: turn.usageLimit,
25774
26533
  fullText: [...carried, turn.text].filter((part) => part.trim()).join("\n\n")
25775
26534
  };
25776
26535
  };
@@ -25837,6 +26596,7 @@ var CliAgentAiService = class {
25837
26596
  };
25838
26597
  const subagentUsage = /* @__PURE__ */ new Map();
25839
26598
  let error;
26599
+ let usageLimit;
25840
26600
  let stepIndex = 0;
25841
26601
  let backgroundAgents = 0;
25842
26602
  const truncate = (value) => value.length > LOG_MAX_CHARS ? `${value.slice(0, LOG_MAX_CHARS)}
@@ -25945,6 +26705,7 @@ var CliAgentAiService = class {
25945
26705
  }
25946
26706
  case "error":
25947
26707
  error = event.message;
26708
+ usageLimit = event.usageLimit;
25948
26709
  return;
25949
26710
  case "turn_end":
25950
26711
  return;
@@ -25963,12 +26724,13 @@ var CliAgentAiService = class {
25963
26724
  settle(id, "The agent ended the turn without reporting this call's result.", false, "not_executed");
25964
26725
  }
25965
26726
  session.nativeSessionId = adapter.nativeSessionId() ?? session.nativeSessionId;
26727
+ session.sessionCost = adapter.sessionCost?.() ?? session.sessionCost;
25966
26728
  if (error || signal?.aborted) session.stop();
25967
26729
  if (error) {
25968
26730
  const failure = failureAfterReply(text.slice(finalRunStart), Math.max(blockStart, finalRunStart) - finalRunStart, error);
25969
26731
  onProgress({ type: "text_retracted", segmentId: finalSegment });
25970
26732
  text = text.slice(0, finalRunStart);
25971
- return { text, researchLog, error: failure, aborted: signal?.aborted, backgroundAgents };
26733
+ return { text, researchLog, error: failure, usageLimit, aborted: signal?.aborted, backgroundAgents };
25972
26734
  }
25973
26735
  return { text, researchLog, error, aborted: signal?.aborted, backgroundAgents };
25974
26736
  }
@@ -26024,7 +26786,7 @@ var CliAgentAiService = class {
26024
26786
  session.stop();
26025
26787
  const start = session.start;
26026
26788
  if (!start) throw new Error("No active planner conversation.");
26027
- return this.startAdapter({ ...start.startOptions, resumeSessionId: session.nativeSessionId ?? void 0 }, start.tools, session);
26789
+ return this.startAdapter({ ...start.startOptions, resumeSessionId: session.nativeSessionId ?? void 0, resumeCost: session.sessionCost ?? void 0 }, start.tools, session);
26028
26790
  }
26029
26791
  /**
26030
26792
  * What the planner gets of the repository's config, its runner being
@@ -26036,9 +26798,9 @@ var CliAgentAiService = class {
26036
26798
  const reader = connectorFor(this.runner)?.repoConfig;
26037
26799
  if (!reader) return { systemPrompt };
26038
26800
  const found = reader.projectServers(cwd);
26039
- const { started, held } = await decideProjectServers(found.servers, { repoRoot: repoRoot(cwd), approval, approvals: this.projectServerApprovals });
26801
+ const { started, held: held2 } = await decideProjectServers(found.servers, { repoRoot: repoRoot(cwd), approval, approvals: this.projectServerApprovals });
26040
26802
  return {
26041
- systemPrompt: [systemPrompt, instructionsBlock(reader.instructions(cwd)), heldServersNote(held.map((server4) => ({ server: server4, reason: NOT_APPROVED })), found.unreadable)].filter(Boolean).join("\n\n"),
26803
+ systemPrompt: [systemPrompt, instructionsBlock(reader.instructions(cwd)), heldServersNote(held2.map((server4) => ({ server: server4, reason: NOT_APPROVED })), found.unreadable)].filter(Boolean).join("\n\n"),
26042
26804
  ...started.length > 0 ? { projectServers: started } : {}
26043
26805
  };
26044
26806
  }
@@ -26203,6 +26965,10 @@ var SessionCatalog = class {
26203
26965
  allowlist() {
26204
26966
  return this.deps.settings().modelAllowlist ?? {};
26205
26967
  }
26968
+ /** The enabled runners and allowlist together, for whoever compares an assignment with them. */
26969
+ scope() {
26970
+ return { enabledRunners: this.enabledRunners(), allowlist: this.allowlist(), models: this.models() };
26971
+ }
26206
26972
  /** The models the user allows on one runner; unset means no restriction. */
26207
26973
  allowlistFor(runner) {
26208
26974
  return this.allowlist()[runner];
@@ -26217,7 +26983,7 @@ var SessionCatalog = class {
26217
26983
  */
26218
26984
  models() {
26219
26985
  const out = { ...this.discovered };
26220
- for (const runner of /* @__PURE__ */ new Set([...Object.keys(out), ...this.deps.planRunners()])) {
26986
+ for (const runner of /* @__PURE__ */ new Set([...Object.keys(out), ...this.deps.planRunners(), ...this.enabledRunners()])) {
26221
26987
  const cached2 = this.deps.modelResolver.getCachedRunnerModels(runner);
26222
26988
  if (cached2.length > 0) out[runner] = cached2;
26223
26989
  }
@@ -26254,9 +27020,16 @@ var SessionCatalog = class {
26254
27020
  * asked for.
26255
27021
  */
26256
27022
  async live() {
26257
- const runners = this.enabledRunners();
26258
- await this.discover(runners);
26259
- return this.queryCatalog(runners);
27023
+ await this.discover(this.enabledRunners());
27024
+ return this.current();
27025
+ }
27026
+ /**
27027
+ * What {@link live} answers, from what is cached and without discovering:
27028
+ * the enabled runners, however the plan was started. The per-turn block
27029
+ * reads this, so it can never offer a different set than `list_runners`.
27030
+ */
27031
+ current() {
27032
+ return this.queryCatalog(this.enabledRunners());
26260
27033
  }
26261
27034
  queryCatalog(runners) {
26262
27035
  const registered = runners.filter((runner) => this.deps.registry.getManifest(runner) !== void 0);
@@ -26282,7 +27055,8 @@ var SessionCatalog = class {
26282
27055
  modelsByRunner: this.models(),
26283
27056
  runnerModes: this.modes(runners),
26284
27057
  perRunnerAllowlist: this.allowlist(),
26285
- autonomousDefault: this.deps.config.autonomousMode
27058
+ autonomousDefault: this.deps.config.autonomousMode,
27059
+ ...this.deps.liveRunner ? { liveRunner: this.deps.liveRunner } : {}
26286
27060
  };
26287
27061
  }
26288
27062
  /** What a runner offers, as `runnerAssignment` needs it. Spawns the runner's CLI to list models. */
@@ -26347,6 +27121,108 @@ function forkPlanState(plan, tasks, dialogue, now) {
26347
27121
  };
26348
27122
  }
26349
27123
 
27124
+ // src/services/PlannerResend.ts
27125
+ var HELD = "Send a message once it has reset: what waits goes with it.";
27126
+ var WAIT = "planner";
27127
+ var PlannerResend = class {
27128
+ constructor(host) {
27129
+ this.host = host;
27130
+ }
27131
+ host;
27132
+ waits = new UsageLimitWaits(() => this.due());
27133
+ /** When the last resend at a reset went out, until a turn lands: a limit in that window is a relapse. */
27134
+ resentAt = null;
27135
+ /** Whether turns wait on the limit: until the reset, or for the user's next message. */
27136
+ holds() {
27137
+ return this.host.plan()?.plannerResend !== void 0;
27138
+ }
27139
+ /** A planner turn Ordewell started ended on the planner's usage limit, which it reported as `said`. */
27140
+ limited(said2) {
27141
+ const plan = this.host.plan();
27142
+ const runner = this.host.plannerRunner() ?? void 0;
27143
+ const now = /* @__PURE__ */ new Date();
27144
+ const relapsed = this.resentAt !== null && now.getTime() - this.resentAt < RELAPSE_MS;
27145
+ this.resentAt = null;
27146
+ this.waits.clear();
27147
+ const name = runner ?? "Planner";
27148
+ const reset = runner && !relapsed ? readUsageLimitReset(runner, said2, now) : null;
27149
+ if (!reset) {
27150
+ if (plan) plan.plannerResend = runner ? { runner } : {};
27151
+ const held2 = relapsed ? "hit it again right after the resend" : "did not say when it resets";
27152
+ const text = 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}`;
27153
+ return { text, resendAt: null, held: held2 };
27154
+ }
27155
+ const at = new Date(reset.getTime() + RESUME_MARGIN_MS);
27156
+ if (plan) plan.plannerResend = { runner, at: at.toISOString() };
27157
+ this.waits.arm(WAIT, at);
27158
+ return { text: `${name} usage limit, resets ${resumeTime(reset, now)}. Resending at ${resumeTime(at, now)}; anything you send meanwhile goes with it.`, resendAt: at };
27159
+ }
27160
+ /**
27161
+ * A new input arrived — the user's message, or a wake-up the supervisor
27162
+ * asked for. What was held for one goes with it now; so does what waits on
27163
+ * a runner the planner has left, since nothing it waits for applies to the
27164
+ * planner that will answer. True when it lifted a wait.
27165
+ */
27166
+ arrived() {
27167
+ const saved = this.host.plan()?.plannerResend;
27168
+ if (!saved || saved.at !== void 0 && !this.switched(saved)) return false;
27169
+ this.lift();
27170
+ return true;
27171
+ }
27172
+ /** Keep what waits from going on its own; it goes with the user's next message. False when nothing waits on a reset. */
27173
+ cancel() {
27174
+ const plan = this.host.plan();
27175
+ const saved = plan?.plannerResend;
27176
+ if (!plan || saved?.at === void 0) return false;
27177
+ this.waits.clear();
27178
+ plan.plannerResend = held(saved);
27179
+ return true;
27180
+ }
27181
+ /** A turn landed: the resend before it got past the limit. */
27182
+ landed() {
27183
+ this.resentAt = null;
27184
+ }
27185
+ /** A plan adopted off disk: re-arm the wait it saved. A time already past resends at once. */
27186
+ restore() {
27187
+ this.reset();
27188
+ const saved = this.host.plan()?.plannerResend;
27189
+ if (saved?.at === void 0) return;
27190
+ const at = Date.parse(saved.at);
27191
+ if (Number.isNaN(at)) delete saved.at;
27192
+ else this.waits.arm(WAIT, new Date(at));
27193
+ }
27194
+ /** The plan is gone: nothing it waited on carries over. */
27195
+ reset() {
27196
+ this.waits.clear();
27197
+ this.resentAt = null;
27198
+ }
27199
+ due() {
27200
+ const plan = this.host.plan();
27201
+ const saved = plan?.plannerResend;
27202
+ if (!plan || saved?.at === void 0) return;
27203
+ if (this.switched(saved)) {
27204
+ plan.plannerResend = held(saved);
27205
+ this.host.changed();
27206
+ this.host.notice(`The planner no longer runs on ${saved.runner}, whose usage limit it waited on, so nothing was resent. ${HELD}`);
27207
+ return;
27208
+ }
27209
+ delete plan.plannerResend;
27210
+ this.resentAt = Date.now();
27211
+ this.host.due();
27212
+ }
27213
+ lift() {
27214
+ this.waits.clear();
27215
+ const plan = this.host.plan();
27216
+ if (plan) delete plan.plannerResend;
27217
+ }
27218
+ switched(saved) {
27219
+ return saved.runner !== void 0 && saved.runner !== (this.host.plannerRunner() ?? void 0);
27220
+ }
27221
+ };
27222
+ function held({ runner }) {
27223
+ return runner ? { runner } : {};
27224
+ }
27225
+
26350
27226
  // src/services/PlanTurns.ts
26351
27227
  var START_HOLD_TIMEOUT_MS = 5 * 6e4;
26352
27228
  function turnPrompt(batch) {
@@ -26363,6 +27239,16 @@ var PlanTurns = class {
26363
27239
  constructor(host, holdTimeoutMs = START_HOLD_TIMEOUT_MS) {
26364
27240
  this.host = host;
26365
27241
  this.holdTimeoutMs = holdTimeoutMs;
27242
+ this.resend = new PlannerResend({
27243
+ plan: () => host.plan(),
27244
+ plannerRunner: () => host.plannerRunner(),
27245
+ due: () => {
27246
+ host.announce();
27247
+ this.pump();
27248
+ },
27249
+ changed: () => host.announce(),
27250
+ notice: (message) => host.notice(message)
27251
+ });
26366
27252
  }
26367
27253
  host;
26368
27254
  holdTimeoutMs;
@@ -26375,6 +27261,33 @@ var PlanTurns = class {
26375
27261
  /** The user stopped the turn wake-ups waited behind: they wait for the next input to open one, not for the stop's release. */
26376
27262
  quiet = false;
26377
27263
  seq = 0;
27264
+ resend;
27265
+ /** A turn ended on the planner's usage limit: arm the wait for its inputs. Saved with the failure the turn records. */
27266
+ usageLimit(said2) {
27267
+ return this.resend.limited(said2);
27268
+ }
27269
+ /** Keep what waits on the planner's usage limit from being sent at the reset; see {@link PlannerResend.cancel}. */
27270
+ cancelResend() {
27271
+ if (!this.resend.cancel()) return false;
27272
+ this.host.announce();
27273
+ return true;
27274
+ }
27275
+ /**
27276
+ * The conversation's opening ended on the planner's usage limit: its goal
27277
+ * waits with the limit, ahead of what was sent behind it, whose senders are
27278
+ * answered now as any sent during the wait are.
27279
+ */
27280
+ waitOnLimit(input) {
27281
+ const plan = this.host.plan();
27282
+ if (!plan) return;
27283
+ plan.pendingTurns = [this.pending(input, false), ...plan.pendingTurns ?? []];
27284
+ this.host.persist();
27285
+ for (const t of plan.pendingTurns) if (t.source === "user") this.settle(t.id, (w) => w.resolve(plan));
27286
+ }
27287
+ /** A plan adopted off disk: the wait it saved is armed again. */
27288
+ restoreResend() {
27289
+ this.resend.restore();
27290
+ }
26378
27291
  /** Whether a turn this module opened is in flight. */
26379
27292
  get busy() {
26380
27293
  return this.running !== null;
@@ -26416,25 +27329,33 @@ var PlanTurns = class {
26416
27329
  admit(input, forced) {
26417
27330
  const plan = this.host.plan();
26418
27331
  if (!plan) return Promise.reject(new Error("No planning conversation to continue"));
26419
- const entry = {
26420
- id: `turn-${Date.now()}-${++this.seq}`,
26421
- source: input.source,
26422
- text: input.text,
26423
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
26424
- ...input.skills && input.skills.length > 0 ? { skills: [...input.skills] } : {},
26425
- ...forced ? { forced: true } : {}
26426
- };
27332
+ const entry = this.pending(input, forced);
26427
27333
  this.quiet = false;
27334
+ if (this.resend.arrived()) this.host.announce();
26428
27335
  const pending = plan.pendingTurns ?? [];
26429
27336
  const at = forced ? pending.findIndex((t) => !t.forced && !this.running?.ids.has(t.id)) : -1;
26430
27337
  plan.pendingTurns = at === -1 ? [...pending, entry] : [...pending.slice(0, at), entry, ...pending.slice(at)];
26431
27338
  this.host.persist();
27339
+ if (input.source === "user" && this.resend.holds()) {
27340
+ this.pump();
27341
+ return Promise.resolve(plan);
27342
+ }
26432
27343
  const settled = new Promise((resolve9, reject) => {
26433
27344
  this.waiters.set(entry.id, { resolve: resolve9, reject, signal: input.signal });
26434
27345
  });
26435
27346
  this.pump();
26436
27347
  return settled;
26437
27348
  }
27349
+ pending(input, forced) {
27350
+ return {
27351
+ id: `turn-${Date.now()}-${++this.seq}`,
27352
+ source: input.source,
27353
+ text: input.text,
27354
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
27355
+ ...input.skills && input.skills.length > 0 ? { skills: [...input.skills] } : {},
27356
+ ...forced ? { forced: true } : {}
27357
+ };
27358
+ }
26438
27359
  /**
26439
27360
  * Start the next turn if one is due: nothing of ours is in flight, the
26440
27361
  * conversation is free, and inputs wait. The host calls it when any planner
@@ -26445,7 +27366,7 @@ var PlanTurns = class {
26445
27366
  * the plan, what still waits, and the planner in use at that moment.
26446
27367
  */
26447
27368
  pump() {
26448
- if (this.running || this.detecting) return;
27369
+ if (this.running || this.detecting || this.resend.holds()) return;
26449
27370
  const plan = this.host.plan();
26450
27371
  if (!plan || this.host.conversationBusy()) return;
26451
27372
  const batch = this.liveBatch(plan);
@@ -26525,6 +27446,7 @@ var PlanTurns = class {
26525
27446
  * conversation's to abandon; whatever it settles lands nothing here.
26526
27447
  */
26527
27448
  abandon() {
27449
+ this.resend.reset();
26528
27450
  this.running?.release();
26529
27451
  this.running = null;
26530
27452
  this.quiet = false;
@@ -26568,8 +27490,10 @@ var PlanTurns = class {
26568
27490
  clearTimeout(timeout);
26569
27491
  if (this.running !== running) return;
26570
27492
  const forceStopped = !outcome.ok && running.forceStopped === true;
27493
+ const limited = !outcome.ok && outcome.err instanceof PlannerUsageLimitError;
27494
+ if (outcome.ok) this.resend.landed();
26571
27495
  try {
26572
- const dropped = outcome.ok || isInterruption(outcome.err) && !forceStopped ? ids : new Set(batch.filter((t) => t.source === "user").map((t) => t.id));
27496
+ 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));
26573
27497
  const took = this.take(plan, dropped);
26574
27498
  if (forceStopped) plan.pendingTurns = [...(plan.pendingTurns ?? []).filter((t) => t.forced), ...(plan.pendingTurns ?? []).filter((t) => !t.forced)];
26575
27499
  if (took || forceStopped) this.host.persist();
@@ -26583,7 +27507,11 @@ var PlanTurns = class {
26583
27507
  const ended = outcome;
26584
27508
  const waiting = new Set((plan.pendingTurns ?? []).map((t) => t.id));
26585
27509
  for (const t of batch) {
26586
- if (forceStopped && waiting.has(t.id)) continue;
27510
+ if (limited && t.source === "user") {
27511
+ this.settle(t.id, (w) => w.resolve(plan));
27512
+ continue;
27513
+ }
27514
+ if ((forceStopped || limited) && waiting.has(t.id)) continue;
26587
27515
  const delivered = this.settle(t.id, (w) => ended.ok ? w.resolve(ended.plan) : w.reject(ended.err));
26588
27516
  if (!delivered && !ended.ok && !isInterruption(ended.err) && t.source === "user") this.host.notice(replayFailed(t, ended.err));
26589
27517
  }
@@ -26642,7 +27570,6 @@ var RunSupervision = class {
26642
27570
  blockedBy: (failedId) => [...blockingFailures([...this.host.tasks()])].filter(([, failures]) => failures.includes(failedId)).map(([taskId]) => taskId),
26643
27571
  readBudget: () => this.settings()?.wakeUpReadBudget ?? host.userSettings().wakeUpReadBudget,
26644
27572
  retryLimit: () => this.retryLimit(),
26645
- plannerRunner: () => host.plannerRunner(),
26646
27573
  restartPlanner: () => host.restartPlanner(),
26647
27574
  abandonTurn: () => host.abandonTurn(),
26648
27575
  goUnsupervised: (reason) => this.goUnsupervised(reason),
@@ -27123,8 +28050,8 @@ function dropLegacyFile(sessionsDir, meta) {
27123
28050
  const legacy = path30.join(sessionsDir, legacySessionFilename(meta));
27124
28051
  if (legacy === path30.join(sessionsDir, sessionFilename(meta)) || !fs14.existsSync(legacy)) return;
27125
28052
  try {
27126
- const held = JSON.parse(fs14.readFileSync(legacy, "utf-8")).meta?.id;
27127
- if (held === meta.id) fs14.unlinkSync(legacy);
28053
+ const held2 = JSON.parse(fs14.readFileSync(legacy, "utf-8")).meta?.id;
28054
+ if (held2 === meta.id) fs14.unlinkSync(legacy);
27128
28055
  } catch {
27129
28056
  }
27130
28057
  }
@@ -27257,24 +28184,25 @@ function listTaskLogAttempts(location, taskId) {
27257
28184
  }
27258
28185
  return names.map((name) => /^([1-9]\d*)\.jsonl$/.exec(name)?.[1]).filter((n) => n !== void 0).map(Number).sort((a, b) => a - b);
27259
28186
  }
27260
- function openTaskLog(location, taskId) {
28187
+ function openTaskLog(location, taskId, attempt) {
27261
28188
  ensureStateDirIgnored(location.baseDir);
27262
28189
  const dir = taskDir(location, taskId);
27263
28190
  fs15.mkdirSync(dir, { recursive: true });
27264
- let attempt = (listTaskLogAttempts(location, taskId).pop() ?? 0) + 1;
28191
+ const next = (listTaskLogAttempts(location, taskId).pop() ?? 0) + 1;
28192
+ let number = attempt !== void 0 && Number.isInteger(attempt) && attempt >= 1 ? attempt : next;
27265
28193
  for (; ; ) {
27266
- const file = path31.join(dir, `${attempt}${EXTENSION}`);
28194
+ const file = path31.join(dir, `${number}${EXTENSION}`);
27267
28195
  try {
27268
28196
  fs15.writeFileSync(file, "", { flag: "wx" });
27269
28197
  } catch (err) {
27270
28198
  if (err.code === "EEXIST") {
27271
- attempt += 1;
28199
+ number = Math.max(number + 1, next);
27272
28200
  continue;
27273
28201
  }
27274
28202
  throw err;
27275
28203
  }
27276
28204
  return {
27277
- attempt,
28205
+ attempt: number,
27278
28206
  append(events) {
27279
28207
  if (events.length === 0) return;
27280
28208
  fs15.appendFileSync(file, events.map((e) => `${JSON.stringify(e)}
@@ -27320,11 +28248,13 @@ var TaskLogRecorder = class {
27320
28248
  open;
27321
28249
  flushMs;
27322
28250
  logger;
28251
+ /** Each task's latest attempt, by the session it records: its log takes a {@link note} until the next attempt starts. */
28252
+ latest = /* @__PURE__ */ new Map();
27323
28253
  wrap(runner) {
27324
28254
  return {
27325
28255
  spawn: async (opts) => {
27326
28256
  const session = await runner.spawn(opts);
27327
- this.record(opts.taskId, session, opts.skills);
28257
+ this.record(opts.taskId, session, opts.skills, opts.attempt);
27328
28258
  return session;
27329
28259
  },
27330
28260
  stop: (sessionId) => runner.stop(sessionId),
@@ -27334,11 +28264,20 @@ var TaskLogRecorder = class {
27334
28264
  }
27335
28265
  };
27336
28266
  }
28267
+ /**
28268
+ * An entry the session did not emit, written into the log of the attempt
28269
+ * that runs `sessionId` and sent at once: what Ordewell made of one of its
28270
+ * events once that settled. Dropped once the task's next attempt started.
28271
+ */
28272
+ note(sessionId, taskId, entry) {
28273
+ const attempt = this.latest.get(taskId);
28274
+ if (attempt?.sessionId === sessionId) attempt.note(entry);
28275
+ }
27337
28276
  /** Start a new attempt's log for `session`, opening with the skills it was given, and keep it until the session exits. */
27338
- record(taskId, session, skills = []) {
28277
+ record(taskId, session, skills = [], attemptNumber) {
27339
28278
  let file;
27340
28279
  try {
27341
- file = this.open(this.deps.location(), taskId);
28280
+ file = this.open(this.deps.location(), taskId, attemptNumber);
27342
28281
  } catch (err) {
27343
28282
  this.logger.warn("taskLog", `could not create the log of task ${taskId}; it streams but is not saved`, err);
27344
28283
  file = null;
@@ -27365,6 +28304,10 @@ var TaskLogRecorder = class {
27365
28304
  }
27366
28305
  this.deps.broadcast({ type: "task_log", taskId, attempt, events: taskLogForSurface(events) });
27367
28306
  };
28307
+ this.latest.set(taskId, { sessionId: session.id, note: (entry) => {
28308
+ pending.push(entry);
28309
+ flush();
28310
+ } });
27368
28311
  const openCalls = /* @__PURE__ */ new Set();
27369
28312
  session.onEvent((event) => {
27370
28313
  const entry = toTaskLogEvent(event);
@@ -27385,21 +28328,82 @@ var TaskLogRecorder = class {
27385
28328
  }
27386
28329
  };
27387
28330
 
28331
+ // src/services/ContextWindowFill.ts
28332
+ var ContextWindowFill = class {
28333
+ constructor(catalog, last = {}) {
28334
+ this.catalog = catalog;
28335
+ if (last.contextWindow !== void 0) this.known.set(modelKey(last.source, last.model), last.contextWindow);
28336
+ }
28337
+ catalog;
28338
+ /** The window known for each model of the stream, by reporter and model. */
28339
+ known = /* @__PURE__ */ new Map();
28340
+ fill(record) {
28341
+ if (record.subagentId || record.aggregate) return record;
28342
+ const key = modelKey(record.source, record.model);
28343
+ const reported = record.contextWindow !== void 0 && record.contextWindow > 0 ? record.contextWindow : void 0;
28344
+ const window = reported ?? this.known.get(key) ?? (record.model ? positive(this.catalog(record.source, record.model)) : void 0);
28345
+ if (window !== void 0) this.known.set(key, window);
28346
+ return window === void 0 || window === record.contextWindow ? record : { ...record, contextWindow: window };
28347
+ }
28348
+ };
28349
+ function modelKey(source, model) {
28350
+ return JSON.stringify([source ?? null, model ?? null]);
28351
+ }
28352
+ function withContextWindows(runner, catalog) {
28353
+ return {
28354
+ spawn: async (opts) => windowedSession(await runner.spawn(opts), new ContextWindowFill(catalog, opts.resumeWindow)),
28355
+ stop: (sessionId) => runner.stop(sessionId),
28356
+ stopAll: () => runner.stopAll(),
28357
+ get activeCount() {
28358
+ return runner.activeCount;
28359
+ }
28360
+ };
28361
+ }
28362
+ function windowedSession(session, windows) {
28363
+ const listeners = [];
28364
+ session.onEvent((event) => {
28365
+ const filled = event.type === "usage" ? { ...event, record: windows.fill(event.record) } : event;
28366
+ for (const listener of listeners) listener(filled);
28367
+ });
28368
+ const onEvent = (listener) => {
28369
+ listeners.push(listener);
28370
+ };
28371
+ return new Proxy(session, {
28372
+ get: (target, prop) => {
28373
+ if (prop === "onEvent") return onEvent;
28374
+ const value = Reflect.get(target, prop, target);
28375
+ return typeof value === "function" ? value.bind(target) : value;
28376
+ }
28377
+ });
28378
+ }
28379
+ function positive(n) {
28380
+ return n !== void 0 && n > 0 ? n : void 0;
28381
+ }
28382
+
27388
28383
  // src/services/PlannerUsage.ts
27389
28384
  var PlannerUsageLedger = class {
28385
+ /** `catalogWindow` serves the window of a planner model whose backend reports none. */
28386
+ constructor(catalogWindow = () => void 0) {
28387
+ this.catalogWindow = catalogWindow;
28388
+ this.windows = new ContextWindowFill(catalogWindow);
28389
+ }
28390
+ catalogWindow;
27390
28391
  usage = { totals: {} };
28392
+ windows;
27391
28393
  /** Fold one model call's record into the totals; returns the running ledger. */
27392
28394
  record(record) {
27393
- this.usage = addPlannerUsage(this.usage, record);
28395
+ this.usage = addPlannerUsage(this.usage, this.windows.fill(record));
27394
28396
  return this.usage;
27395
28397
  }
27396
28398
  /** Adopt a persisted ledger (a reopened session) or start from zero. */
27397
28399
  restore(usage) {
27398
28400
  this.usage = usage ?? { totals: {} };
28401
+ this.windows = new ContextWindowFill(this.catalogWindow, this.usage);
27399
28402
  }
27400
28403
  /** Start a fresh session's ledger from zero. */
27401
28404
  clear() {
27402
28405
  this.usage = { totals: {} };
28406
+ this.windows = new ContextWindowFill(this.catalogWindow);
27403
28407
  }
27404
28408
  /** Whether anything has been recorded — a plan with no usage says nothing. */
27405
28409
  get hasUsage() {
@@ -27494,19 +28498,54 @@ var PlanEditor = class {
27494
28498
  * between AI and MAN, are the patches that can leave a task incoherent
27495
28499
  * (unschedulable, or carrying fields that mean nothing for its new type),
27496
28500
  * so both go through the same {@link validateTaskEdit} guard the planner's
27497
- * task-ops applier uses (as the 'direct' actor, which skips the lock rule)
27498
- * rather than a second copy of the rules — and throws, so the surface can
27499
- * say why. Gated on the task existing so an edit to an unknown id still
28501
+ * task-ops applier uses (as the 'direct' actor, which skips the lock rule
28502
+ * but not a live runner's hold on its model) rather than a second copy of
28503
+ * the rules — and throws, so the surface can say why. Gated on the task existing so an edit to an unknown id still
27500
28504
  * falls through to the no-op `store.update` below instead of throwing.
27501
28505
  * Attached skills meet the rule a planner's do: a planner skill is refused,
27502
- * a name not found yet only warns.
28506
+ * a name not found yet only warns. A runner change is made by
28507
+ * {@link setTaskRunner}, which re-derives what the runner scopes, ahead of
28508
+ * the rest of the patch.
27503
28509
  *
27504
28510
  * Edits to one task commit in call order: the skill check waits on git, so
27505
28511
  * two quick chip edits (`[a]` then `[a, b]`) could otherwise land the first
27506
28512
  * one last.
27507
28513
  */
27508
28514
  updateTask(taskId, changes) {
27509
- const edit = (this.taskEdits.get(taskId) ?? Promise.resolve()).then(() => this.applyTaskUpdate(taskId, changes));
28515
+ return this.inTaskOrder(taskId, () => this.applyTaskUpdate(taskId, changes));
28516
+ }
28517
+ /**
28518
+ * The plan follows a model switch the user made with the runner's own
28519
+ * command (#106), as a direct edit of theirs: saved and announced as
28520
+ * `task_updated`, so every surface shows it, and what a later retry or
28521
+ * continue spawns on. It is the one model edit a live runner allows, since
28522
+ * it says what that runner already runs, and the one that skips the
28523
+ * allowlist: the user picked it in the runner. The runner's own label is
28524
+ * used only for a model discovery does not list. Effort is the runner's
28525
+ * when it reported one, else the plan's, either clamped to what the new
28526
+ * model offers; none for a model discovery does not list. `live` is asked
28527
+ * as the edit lands, after the edits queued ahead of it: by then the plan
28528
+ * may be another one, whose task of the same id the switch says nothing of.
28529
+ */
28530
+ followRunnerModel(taskId, model, effort, live) {
28531
+ return this.inTaskOrder(taskId, async () => {
28532
+ const task = this.store.get(taskId);
28533
+ if (!task || task.type === "user" || !live()) return null;
28534
+ const listed3 = this.catalog.models()[task.assignedRunner]?.find((m) => m.modelId === model.model);
28535
+ const thinkingEffort = clampThinkingEffort(effort ?? task.assignedModel?.thinkingEffort, listed3?.variants ?? []);
28536
+ if (task.assignedModel?.modelId === model.model && task.assignedModel.thinkingEffort === thinkingEffort) return this.plan();
28537
+ const assignedModel = {
28538
+ modelId: model.model,
28539
+ modelLabel: listed3?.modelLabel ?? model.label ?? model.model,
28540
+ ...thinkingEffort ? { thinkingEffort } : {},
28541
+ ...listed3 ? { availableVariants: listed3.variants.map((v) => v.id) } : {}
28542
+ };
28543
+ return this.commitTaskUpdate(taskId, { assignedModel });
28544
+ });
28545
+ }
28546
+ /** Edits to one task commit in call order; see {@link updateTask}. */
28547
+ inTaskOrder(taskId, apply) {
28548
+ const edit = (this.taskEdits.get(taskId) ?? Promise.resolve()).then(apply);
27510
28549
  const settled = edit.catch(() => {
27511
28550
  });
27512
28551
  this.taskEdits.set(taskId, settled);
@@ -27522,22 +28561,31 @@ var PlanEditor = class {
27522
28561
  const target = this.store.get(taskId);
27523
28562
  const skills = "skills" in changes && target ? await checkTaskSkillsEdit(target, typedSkills, this.taskSkills()) : void 0;
27524
28563
  if (skills?.errors.length) throw new PlanEditError(skills.errors.map((e) => e.message).join(" "));
27525
- if ((changes.dependencies || changes.type || changes.assignedModel || changes.taskMode || "ops" in changes) && target) {
28564
+ if ((changes.dependencies || changes.type || "assignedModel" in changes || "assignedRunner" in changes || changes.taskMode || "ops" in changes) && target) {
27526
28565
  const check2 = validateTaskEdit("direct", this.store.planTasks, taskId, changes, this.catalog.edit());
27527
28566
  if (!check2.ok) throw new PlanEditError(check2.error ?? "Those changes are not valid");
27528
28567
  if (check2.clear?.length || check2.set) {
27529
28568
  changes = { ...changes, ...Object.fromEntries((check2.clear ?? []).map((f) => [f, void 0])), ...check2.set };
27530
28569
  }
27531
28570
  }
27532
- const plan = await this.commit(
28571
+ const { assignedRunner, ...rest } = changes;
28572
+ if (target && assignedRunner && assignedRunner !== target.assignedRunner) {
28573
+ const moved = await this.setTaskRunner(taskId, assignedRunner);
28574
+ if (!moved || Object.keys(rest).length === 0) return moved;
28575
+ changes = rest;
28576
+ }
28577
+ const plan = await this.commitTaskUpdate(taskId, changes);
28578
+ if (plan) for (const warning of skills?.warnings ?? []) this.notice("warn", warning);
28579
+ return plan;
28580
+ }
28581
+ commitTaskUpdate(taskId, changes) {
28582
+ return this.commit(
27533
28583
  () => {
27534
28584
  changes = this.scheduler.releaseMovedResume(taskId, changes);
27535
28585
  return Boolean(this.store.update(taskId, changes));
27536
28586
  },
27537
28587
  () => this.broadcast({ type: "task_updated", taskId, changes })
27538
28588
  );
27539
- if (plan) for (const warning of skills?.warnings ?? []) this.notice("warn", warning);
27540
- return plan;
27541
28589
  }
27542
28590
  /**
27543
28591
  * Replace one task's dependency list — the named entry point the surfaces'
@@ -27554,7 +28602,8 @@ var PlanEditor = class {
27554
28602
  * because a runner change is never a single-field edit: the task's model,
27555
28603
  * thinking effort and mode are all scoped to its runner, so they are
27556
28604
  * re-derived from the new runner's catalog (see {@link retargetTaskRunner}).
27557
- * That needs discovery, which is why it is not a branch inside `updateTask`.
28605
+ * That needs discovery, which is why `updateTask` hands a runner change here
28606
+ * rather than writing it as a field.
27558
28607
  *
27559
28608
  * The runner is also admitted into `plan.runners` — see {@link admitRunner}.
27560
28609
  */
@@ -27564,11 +28613,13 @@ var PlanEditor = class {
27564
28613
  const task = this.store.get(taskId);
27565
28614
  if (!task) return null;
27566
28615
  if (task.assignedRunner === runner || task.type === "user") return plan;
28616
+ this.refuseLiveRetarget(task);
27567
28617
  const catalog = await this.catalog.runner(runner);
27568
28618
  const from = this.catalog.edit([task.assignedRunner]).runnerModes[task.assignedRunner];
27569
28619
  const changes = retargetTaskRunner(task, runner, this.allowedCatalog(catalog, runner), from);
27570
28620
  if (Object.keys(changes).length === 0) return this.plan();
27571
28621
  this.refuseUnavailableMode(runner, changes.taskMode, catalog.modes);
28622
+ this.refuseLiveRetarget(task);
27572
28623
  return this.commit(() => {
27573
28624
  if (!this.store.update(taskId, this.scheduler.releaseMovedResume(taskId, changes))) return false;
27574
28625
  this.admitRunner(runner, catalog.models);
@@ -27602,6 +28653,9 @@ var PlanEditor = class {
27602
28653
  return true;
27603
28654
  });
27604
28655
  }
28656
+ refuseLiveRetarget(task) {
28657
+ if (this.catalog.edit().liveRunner?.(task.id)) throw new PlanEditError(liveRunnerRefusal(task));
28658
+ }
27605
28659
  /**
27606
28660
  * A mode the host cannot run is refused, with its reason, not swapped for
27607
28661
  * another: the plan is the source of truth for what runs (ADR-0001), and a
@@ -27931,6 +28985,10 @@ var ExecutionCommands = class {
27931
28985
  const all = this.all(tasks);
27932
28986
  const live = all.filter(({ task }) => this.host.running(task.id));
27933
28987
  if (this.host.hasLiveWork()) {
28988
+ if (actor.kind === "user") {
28989
+ const waiting = all.filter(({ task }) => this.host.waitsForApproval(task.id)).length;
28990
+ return { act: async () => ({ message: await this.host.joinRun(waiting) }) };
28991
+ }
27934
28992
  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.` };
27935
28993
  }
27936
28994
  const toRun = all.filter(({ task }) => task.type === "ai" && task.status !== "completed" && task.status !== "skipped");
@@ -28356,8 +29414,11 @@ function mergeStopped(result) {
28356
29414
  switch (result.outcome) {
28357
29415
  case "merged":
28358
29416
  return "it merged";
28359
- case "blocked":
28360
- return "the user's checkout has changes that overlap what it would merge";
29417
+ case "blocked": {
29418
+ const off = result.blocked.filter((b) => b.reason === "wrong-branch");
29419
+ if (off.length === 0) return "the user's checkout has changes that overlap what it would merge";
29420
+ 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("; ");
29421
+ }
28361
29422
  case "conflict":
28362
29423
  return `it conflicts in ${result.repo}${result.files?.length ? `: ${result.files.slice(0, 5).join(", ")}` : ""}`;
28363
29424
  case "failed":
@@ -28482,11 +29543,17 @@ function liveAiService(config, workspaceRoot, mcpServer) {
28482
29543
  function isPlannerApproval(request) {
28483
29544
  return !isRunnerApproval(request);
28484
29545
  }
29546
+ function writtenTasks(before, after) {
29547
+ const said2 = ({ subtasks: _subtasks, status: _status, ...task }) => JSON.stringify(task);
29548
+ const prior = new Map(flattenTasks(before).map((t) => [t.id, said2(t)]));
29549
+ return flattenTasks(after).filter((t) => prior.get(t.id) !== said2(t)).map((t) => t.id);
29550
+ }
28485
29551
  function createSession(deps) {
28486
29552
  const pinnedAiService = deps.aiService;
28487
29553
  const mcpServer = deps.mcpServer ?? sharedMcpServer();
28488
29554
  const aiService = pinnedAiService ? () => pinnedAiService : liveAiService(deps.config, deps.workspaceRoot, mcpServer);
28489
29555
  const store = new PlanStore();
29556
+ const catalogWindow = (source, model) => deps.modelResolver.contextWindowFor?.(source, model);
28490
29557
  const taskLogs = new TaskLogRecorder({
28491
29558
  broadcast: deps.broadcast,
28492
29559
  // Read through the Session, whose id and workspace are the ones it saves
@@ -28520,7 +29587,7 @@ function createSession(deps) {
28520
29587
  const orchestrator = TaskOrchestrator.compose({
28521
29588
  config: deps.config,
28522
29589
  notifications: deps.notifications,
28523
- runner: taskLogs.wrap(runnerApprovals.wrap(deps.runner)),
29590
+ runner: taskLogs.wrap(runnerApprovals.wrap(withContextWindows(deps.runner, catalogWindow))),
28524
29591
  store,
28525
29592
  output: deps.taskOutput,
28526
29593
  isolation: deps.isolation,
@@ -28528,10 +29595,11 @@ function createSession(deps) {
28528
29595
  workspaceRoot: deps.workspaceRoot,
28529
29596
  skillsAt: (roots) => (deps.skillsService ?? new SkillsService(roots)).forRoot(roots),
28530
29597
  previousAttemptFromLog: (taskId) => lastAttemptDigest(session.taskLogLocation, taskId),
29598
+ lastLoggedAttempt: (taskId) => session.taskLogAttempts(taskId).at(-1) ?? 0,
28531
29599
  runSettings: () => session.planState?.runSettings,
28532
29600
  isApproved: (taskId) => session.isApproved(taskId)
28533
29601
  });
28534
- const usage = new PlannerUsageLedger();
29602
+ const usage = new PlannerUsageLedger(catalogWindow);
28535
29603
  const events = new SessionEventRelay({
28536
29604
  broadcast: deps.broadcast,
28537
29605
  onNotice: deps.onNotice,
@@ -28580,7 +29648,8 @@ function createSession(deps) {
28580
29648
  modelResolver: deps.modelResolver,
28581
29649
  settings: deps.settings,
28582
29650
  // Read through the Session, which holds the plan; the catalog is built first.
28583
- planRunners: () => session.planState?.runners ?? []
29651
+ planRunners: () => session.planState?.runners ?? [],
29652
+ liveRunner: (taskId) => orchestrator.getAttempt(taskId) !== void 0
28584
29653
  });
28585
29654
  const session = new Session({
28586
29655
  config: deps.config,
@@ -28602,6 +29671,7 @@ function createSession(deps) {
28602
29671
  usage,
28603
29672
  approvals,
28604
29673
  approvalPolicy,
29674
+ taskLogs,
28605
29675
  // Every planner's web reads, carried here or natively, are decided by the
28606
29676
  // one policy, which asks through the same channel as paths and commands.
28607
29677
  // The response cache and search provider are process-wide, so parallel
@@ -28652,6 +29722,7 @@ var Session = class {
28652
29722
  fsAdapter;
28653
29723
  approvals;
28654
29724
  approvalPolicy;
29725
+ taskLogs;
28655
29726
  fetcher;
28656
29727
  settingsFn;
28657
29728
  save;
@@ -28687,7 +29758,7 @@ var Session = class {
28687
29758
  turnId: () => this.conversation.currentTurnId,
28688
29759
  recordSkillLoad: (skill) => this.conversation.recordPlannerSkill(skill),
28689
29760
  liveCatalog: () => this.liveCatalog(),
28690
- coerce: (tasks) => coerceAssignments(tasks, this.catalog.allowlist(), this.catalog.models()),
29761
+ coerce: (tasks) => coerceAssignments(tasks, this.catalog.allowlist(), this.catalog.models(), this.store.planTasks),
28691
29762
  submitPlan: (tasks) => this.submitPlanFromTool(tasks),
28692
29763
  editPlan: (ops) => this.editPlanFromTool(ops),
28693
29764
  tasks: () => this.store.planTasks,
@@ -28732,6 +29803,7 @@ var Session = class {
28732
29803
  this.usage = parts.usage;
28733
29804
  this.approvals = parts.approvals;
28734
29805
  this.approvalPolicy = parts.approvalPolicy;
29806
+ this.taskLogs = parts.taskLogs;
28735
29807
  this.fetcher = parts.fetcher;
28736
29808
  this.skillsService = parts.skillsService;
28737
29809
  this.save = parts.saveSession;
@@ -28741,7 +29813,8 @@ var Session = class {
28741
29813
  aiService: () => this.aiService(),
28742
29814
  onProgress: (p) => this.events.progress(p),
28743
29815
  opening: (runners) => this.conversationOpening(runners),
28744
- catalog: () => this.catalog.queryCatalog(this.plan?.runners ?? []),
29816
+ catalog: () => this.catalog.current(),
29817
+ assignmentScope: () => this.catalog.scope(),
28745
29818
  autorun: () => this.userRunSettings().autorun,
28746
29819
  takeRunUpdates: () => this.untoldRunUpdates.splice(0),
28747
29820
  tasks: () => this.store.planTasks,
@@ -28761,13 +29834,18 @@ var Session = class {
28761
29834
  turnAborted: () => this.approvals.clear(isPlannerApproval),
28762
29835
  // After the turn has announced its end, so the next one never starts
28763
29836
  // ahead of it on a surface.
28764
- turnReleased: () => queueMicrotask(() => this.turns.pump())
29837
+ turnReleased: () => queueMicrotask(() => this.turns.pump()),
29838
+ usageLimit: (said2) => this.turns.usageLimit(said2)
28765
29839
  });
28766
29840
  this.turns = new PlanTurns({
28767
29841
  plan: () => this.closed ? null : this.plan,
28768
29842
  conversationBusy: () => this.conversation.isTurnInFlight,
28769
29843
  mayWake: () => this.supervision.mayWake(),
28770
29844
  persist: () => this.persist({ background: true }),
29845
+ announce: () => {
29846
+ this.persist({ background: true });
29847
+ this.events.planGenerated(this.plan, this.goal);
29848
+ },
28771
29849
  interrupt: () => {
28772
29850
  this.conversation.stopTurn("force_send");
28773
29851
  },
@@ -28784,7 +29862,8 @@ var Session = class {
28784
29862
  },
28785
29863
  notice: (message) => this.notice("error", message),
28786
29864
  plannerDetecting: () => installedPlannerAnswered() ? null : installedPlannerDetected(),
28787
- assertPlannerReady: () => this.assertPlannerReady()
29865
+ assertPlannerReady: () => this.assertPlannerReady(),
29866
+ plannerRunner: () => runnerForProvider(this.config.aiProvider)
28788
29867
  });
28789
29868
  this.supervision = new RunSupervision({
28790
29869
  plan: () => this.plan,
@@ -28802,7 +29881,6 @@ var Session = class {
28802
29881
  },
28803
29882
  deliver: (digest) => this.turns.enqueue({ source: "supervisor", text: digest }),
28804
29883
  pump: () => this.turns.pump(),
28805
- plannerRunner: () => runnerForProvider(this.config.aiProvider),
28806
29884
  restartPlanner: () => this.aiService().restart?.(),
28807
29885
  abandonTurn: () => this.conversation.abandonHungTurn(),
28808
29886
  notice: (level, message) => this.notice(level, message)
@@ -28884,6 +29962,7 @@ var Session = class {
28884
29962
  tasks: () => this.plan ? this.store.planTasks : null,
28885
29963
  planApproved: () => this.plan?.approved === true,
28886
29964
  taskApproved: (taskId) => this.isApproved(taskId),
29965
+ waitsForApproval: (taskId) => this.waitsForApproval(taskId),
28887
29966
  approve: (taskIds) => this.approve(taskIds),
28888
29967
  autonomous: () => this.config.autonomousMode,
28889
29968
  autorun: () => this.userRunSettings().autorun,
@@ -28905,6 +29984,7 @@ var Session = class {
28905
29984
  blockedBy: (taskId) => this.orchestrator.getBlockedBy(taskId),
28906
29985
  waitingRequests: (taskId) => this.approvals.outstanding().filter(({ request }) => isRunnerApproval(request) && request.taskId === taskId).map(({ id, request }) => ({ id, subject: request.subject })),
28907
29986
  runPlan: (actor) => this.runWholePlan(runOrigin(actor)),
29987
+ joinRun: (approved) => this.joinRun(approved),
28908
29988
  startTask: (taskId, actor) => this.startOneTask(taskId, runOrigin(actor)),
28909
29989
  stopTask: (taskId) => this.cancelTask(taskId),
28910
29990
  stopRun: () => this.stopExecution(),
@@ -28933,8 +30013,8 @@ var Session = class {
28933
30013
  * one: nothing it would start is approved until the user executes it.
28934
30014
  */
28935
30015
  heldEdit(taskId) {
28936
- const held = this.conversation.pendingOps();
28937
- if (!held || held.length === 0 || !this.plan) return false;
30016
+ const held2 = this.conversation.pendingOps();
30017
+ if (!held2 || held2.length === 0 || !this.plan) return false;
28938
30018
  if (taskId === void 0) return true;
28939
30019
  const before = this.store.get(taskId);
28940
30020
  const after = this.asEdited(taskId);
@@ -28947,10 +30027,10 @@ var Session = class {
28947
30027
  /** {@link editedTasks}, with the id each of the held batch's handles names in them. */
28948
30028
  editedPlan() {
28949
30029
  const unedited = { tasks: this.store.planTasks, handles: /* @__PURE__ */ new Map() };
28950
- const held = this.conversation.pendingOps();
28951
- if (!held || held.length === 0 || !this.plan) return unedited;
30030
+ const held2 = this.conversation.pendingOps();
30031
+ if (!held2 || held2.length === 0 || !this.plan) return unedited;
28952
30032
  const runners = this.editRunners();
28953
- const result = applyTaskOps(this.store.planTasks, held, runners, this.catalog.edit(runners));
30033
+ const result = applyTaskOps(this.store.planTasks, held2, runners, this.catalog.edit(runners));
28954
30034
  return result.ok ? { tasks: result.tasks, handles: result.handles ?? /* @__PURE__ */ new Map() } : unedited;
28955
30035
  }
28956
30036
  /** The task as the open turn's held edit leaves it; undefined when that edit removes it. */
@@ -28974,9 +30054,9 @@ var Session = class {
28974
30054
  * rules, then, for the supervisor, the oscillation guard. `held` are the
28975
30055
  * ops the turn already holds, which `ops` join; errors are numbered by `ops`.
28976
30056
  */
28977
- checkPlannerOps(ops, held = []) {
30057
+ checkPlannerOps(ops, held2 = []) {
28978
30058
  const runners = this.editRunners();
28979
- const result = applyTaskOps(this.store.planTasks, ops, runners, this.catalog.edit(runners), held);
30059
+ const result = applyTaskOps(this.store.planTasks, ops, runners, this.catalog.edit(runners), held2);
28980
30060
  if (!result.ok || !this.supervisorEditing()) return result;
28981
30061
  const trip = this.supervision.checkEdit(this.store.planTasks, result.tasks);
28982
30062
  if (!trip) return result;
@@ -29146,6 +30226,9 @@ var Session = class {
29146
30226
  onUsageLimitWait: ({ taskId, runner, resumeAt, cancelled }) => {
29147
30227
  if (!cancelled) this.supervision.observe({ kind: "usage_limit", taskId, runner, resumeAt });
29148
30228
  },
30229
+ onModelSwitched: (switched) => {
30230
+ void this.followRunnerModel(switched);
30231
+ },
29149
30232
  onTaskIdle: ({ taskId, idleSince }) => {
29150
30233
  this.supervision.observe({ kind: "task_idle", taskId, detail: `no output since ${idleSince}` });
29151
30234
  },
@@ -29266,10 +30349,12 @@ var Session = class {
29266
30349
  * boundary rewrites `in_progress` to `pending` — nothing is running when a
29267
30350
  * session comes off a file — so a surface that re-reads the plan mid-run must
29268
30351
  * come here instead, or every task it is watching reads as never started.
30352
+ * An archived snapshot carries the task as it is now: a retry or continue
30353
+ * keeps the archive while the live task runs on and spends more.
29269
30354
  */
29270
30355
  get currentPlanState() {
29271
30356
  if (!this.plan) return null;
29272
- const executionLog = this.store.getExecutionLog();
30357
+ const executionLog = this.store.getExecutionLog().map((s) => ({ ...s, ...this.store.get(s.id) }));
29273
30358
  const logged = new Set(executionLog.map((s) => s.id));
29274
30359
  const pendingTasks = this.store.planTasks.filter((t) => !logged.has(t.id));
29275
30360
  const record = approvalRecordOf(this.plan);
@@ -29280,7 +30365,7 @@ var Session = class {
29280
30365
  phase: "executing",
29281
30366
  history: [],
29282
30367
  message: "",
29283
- executionLog: [...executionLog],
30368
+ executionLog,
29284
30369
  pendingTasks,
29285
30370
  goal: this.goal,
29286
30371
  runners: this.plan.runners,
@@ -29450,16 +30535,20 @@ var Session = class {
29450
30535
  opened = this.openTurn(() => this.conversation.start(this.resolveSkillInvocation(goal), () => this.conversationOpening(chosenRunners), options?.signal));
29451
30536
  } catch (err) {
29452
30537
  if (this.opening === pending) this.opening = null;
29453
- for (const held2 of pending.held ?? []) held2.reject(err);
30538
+ for (const held3 of pending.held ?? []) held3.reject(err);
29454
30539
  throw err;
29455
30540
  }
29456
- const held = pending.held ?? [];
30541
+ const held2 = pending.held ?? [];
29457
30542
  pending.held = null;
29458
- for (const h of held) this.turns.follow(h.input, h.forced).then(h.resolve, h.reject);
30543
+ for (const h of held2) this.turns.follow(h.input, h.forced).then(h.resolve, h.reject);
29459
30544
  try {
29460
30545
  const { landed: landed2, effects } = await opened;
29461
30546
  this.autorunSubmitted(effects);
29462
30547
  return landed2;
30548
+ } catch (err) {
30549
+ if (!(err instanceof PlannerUsageLimitError) || !this.plan) throw err;
30550
+ this.turns.waitOnLimit({ source: "user", ...this.resolveSkillInvocation(goal) });
30551
+ return this.plan;
29463
30552
  } finally {
29464
30553
  if (this.opening === pending) this.opening = null;
29465
30554
  }
@@ -29474,10 +30563,10 @@ var Session = class {
29474
30563
  behindOpening(input, forced = false) {
29475
30564
  const pending = this.opening;
29476
30565
  if (!pending) return null;
29477
- const held = pending.held;
29478
- if (!held) return this.turns.follow(input, forced);
30566
+ const held2 = pending.held;
30567
+ if (!held2) return this.turns.follow(input, forced);
29479
30568
  return new Promise((resolve9, reject) => {
29480
- held.push({ input, forced, resolve: resolve9, reject });
30569
+ held2.push({ input, forced, resolve: resolve9, reject });
29481
30570
  });
29482
30571
  }
29483
30572
  /**
@@ -29576,12 +30665,12 @@ var Session = class {
29576
30665
  await this.liveCatalog();
29577
30666
  if (this.conversation.currentTurnId !== turn) return refuse(PLANNER_TURN_ENDED);
29578
30667
  if (!this.plan || this.store.planTasks.length === 0) return refuse("There is no plan to edit yet: submit one with submit_plan.");
29579
- const held = this.conversation.pendingOps();
29580
- 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.");
29581
- const result = this.checkPlannerOps(ops, held);
30668
+ const held2 = this.conversation.pendingOps();
30669
+ 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.");
30670
+ const result = this.checkPlannerOps(ops, held2);
29582
30671
  if (!result.ok) return { ok: false, errors: result.errors };
29583
- if (!this.conversation.submit({ kind: "task_ops", ops: [...held, ...ops] })) return refuse("No planning turn is open to take the edit.");
29584
- const { coerced } = coerceAssignments(result.tasks, this.catalog.allowlist(), this.catalog.models());
30672
+ if (!this.conversation.submit({ kind: "task_ops", ops: [...held2, ...ops] })) return refuse("No planning turn is open to take the edit.");
30673
+ const { coerced } = coerceAssignments(result.tasks, this.catalog.allowlist(), this.catalog.models(), this.store.planTasks);
29585
30674
  return { ok: true, summary: result.summary, coerced: coerced.map(({ taskId: _id, ...rest }) => rest) };
29586
30675
  }
29587
30676
  /** Whether the planner conversation is live (started and not yet committed to a plan). */
@@ -29618,9 +30707,6 @@ var Session = class {
29618
30707
  autonomousDefault: this.config.autonomousMode,
29619
30708
  autorun: this.userRunSettings().autorun,
29620
30709
  isolatedExecution: await this.runs.plannerLayout(),
29621
- // The planner's own model window, when a cached catalog knows it, so the
29622
- // usage line can show context fill (#49). Unknown stays absent.
29623
- contextWindow: this.modelResolver.contextWindowFor?.(this.config.orchestratorModel),
29624
30710
  fs: this.fsAdapter,
29625
30711
  fetcher: this.fetcher,
29626
30712
  approval: this.approvalPolicy,
@@ -29647,7 +30733,7 @@ var Session = class {
29647
30733
  for (const runner of runnersOf(tasks)) this.editor.admitRunner(runner, this.catalog.known(runner));
29648
30734
  const runners = this.plan.runners;
29649
30735
  const before = this.store.planTasks;
29650
- let coerced = coerceAssignments(tasks, this.catalog.allowlist(), this.catalog.models()).tasks;
30736
+ let coerced = coerceAssignments(tasks, this.catalog.allowlist(), this.catalog.models(), before).tasks;
29651
30737
  if (how === "commit") coerced = keepExecutionState(before, carryPlannedModes(before, coerced, this.catalog.edit(runners)));
29652
30738
  const bySupervisor = this.supervisorEditing();
29653
30739
  const { tasks: stamped, added } = stampOrigin(before, coerced, bySupervisor);
@@ -29660,6 +30746,7 @@ var Session = class {
29660
30746
  this.store.resetForRun({ keepScheduledResumes: true });
29661
30747
  }
29662
30748
  if (bySupervisor) this.approve(added);
30749
+ else if (this.conversation.currentWakeUp === void 0 && this.userRunSettings().autorun) this.approve(writtenTasks(before, coerced));
29663
30750
  return coerced.length;
29664
30751
  }
29665
30752
  preparePlannerTaskUpdates(tasks) {
@@ -29694,6 +30781,11 @@ var Session = class {
29694
30781
  if (changes.mergeAtGatesAutomatically) this.orchestrator.runSettingsChanged();
29695
30782
  }
29696
30783
  async executePlan() {
30784
+ if (this.orchestrator.hasLiveWork) {
30785
+ const outcome = await this.execute({ command: "run_plan" }, { kind: "user" });
30786
+ if (!outcome.ok) throw new TaskControlError(outcome.message);
30787
+ return;
30788
+ }
29697
30789
  this.idlePlan();
29698
30790
  this.approve(flattenTasks(this.store.planTasks).map((t) => t.id));
29699
30791
  await this.runWholePlan("user");
@@ -29715,6 +30807,25 @@ var Session = class {
29715
30807
  await this.orchestrator.approveReview(origin);
29716
30808
  });
29717
30809
  }
30810
+ /**
30811
+ * Execute while a run is under way (#95): the seam has approved every task,
30812
+ * as executing does, and the run takes them in. A run the scheduler was not
30813
+ * driving, one task started on its own, becomes the plan's run, as Execute
30814
+ * would make it.
30815
+ */
30816
+ async joinRun(approved) {
30817
+ const plan = this.plan;
30818
+ const scheduling = this.orchestrator.isScheduling;
30819
+ await this.withSave(async () => {
30820
+ plan.approved = true;
30821
+ if (scheduling) await this.orchestrator.tick();
30822
+ else if (this.orchestrator.isReviewApproved) await this.orchestrator.start("user");
30823
+ else await this.orchestrator.approveReview("user");
30824
+ });
30825
+ 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.`;
30826
+ this.notice("info", line);
30827
+ return line;
30828
+ }
29718
30829
  async approveReview() {
29719
30830
  if (!this.plan) throw new NoPlanError("review");
29720
30831
  const plan = this.plan;
@@ -29723,9 +30834,12 @@ var Session = class {
29723
30834
  await this.withSave(() => this.orchestrator.approveReview("user"));
29724
30835
  return plan;
29725
30836
  }
30837
+ /** Answers the line saying the task waits for its dependencies, when it was queued behind them rather than started. */
29726
30838
  async forceStartTask(taskId) {
29727
30839
  this.approveTask(taskId);
29728
- await this.startOneTask(taskId, "user");
30840
+ if (!this.plan) return;
30841
+ this.requireTask(taskId);
30842
+ return this.withSave(() => this.orchestrator.forceStartTask(taskId, "user"));
29729
30843
  }
29730
30844
  async startOneTask(taskId, origin) {
29731
30845
  if (!this.plan) return;
@@ -29782,10 +30896,51 @@ var Session = class {
29782
30896
  async resumeNow(taskId) {
29783
30897
  await this.withSave(() => this.orchestrator.resumeNow(taskId, "user"));
29784
30898
  }
30899
+ /**
30900
+ * Keep what waits on the planner's usage limit from being sent at the reset:
30901
+ * it goes with the user's next message instead. False when nothing waits on
30902
+ * a reset.
30903
+ */
30904
+ cancelPlannerResend() {
30905
+ return this.turns.cancelResend();
30906
+ }
29785
30907
  /** Keep a task paused on a usage limit from resuming on its own; see {@link TaskOrchestrator.cancelScheduledResume}. */
29786
30908
  cancelScheduledResume(taskId) {
29787
30909
  return this.saved(() => this.orchestrator.cancelScheduledResume(taskId));
29788
30910
  }
30911
+ /**
30912
+ * The plan follows a model switch the user made in the runner (#106), and
30913
+ * the task's log says what the plan holds once that settled: a write-back
30914
+ * not made — no model command asked for the change, or the edit failed — is
30915
+ * stated, never shown as made. Once the switch's attempt has ended, the plan
30916
+ * may be another one, so nothing of it is said.
30917
+ */
30918
+ async followRunnerModel({ taskId, sessionId, from, model, effort, asked, attemptEnded }) {
30919
+ let kept = asked ? void 0 : "no model command in the chat asked for it";
30920
+ const live = () => this.orchestrator.activeSessionMap.get(taskId) === sessionId;
30921
+ const ended = () => this.taskLogs.note(sessionId, taskId, { type: "model_switched", from, to: model, kept: kept ?? "its attempt had already ended" });
30922
+ if (attemptEnded) return ended();
30923
+ if (asked) {
30924
+ try {
30925
+ if (!await this.editor.followRunnerModel(taskId, model, effort, live) && !live()) return ended();
30926
+ } catch (err) {
30927
+ kept = `the plan could not follow it: ${err instanceof Error ? err.message : String(err)}`;
30928
+ 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)}`);
30929
+ }
30930
+ }
30931
+ const assigned = this.store.get(taskId)?.assignedModel;
30932
+ if (!kept && assigned?.modelId !== model.model) kept = "the plan did not take the edit";
30933
+ this.taskLogs.note(sessionId, taskId, {
30934
+ type: "model_switched",
30935
+ from,
30936
+ to: model,
30937
+ plan: {
30938
+ ...assigned ? { model: { model: assigned.modelId, label: assigned.modelLabel } } : {},
30939
+ ...assigned?.thinkingEffort ? { effort: assigned.thinkingEffort } : {}
30940
+ },
30941
+ ...kept ? { kept } : {}
30942
+ });
30943
+ }
29789
30944
  /** Said through both channels: a host shows one or the other, never both (the daemon's toasts are no-ops). */
29790
30945
  notice(level, message) {
29791
30946
  this.notifications[level](message);
@@ -29857,8 +31012,8 @@ var Session = class {
29857
31012
  if (!this.plan) return EMPTY_RUN;
29858
31013
  const planned = reduceRun(EMPTY_RUN, { type: "plan_snapshot", plan: surfacePlan({ ...this.plan, tasks: this.store.snapshot() }) });
29859
31014
  const view = reduceRun(planned, this.events.statusMessage());
29860
- const held = this.sentHeld ? reduceRun(view, { type: "plan_edit_held", held: this.sentHeld }) : view;
29861
- return this.sentHandoff ? reduceRun(held, { type: "isolation_handoff", ...this.sentHandoff }) : held;
31015
+ const held2 = this.sentHeld ? reduceRun(view, { type: "plan_edit_held", held: this.sentHeld }) : view;
31016
+ return this.sentHandoff ? reduceRun(held2, { type: "isolation_handoff", ...this.sentHandoff }) : held2;
29862
31017
  }
29863
31018
  /** The ancestors a task waits on at its merge gate (ADR-0020); empty when it waits for no Merge all. */
29864
31019
  mergeGate(taskId) {
@@ -30017,6 +31172,7 @@ var Session = class {
30017
31172
  if (adopting) this.dropPlanScope();
30018
31173
  this.plan = plan;
30019
31174
  if (adopting) this.supervision.restore(plan.supervision);
31175
+ if (adopting) this.turns.restoreResend();
30020
31176
  this.goal = goal;
30021
31177
  this.workspace = workspace;
30022
31178
  plan.approved ??= plan.status !== "draft" && plan.status !== "rejected";
@@ -30224,6 +31380,7 @@ var StructuredSession = class extends AbstractRunnerSession {
30224
31380
  messageCount = 0;
30225
31381
  interrupting = null;
30226
31382
  lastSessionId = null;
31383
+ lastSessionCost = null;
30227
31384
  structuredEmitter = new EventEmitter2();
30228
31385
  permissionCount = 0;
30229
31386
  /**
@@ -30428,6 +31585,9 @@ var StructuredSession = class extends AbstractRunnerSession {
30428
31585
  nativeSessionId() {
30429
31586
  return this.adapter?.nativeSessionId() ?? this.lastSessionId;
30430
31587
  }
31588
+ sessionCost() {
31589
+ return this.adapter?.sessionCost?.() ?? this.lastSessionCost;
31590
+ }
30431
31591
  answerPermission(id, decision) {
30432
31592
  const open = this.permissions.get(id);
30433
31593
  if (!open) return false;
@@ -30475,6 +31635,7 @@ var StructuredSession = class extends AbstractRunnerSession {
30475
31635
  */
30476
31636
  async restartInterrupted(turn) {
30477
31637
  const resumeSessionId = this.nativeSessionId() ?? this.startOptions.resumeSessionId;
31638
+ const resumeCost = this.sessionCost() ?? this.startOptions.resumeCost;
30478
31639
  this.generation += 1;
30479
31640
  turn.abort.abort();
30480
31641
  if (this.adapter) {
@@ -30484,7 +31645,7 @@ var StructuredSession = class extends AbstractRunnerSession {
30484
31645
  for (const message of this.queue) this.requeue(message);
30485
31646
  try {
30486
31647
  this.revokeTools();
30487
- await this.startAttached({ ...this.startOptions, resumeSessionId });
31648
+ await this.startAttached({ ...this.startOptions, resumeSessionId, resumeCost });
30488
31649
  } catch (err) {
30489
31650
  this.text.line(`Could not restart ${this.launch.runner} after the interrupt: ${err instanceof Error ? err.message : String(err)}`);
30490
31651
  this.withdrawPermissions();
@@ -30511,6 +31672,7 @@ var StructuredSession = class extends AbstractRunnerSession {
30511
31672
  return;
30512
31673
  }
30513
31674
  this.lastSessionId = adapter.nativeSessionId() ?? this.lastSessionId;
31675
+ this.lastSessionCost = adapter.sessionCost?.() ?? this.lastSessionCost;
30514
31676
  this.withdrawPermissions();
30515
31677
  this.baseHandleExit(code);
30516
31678
  });
@@ -30548,6 +31710,7 @@ var StructuredSession = class extends AbstractRunnerSession {
30548
31710
  this.handleEvent({ type: "error", message: err instanceof Error ? err.message : String(err) });
30549
31711
  }).then(() => {
30550
31712
  this.lastSessionId = adapter.nativeSessionId() ?? this.lastSessionId;
31713
+ this.lastSessionCost = adapter.sessionCost?.() ?? this.lastSessionCost;
30551
31714
  if (generation2 === this.generation) this.endTurn(turn, turn.reason);
30552
31715
  });
30553
31716
  this.offerNext();
@@ -30674,6 +31837,11 @@ var StructuredSession = class extends AbstractRunnerSession {
30674
31837
  case "error":
30675
31838
  this.text.failure(event.message);
30676
31839
  break;
31840
+ // The plan follows the switch (#106), so a restart after an interrupt
31841
+ // runs what the plan now says rather than what this attempt started on.
31842
+ case "model_switched":
31843
+ this.startOptions = { ...this.startOptions, model: event.to.model };
31844
+ break;
30677
31845
  default:
30678
31846
  break;
30679
31847
  }
@@ -30754,7 +31922,8 @@ var StructuredRunner = class extends AbstractRunner {
30754
31922
  model: opts.modelId,
30755
31923
  mode: opts.mode ?? "default",
30756
31924
  flags: resolveTaskRunnerFlags(manifest, { mode: opts.mode ?? "default", model: opts.modelId, thinkingEffort: opts.thinkingEffort }),
30757
- resumeSessionId: opts.resumeSessionId
31925
+ resumeSessionId: opts.resumeSessionId,
31926
+ resumeCost: opts.resumeCost
30758
31927
  },
30759
31928
  interruptGraceMs: this.interruptGraceMs,
30760
31929
  tools: { server: this.mcp, scope: { sessionId: opts.planSessionId ?? "", taskId: opts.taskId, attempt: opts.attempt ?? 1 } },
@@ -31056,6 +32225,7 @@ export {
31056
32225
  PlannerNotReadyError,
31057
32226
  PlannerTurnDiscardedError,
31058
32227
  PlannerTurnStoppedError,
32228
+ PlannerUsageLimitError,
31059
32229
  REFUSED_COMMANDS,
31060
32230
  RUN_SETTINGS_DEFAULTS,
31061
32231
  RunSettingsError,
@@ -31112,6 +32282,8 @@ export {
31112
32282
  approvalScopes,
31113
32283
  assertWorkspaceExists,
31114
32284
  assertWorkspaceIsProject,
32285
+ assignmentIssue,
32286
+ assignmentIssueText,
31115
32287
  augmentedPath,
31116
32288
  automergeSummary,
31117
32289
  autonomyLevelLabel,
@@ -31128,6 +32300,7 @@ export {
31128
32300
  canSetDependencies,
31129
32301
  canSplitTask,
31130
32302
  capConflictFiles,
32303
+ checkoutRule,
31131
32304
  classifyCommand,
31132
32305
  classifyOutcome,
31133
32306
  clearAugmentedPathCache,
@@ -31141,6 +32314,7 @@ export {
31141
32314
  commandAllowedBy,
31142
32315
  configuredProviders,
31143
32316
  continuability,
32317
+ countedSessionCost,
31144
32318
  createAiService,
31145
32319
  createEmptyPlan,
31146
32320
  createSession,
@@ -31204,6 +32378,7 @@ export {
31204
32378
  isMeasured,
31205
32379
  isOpenAiProvider,
31206
32380
  isPlannerBackend,
32381
+ isRepoGroup,
31207
32382
  isRunnerApproval,
31208
32383
  isSupervisorLevel,
31209
32384
  isTaskRunning,
@@ -31226,6 +32401,7 @@ export {
31226
32401
  markdownHtml,
31227
32402
  markdownTokens,
31228
32403
  mcpClientConfig,
32404
+ mergeTarget,
31229
32405
  migrateOldConfigDir,
31230
32406
  migratePlanIsolation,
31231
32407
  migratePlanState,
@@ -31275,6 +32451,7 @@ export {
31275
32451
  removedPluginNotice,
31276
32452
  renderTaskQueryAnswer,
31277
32453
  replayTaskLog,
32454
+ reposWithWork,
31278
32455
  researchShellWarning,
31279
32456
  researchToolsPath,
31280
32457
  resetInstalledPlanner,
@@ -31288,6 +32465,7 @@ export {
31288
32465
  resolveWithin,
31289
32466
  resumeLabel,
31290
32467
  resumeTime,
32468
+ resumedSessionWindow,
31291
32469
  runSettingsOf,
31292
32470
  runnerForProvider,
31293
32471
  runnerModesFrom,
@@ -31328,6 +32506,7 @@ export {
31328
32506
  taskRowView,
31329
32507
  taskStartedNotice,
31330
32508
  taskStatusKind,
32509
+ taskUsageSummary,
31331
32510
  textHasTaskOps,
31332
32511
  textHasTaskQuery,
31333
32512
  titledRefs,