@ordewell/core 0.10.4 → 0.10.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/{IFileSystem-_effm8-b.d.ts → IFileSystem-ClBdp59f.d.ts} +1 -1
  2. package/dist/{IFileSystem-vyqMdL-x.d.mts → IFileSystem-Db34maxC.d.mts} +1 -1
  3. package/dist/{IRunner-D_UkkMbJ.d.ts → IRunner-Crkt3tHD.d.ts} +64 -3
  4. package/dist/{IRunner-NtTHQgLR.d.mts → IRunner-lMQIa5Wc.d.mts} +64 -3
  5. package/dist/{ModeResolver-wnTFsDnF.d.mts → ModeResolver-B4hhABNf.d.mts} +1 -1
  6. package/dist/{ModeResolver-Dy3Ef6p-.d.ts → ModeResolver-CUR5jCSU.d.ts} +1 -1
  7. package/dist/{Task-DZs9eKgg.d.mts → Task-D4QHjdGv.d.mts} +92 -10
  8. package/dist/{Task-DZs9eKgg.d.ts → Task-D4QHjdGv.d.ts} +92 -10
  9. package/dist/{chunk-A57ITQE7.mjs → chunk-4QLFM73A.mjs} +34 -5
  10. package/dist/chunk-4QLFM73A.mjs.map +1 -0
  11. package/dist/{chunk-FH6M5QZG.mjs → chunk-6LEA6WAM.mjs} +88 -7
  12. package/dist/chunk-6LEA6WAM.mjs.map +1 -0
  13. package/dist/{chunk-645FALZX.mjs → chunk-F3KIX7WR.mjs} +3 -3
  14. package/dist/{chunk-OYUSADFM.mjs → chunk-NJGID6BG.mjs} +3 -1
  15. package/dist/chunk-NJGID6BG.mjs.map +1 -0
  16. package/dist/{chunk-SM6BV6XS.mjs → chunk-RT4NQ2BZ.mjs} +3 -1
  17. package/dist/chunk-RT4NQ2BZ.mjs.map +1 -0
  18. package/dist/index.d.mts +296 -64
  19. package/dist/index.d.ts +296 -64
  20. package/dist/index.js +1576 -376
  21. package/dist/index.js.map +1 -1
  22. package/dist/index.mjs +1455 -362
  23. package/dist/index.mjs.map +1 -1
  24. package/dist/parsing.d.mts +2 -2
  25. package/dist/parsing.d.ts +2 -2
  26. package/dist/parsing.js.map +1 -1
  27. package/dist/parsing.mjs +3 -3
  28. package/dist/{plan-utils-POIz5d8z.d.ts → plan-utils-BVJCzNvm.d.ts} +204 -19
  29. package/dist/{plan-utils-CtKceSsQ.d.mts → plan-utils-wI8MuDBC.d.mts} +204 -19
  30. package/dist/plan-utils.d.mts +4 -4
  31. package/dist/plan-utils.d.ts +4 -4
  32. package/dist/plan-utils.js +94 -5
  33. package/dist/plan-utils.js.map +1 -1
  34. package/dist/plan-utils.mjs +13 -3
  35. package/dist/testing.d.mts +6 -3
  36. package/dist/testing.d.ts +6 -3
  37. package/dist/testing.js +7 -0
  38. package/dist/testing.js.map +1 -1
  39. package/dist/testing.mjs +7 -2
  40. package/dist/testing.mjs.map +1 -1
  41. package/package.json +1 -1
  42. package/dist/chunk-A57ITQE7.mjs.map +0 -1
  43. package/dist/chunk-FH6M5QZG.mjs.map +0 -1
  44. package/dist/chunk-OYUSADFM.mjs.map +0 -1
  45. package/dist/chunk-SM6BV6XS.mjs.map +0 -1
  46. /package/dist/{chunk-645FALZX.mjs.map → chunk-F3KIX7WR.mjs.map} +0 -0
package/dist/index.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
 
@@ -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,7 +6522,8 @@ 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
6529
  return preferredFirst(models, manifest);
@@ -6546,7 +6564,8 @@ function parseOpenCodeModelsApi(stdout, manifest) {
6546
6564
  modelId: fullId,
6547
6565
  modelLabel: prefLabels.get(fullId) || typeof entry.name === "string" && entry.name || entry.modelID,
6548
6566
  runnerProvider: entry.providerID,
6549
- variants: variantIds.map((id) => ({ id, label: id.charAt(0).toUpperCase() + id.slice(1) }))
6567
+ variants: variantIds.map((id) => ({ id, label: id.charAt(0).toUpperCase() + id.slice(1) })),
6568
+ ...contextLimit(entry.limit)
6550
6569
  });
6551
6570
  }
6552
6571
  return preferredFirst(models, manifest);
@@ -8186,6 +8205,7 @@ var LOG_KINDS = {
8186
8205
  turn_start: true,
8187
8206
  command: true,
8188
8207
  command_output: true,
8208
+ model_switched: true,
8189
8209
  compaction: true,
8190
8210
  runner_loaded: true,
8191
8211
  turn_end: true,
@@ -8221,6 +8241,11 @@ ${s.content}` : ""}`).join("\n");
8221
8241
  return `${event.messageId ? `${event.messageId} ` : ""}/${event.name}${event.args ? ` ${event.args}` : ""}`;
8222
8242
  case "command_output":
8223
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
+ }
8224
8249
  case "compaction":
8225
8250
  return `${event.trigger}${event.preTokens !== void 0 && event.postTokens !== void 0 ? `, ${event.preTokens} \u2192 ${event.postTokens} tokens` : ""}`;
8226
8251
  case "runner_loaded":
@@ -9780,6 +9805,7 @@ function canUseToolAnswer({ input, suggestions }, decision) {
9780
9805
  ...decision.decision === "allowForTask" && suggestions.length > 0 ? { updatedPermissions: suggestions } : {}
9781
9806
  };
9782
9807
  }
9808
+ var MODEL_COMMAND = "model";
9783
9809
  var SUBAGENT_TOOLS = /* @__PURE__ */ new Set(["Agent", "Task"]);
9784
9810
  function usageRecord(usage, model, subagentId) {
9785
9811
  const record = {
@@ -9841,9 +9867,9 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
9841
9867
  /** The model answering the planner's current message, as its `message_start` named it. */
9842
9868
  plannerModel;
9843
9869
  /**
9844
- * The session's `total_cost_usd` as last reported. Undefined after a resume:
9845
- * the CLI restores the resumed session's running total, and what Ordewell
9846
- * 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.
9847
9873
  */
9848
9874
  reportedCostUsd = 0;
9849
9875
  reportedModelCostUsd = /* @__PURE__ */ new Map();
@@ -9884,6 +9910,19 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
9884
9910
  terminalOnly = /* @__PURE__ */ new Set();
9885
9911
  /** Whether this process's `runner_loaded` went out: its first `init` sends it. */
9886
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 = [];
9887
9926
  spawnSpec(opts, workspace) {
9888
9927
  if (opts.kind === "task") return this.taskSpawnSpec(opts);
9889
9928
  const args = [
@@ -9909,12 +9948,23 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
9909
9948
  if (opts.effort && opts.effort !== "adaptive") args.push("--effort", opts.effort);
9910
9949
  if (opts.resumeSessionId) {
9911
9950
  args.push("--resume", opts.resumeSessionId);
9912
- this.reportedCostUsd = void 0;
9951
+ this.countFrom(opts.resumeSessionId, opts.resumeCost);
9913
9952
  this.pendingResume = opts.resumeSessionId;
9914
9953
  }
9915
9954
  this.configDir = workspace.CLAUDE_CONFIG_DIR ?? process.env.CLAUDE_CONFIG_DIR ?? path13.join(os6.homedir(), ".claude");
9916
9955
  return { command: "claude", args, env: NO_AUTO_MEMORY };
9917
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
+ }
9918
9968
  savedOutput(abs) {
9919
9969
  return this.configDir !== null && this.sessionId !== null && inSavedOutput(abs, this.configDir, this.sessionId);
9920
9970
  }
@@ -9929,6 +9979,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
9929
9979
  */
9930
9980
  taskSpawnSpec(opts) {
9931
9981
  this.requestedMode = opts.flags.permissionMode;
9982
+ this.followedModel = opts.model;
9932
9983
  const args = [
9933
9984
  ...TASK_PROTOCOL_ARGS,
9934
9985
  "--permission-prompt-tool",
@@ -9942,7 +9993,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
9942
9993
  if (opts.model) args.push("--model", opts.model);
9943
9994
  if (opts.resumeSessionId) {
9944
9995
  args.push("--resume", opts.resumeSessionId);
9945
- this.reportedCostUsd = void 0;
9996
+ this.countFrom(opts.resumeSessionId, opts.resumeCost);
9946
9997
  this.pendingResume = opts.resumeSessionId;
9947
9998
  }
9948
9999
  if (opts.mcp) args.push("--append-system-prompt", TASK_COMPLETION_CONTRACT, ...this.ordewellServerArgs(opts.mcp, "task"));
@@ -9984,6 +10035,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
9984
10035
  if (opts.kind !== "task") return;
9985
10036
  const initialized = (await this.control({ subtype: "initialize" }, MODEL_PIN_TIMEOUT_MS))?.response;
9986
10037
  this.listedCommands = initialized?.commands ?? [];
10038
+ this.listedModels = initialized?.models ?? [];
9987
10039
  if (opts.flags.permissionMode !== "plan" || !opts.model) return;
9988
10040
  const resolved = initialized?.models?.find((m) => m.value === opts.model)?.resolvedModel;
9989
10041
  if (resolved && resolved !== opts.model) await this.control({ subtype: "set_model", model: resolved }, MODEL_PIN_TIMEOUT_MS);
@@ -10037,6 +10089,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
10037
10089
  if (this.role !== "task" || !this.process || this.interruptRequested || this.pendingResume) return false;
10038
10090
  const uuid = randomUUID();
10039
10091
  this.steers.set(uuid, id);
10092
+ this.noteModelCommand(text, 2);
10040
10093
  this.writeLine({ type: "user", uuid, message: { role: "user", content: [{ type: "text", text }] } });
10041
10094
  return true;
10042
10095
  }
@@ -10088,12 +10141,23 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
10088
10141
  }
10089
10142
  turnPayload(message) {
10090
10143
  this.openTurnState();
10144
+ this.noteModelCommand(message, 1);
10091
10145
  return `${JSON.stringify({
10092
10146
  type: "user",
10093
10147
  message: { role: "user", content: [{ type: "text", text: message }] }
10094
10148
  })}
10095
10149
  `;
10096
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
+ }
10097
10161
  openTurnState() {
10098
10162
  this.turnHasText = false;
10099
10163
  this.interruptRequested = false;
@@ -10205,6 +10269,7 @@ ${block.text}` : block.text });
10205
10269
  this.clearFollowOnTimer();
10206
10270
  if (this.refuseOtherMode(msg, emit)) return;
10207
10271
  this.readLoaded(msg, emit);
10272
+ this.followModel(msg.model, emit);
10208
10273
  }
10209
10274
  if (msg.subtype === "commands_changed" && Array.isArray(msg.commands)) this.listedCommands = msg.commands;
10210
10275
  if (msg.subtype === "compact_boundary") {
@@ -10227,6 +10292,7 @@ ${block.text}` : block.text });
10227
10292
  return;
10228
10293
  case "result":
10229
10294
  this.reportSessionCost(msg, emit);
10295
+ for (const command of this.modelCommands) command.turnsLeft = Math.max(0, command.turnsLeft - 1);
10230
10296
  if (this.interruptRequested && (msg.is_error || msg.subtype !== "success")) {
10231
10297
  this.interruptRequested = false;
10232
10298
  this.closeTurn({ type: "turn_end", interrupted: true }, emit);
@@ -10246,6 +10312,55 @@ ${block.text}` : block.text });
10246
10312
  return;
10247
10313
  }
10248
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
+ }
10249
10364
  /**
10250
10365
  * `--permission-mode auto` on a model or account without auto mode is not
10251
10366
  * refused: the CLI says nothing and starts in `default`, then asks about every
@@ -10329,8 +10444,10 @@ ${block.text}` : block.text });
10329
10444
  * {@link AgentEvent}), so nothing here has to reconcile with it.
10330
10445
  */
10331
10446
  handleStreamEvent(event, emit) {
10332
- if (event.type === "message_start") this.plannerModel = event.message?.model;
10333
- 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) });
10334
10451
  else if (event.type === "content_block_start" && event.content_block?.type === "text") {
10335
10452
  this.pendingBreak = this.turnHasText;
10336
10453
  } else if (event.type === "content_block_delta" && event.delta?.type === "text_delta" && event.delta.text) {
@@ -10345,7 +10462,8 @@ ${block.text}` : block.text });
10345
10462
  * What only the result line knows: the cost, which covers every call the
10346
10463
  * session made — subagents included, since none of their lines carries one —
10347
10464
  * and the planner model's window. `total_cost_usd` is a running total, so a
10348
- * 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
10349
10467
  * baseline: its total includes turns counted before, and a turn's own share
10350
10468
  * cannot be told apart from them.
10351
10469
  */
@@ -10925,6 +11043,97 @@ var CodexRpc = class {
10925
11043
  }
10926
11044
  };
10927
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
+
10928
11137
  // src/services/harness/CodexAdapter.ts
10929
11138
  var HANDSHAKE_TIMEOUT_MS = 3e4;
10930
11139
  var MCP_ATTACH_TIMEOUT_MS2 = 1e4;
@@ -11068,6 +11277,11 @@ var CodexAdapter = class extends StdioAgentAdapter {
11068
11277
  turn = null;
11069
11278
  /** The last turn this adapter ended — so its second end signal, landing late, ends nothing. */
11070
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();
11071
11285
  /** An interrupt was asked for during this turn, so however it ends, it was cut short. */
11072
11286
  interruptRequested = false;
11073
11287
  /**
@@ -11463,6 +11677,9 @@ ${this.exitMessage()}`);
11463
11677
  case "thread/tokenUsage/updated":
11464
11678
  this.emitUsage(msg.params, emit);
11465
11679
  return;
11680
+ case "account/rateLimits/updated":
11681
+ this.rateLimits = mergeCodexRateLimits(this.rateLimits, msg.params);
11682
+ return;
11466
11683
  case "turn/started": {
11467
11684
  if (this.subagentOf(msg.params?.threadId)) return;
11468
11685
  this.turnHasText = false;
@@ -11503,7 +11720,7 @@ ${this.exitMessage()}`);
11503
11720
  const failure = msg.params;
11504
11721
  if (failure?.willRetry) return;
11505
11722
  this.closeTurn(emit);
11506
- emit({ type: "error", message: failure?.error?.message || "Codex ended the turn with an error." });
11723
+ emit(this.turnFailed(failure?.error));
11507
11724
  return;
11508
11725
  }
11509
11726
  case "turn/completed": {
@@ -11514,7 +11731,7 @@ ${this.exitMessage()}`);
11514
11731
  if (this.interruptRequested || turn?.status === "interrupted") {
11515
11732
  emit({ type: "turn_end", interrupted: true });
11516
11733
  } else if (turn?.status === "failed") {
11517
- emit({ type: "error", message: turn.error?.message || "Codex ended the turn with an error." });
11734
+ emit(this.turnFailed(turn.error));
11518
11735
  } else {
11519
11736
  emit({ type: "turn_end" });
11520
11737
  }
@@ -11526,6 +11743,11 @@ ${this.exitMessage()}`);
11526
11743
  return;
11527
11744
  }
11528
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
+ }
11529
11751
  turnStarted(id) {
11530
11752
  const turn = this.turn;
11531
11753
  if (!turn || turn.id || typeof id !== "string") return;
@@ -11983,9 +12205,9 @@ function recordUnusable(servers, approval) {
11983
12205
  for (const server4 of servers) approval?.record?.(startRequest(server4), false, "unsupported");
11984
12206
  }
11985
12207
  var NOT_APPROVED = "not approved for planning";
11986
- function heldServersNote(held, unreadable = []) {
12208
+ function heldServersNote(held2, unreadable = []) {
11987
12209
  const lines = [
11988
- ...held.map(({ server: server4, reason }) => `- ${server4.name} (${server4.source}): ${reason}`),
12210
+ ...held2.map(({ server: server4, reason }) => `- ${server4.name} (${server4.source}): ${reason}`),
11989
12211
  ...unreadable.map((file) => `- ${file.source}: Ordewell could not read it (${file.reason})`)
11990
12212
  ];
11991
12213
  if (lines.length === 0) return "";
@@ -12122,8 +12344,8 @@ var PendingSteers = class {
12122
12344
  * False when it was already delivered, so a late failure does not recall it.
12123
12345
  */
12124
12346
  forget(id) {
12125
- for (const [messageId, held] of this.pending) {
12126
- if (held !== id) continue;
12347
+ for (const [messageId, held2] of this.pending) {
12348
+ if (held2 !== id) continue;
12127
12349
  this.pending.delete(messageId);
12128
12350
  return true;
12129
12351
  }
@@ -12201,9 +12423,9 @@ var ChildSessions = class {
12201
12423
  adopt(child, callId) {
12202
12424
  if (this.owners.get(child)) return null;
12203
12425
  this.owners.set(child, callId);
12204
- const held = this.held.get(child) ?? [];
12426
+ const held2 = this.held.get(child) ?? [];
12205
12427
  this.held.delete(child);
12206
- return held;
12428
+ return held2;
12207
12429
  }
12208
12430
  owns(callId) {
12209
12431
  return [...this.owners.values()].includes(callId);
@@ -12727,11 +12949,11 @@ var OpenCodeV2 = class {
12727
12949
  adoptChild(data, state, onEvent, turn) {
12728
12950
  const child = data.metadata?.sessionID;
12729
12951
  const callId = data.id;
12730
- const held = child && callId ? state.children.adopt(child, callId) : null;
12731
- if (!held || !callId) return;
12952
+ const held2 = child && callId ? state.children.adopt(child, callId) : null;
12953
+ if (!held2 || !callId) return;
12732
12954
  const model = state.stepModels.get(data.assistantMessageID ?? "");
12733
12955
  onEvent({ type: "subagent_started", subagentId: callId, brief: state.briefs.get(callId) ?? "", ...model ? { model } : {} });
12734
- for (const frame of held) this.onFrame(frame, state, turn, onEvent);
12956
+ for (const frame of held2) this.onFrame(frame, state, turn, onEvent);
12735
12957
  }
12736
12958
  finishChild(callId, success, output, state, onEvent) {
12737
12959
  if (!state.children.owns(callId)) return;
@@ -13443,13 +13665,13 @@ var OpenCodeAdapter = class {
13443
13665
  trackSubagent(part, callId, turn, onEvent) {
13444
13666
  const state = part.state;
13445
13667
  const child = state?.metadata?.sessionId;
13446
- const held = child ? turn.children.adopt(child, callId) : null;
13447
- if (held) {
13668
+ const held2 = child ? turn.children.adopt(child, callId) : null;
13669
+ if (held2) {
13448
13670
  const input = state?.input ?? {};
13449
13671
  const brief2 = typeof input.description === "string" ? input.description : typeof input.prompt === "string" ? input.prompt : "";
13450
13672
  const model = flatModelId(state?.metadata?.model?.providerID, state?.metadata?.model?.modelID);
13451
13673
  onEvent({ type: "subagent_started", subagentId: callId, brief: brief2, ...model ? { model } : {} });
13452
- for (const frame of held) this.onFrame(frame, turn, onEvent);
13674
+ for (const frame of held2) this.onFrame(frame, turn, onEvent);
13453
13675
  }
13454
13676
  const status = state?.status;
13455
13677
  if (status === "completed" || status === "error") {
@@ -13679,61 +13901,6 @@ var OPENCODE_REPO_CONFIG = {
13679
13901
  }
13680
13902
  };
13681
13903
 
13682
- // src/utils/wallTime.ts
13683
- var MONTHS = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
13684
- function lastMatch(re, text) {
13685
- let last = null;
13686
- for (const m of text.matchAll(re)) last = m;
13687
- return last;
13688
- }
13689
- function hour24(hour, meridiem) {
13690
- const h = Number(hour) % 12;
13691
- return meridiem.toLowerCase() === "pm" ? h + 12 : h;
13692
- }
13693
- function month(name) {
13694
- if (!name) return void 0;
13695
- const at = MONTHS.indexOf(name.toLowerCase());
13696
- return at < 0 ? void 0 : at;
13697
- }
13698
- function placeWallTime(wall, zone, now) {
13699
- const today = wallFields(now, zone);
13700
- const year = wall.year ?? today.year;
13701
- const monthIndex = wall.month ?? today.month;
13702
- const day = wall.day ?? today.day;
13703
- const at = fromWall(year, monthIndex, day, wall.hour, wall.minute, zone);
13704
- if (wall.day !== void 0 || at.getTime() >= now.getTime() - HOUR_MS) return at;
13705
- return fromWall(year, monthIndex, day + 1, wall.hour, wall.minute, zone);
13706
- }
13707
- var HOUR_MS = 60 * 6e4;
13708
- function wallFields(instant, zone) {
13709
- if (!zone) {
13710
- return { year: instant.getFullYear(), month: instant.getMonth(), day: instant.getDate(), hour: instant.getHours(), minute: instant.getMinutes(), second: instant.getSeconds() };
13711
- }
13712
- const parts = new Intl.DateTimeFormat("en-US", {
13713
- timeZone: zone,
13714
- hourCycle: "h23",
13715
- year: "numeric",
13716
- month: "numeric",
13717
- day: "numeric",
13718
- hour: "numeric",
13719
- minute: "numeric",
13720
- second: "numeric"
13721
- }).formatToParts(instant);
13722
- const field = (type) => Number(parts.find((p) => p.type === type)?.value ?? 0);
13723
- return { year: field("year"), month: field("month") - 1, day: field("day"), hour: field("hour"), minute: field("minute"), second: field("second") };
13724
- }
13725
- function zoneOffsetMs(instant, zone) {
13726
- const f = wallFields(new Date(instant), zone);
13727
- return Date.UTC(f.year, f.month, f.day, f.hour, f.minute, f.second) - Math.floor(instant / 1e3) * 1e3;
13728
- }
13729
- function fromWall(year, monthIndex, day, hour, minute, zone) {
13730
- if (!zone) return new Date(year, monthIndex, day, hour, minute);
13731
- const asUtc = Date.UTC(year, monthIndex, day, hour, minute);
13732
- let instant = asUtc - zoneOffsetMs(asUtc, zone);
13733
- instant = asUtc - zoneOffsetMs(instant, zone);
13734
- return new Date(instant);
13735
- }
13736
-
13737
13904
  // src/services/harness/claudeUsageLimit.ts
13738
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;
13739
13906
  function claudeUsageLimitReset(text, now) {
@@ -13749,21 +13916,6 @@ function claudeUsageLimitReset(text, now) {
13749
13916
  return placeWallTime(wall, zone, now);
13750
13917
  }
13751
13918
 
13752
- // src/services/harness/codexUsageLimit.ts
13753
- 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;
13754
- function codexUsageLimitReset(text, now) {
13755
- const m = lastMatch(CODEX_RESET_RE, text);
13756
- if (!m) return null;
13757
- const [, mon, day, year, hour, minute, meridiem] = m;
13758
- const wall = { hour: hour24(hour, meridiem), minute: Number(minute) };
13759
- if (mon) {
13760
- const at = month(mon);
13761
- if (at === void 0) return null;
13762
- Object.assign(wall, { month: at, day: Number(day), year: Number(year) });
13763
- }
13764
- return placeWallTime(wall, void 0, now);
13765
- }
13766
-
13767
13919
  // src/services/harness/connectors.ts
13768
13920
  var CONNECTORS = {
13769
13921
  "claude-code": { create: (deps) => new ClaudeCodeAdapter(deps), ordewellTools: CLAUDE_ORDEWELL, repoConfig: CLAUDE_REPO_CONFIG, usageLimitReset: claudeUsageLimitReset },
@@ -14162,7 +14314,8 @@ async function discoverGeminiModels(apiKey, baseUrl, fetchImpl) {
14162
14314
  modelId,
14163
14315
  modelLabel: m.displayName || modelId,
14164
14316
  runnerProvider: "gemini",
14165
- variants: []
14317
+ variants: [],
14318
+ ...typeof m.inputTokenLimit === "number" && m.inputTokenLimit > 0 ? { contextWindow: m.inputTokenLimit } : {}
14166
14319
  });
14167
14320
  }
14168
14321
  }
@@ -14570,6 +14723,7 @@ function parseMaxParallel(value) {
14570
14723
  // src/services/SettingsService.ts
14571
14724
  import * as fs10 from "fs";
14572
14725
  import * as path25 from "path";
14726
+ import { randomUUID as randomUUID3 } from "crypto";
14573
14727
 
14574
14728
  // src/services/LoopGuards.ts
14575
14729
  var DEFAULT_SUPERVISOR_RETRY_LIMIT = 2;
@@ -14836,23 +14990,34 @@ function createWakePolicy(options) {
14836
14990
  // src/services/runnerExit.ts
14837
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;
14838
14992
  var USAGE_LIMIT_TAIL = 4096;
14839
- function classifyRunnerStop(output) {
14840
- 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;
14841
14999
  }
14842
15000
  function stopsRunner(reason) {
14843
15001
  return reason !== "stop" && reason !== "load";
14844
15002
  }
14845
- 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) {
14846
15014
  const read2 = connectorFor(runner)?.usageLimitReset;
14847
15015
  if (!read2) return null;
14848
- let reset;
14849
15016
  try {
14850
- reset = read2(output.slice(-USAGE_LIMIT_TAIL), now);
15017
+ return read2(output.slice(-USAGE_LIMIT_TAIL), now);
14851
15018
  } catch {
14852
15019
  return null;
14853
15020
  }
14854
- if (!reset || reset.getTime() < now.getTime() - DAY_MS) return null;
14855
- return reset;
14856
15021
  }
14857
15022
  var DAY_MS = 24 * 60 * 6e4;
14858
15023
 
@@ -14956,15 +15121,15 @@ var ReplySplitter = class {
14956
15121
  push(segmentId, text) {
14957
15122
  const state = this.segments.get(segmentId);
14958
15123
  if (state === "text" || state === "plan") return { route: state, text };
14959
- const held = (state?.held ?? "") + text;
14960
- const opensWithObject = opensWithJsonObject(held);
15124
+ const held2 = (state?.held ?? "") + text;
15125
+ const opensWithObject = opensWithJsonObject(held2);
14961
15126
  if (opensWithObject === void 0) {
14962
- this.segments.set(segmentId, { held });
15127
+ this.segments.set(segmentId, { held: held2 });
14963
15128
  return null;
14964
15129
  }
14965
15130
  const route = opensWithObject ? "plan" : "text";
14966
15131
  this.segments.set(segmentId, route);
14967
- return { route, text: held };
15132
+ return { route, text: held2 };
14968
15133
  }
14969
15134
  };
14970
15135
  var TurnStream = class {
@@ -15119,6 +15284,7 @@ function plannerTranscript(history) {
15119
15284
  const loadsOf = /* @__PURE__ */ new Map();
15120
15285
  let owner = -1;
15121
15286
  for (const entry of history) {
15287
+ if (entry.kind === "error") continue;
15122
15288
  if (entry.kind === "skill_load" && entry.skill) {
15123
15289
  const loads = entry.skill.invokedBy === "user" ? loadsOf.get(owner) : void 0;
15124
15290
  if (loads) loads.push(entry.skill);
@@ -16456,6 +16622,45 @@ function markOf(was, now) {
16456
16622
  return editedFields(was, now).length > 0 || was.status !== now.status ? "changed" : void 0;
16457
16623
  }
16458
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
+
16459
16664
  // src/services/PlannerConversation.ts
16460
16665
  var CATALOG_MODEL_CAP = 100;
16461
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.";
@@ -16520,6 +16725,17 @@ var PlannerTurnFailedError = class extends Error {
16520
16725
  this.name = "PlannerTurnFailedError";
16521
16726
  }
16522
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
+ };
16523
16739
  var PlannerTurnStoppedError = class extends Error {
16524
16740
  constructor(options) {
16525
16741
  super("The planner turn was stopped.", options);
@@ -16545,6 +16761,8 @@ var PlannerConversation = class {
16545
16761
  savedInBackground = 0;
16546
16762
  /** What the planner was last told of autorun: by its opening prompt, or a turn's block since. */
16547
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;
16548
16766
  live = null;
16549
16767
  openTurn = null;
16550
16768
  get transcript() {
@@ -16660,8 +16878,8 @@ var PlannerConversation = class {
16660
16878
  * from the moment it is handed in until it lands, is refused, or the turn
16661
16879
  * ends: the backend's reply claims it, but it has not landed by then.
16662
16880
  */
16663
- showHeld(turn, held) {
16664
- 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;
16665
16883
  if (!result?.ok) {
16666
16884
  this.clearHeld(turn);
16667
16885
  return;
@@ -16680,9 +16898,9 @@ var PlannerConversation = class {
16680
16898
  * a whole plan, which an edit cannot be layered on.
16681
16899
  */
16682
16900
  pendingOps() {
16683
- const held = this.openTurn?.submitted;
16684
- if (!held) return [];
16685
- 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;
16686
16904
  }
16687
16905
  /**
16688
16906
  * A read the planner makes through a tool. It spends the same per-turn budget
@@ -16722,7 +16940,8 @@ var PlannerConversation = class {
16722
16940
  history: plan.conversationHistory,
16723
16941
  researchLog: plan.researchLog,
16724
16942
  persisted: this.persisted,
16725
- savedInBackground: this.savedInBackground
16943
+ savedInBackground: this.savedInBackground,
16944
+ toldScope: this.toldScope
16726
16945
  };
16727
16946
  }
16728
16947
  /**
@@ -16736,7 +16955,9 @@ var PlannerConversation = class {
16736
16955
  * the undo is saved in turn, or a reload would bring the writes back.
16737
16956
  */
16738
16957
  restore(snapshot) {
16739
- 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;
16740
16961
  const undo = () => {
16741
16962
  const actions = (snapshot.plan.conversationHistory ?? []).filter((entry) => entry.actor === "Ordewell setting" && !(snapshot.history ?? []).includes(entry));
16742
16963
  snapshot.plan.conversationHistory = actions.length > 0 ? [...snapshot.history ?? [], ...actions] : snapshot.history;
@@ -16771,8 +16992,9 @@ var PlannerConversation = class {
16771
16992
  * at it.
16772
16993
  */
16773
16994
  rewindTargets() {
16995
+ const opener = this.transcript.findIndex((m) => m.kind !== "error");
16774
16996
  return this.transcript.flatMap((m, index) => {
16775
- if (!isUserMessage(m) || index === 0) return [];
16997
+ if (!isUserMessage(m) || index === opener) return [];
16776
16998
  const line = m.content.split("\n")[0];
16777
16999
  const preview = line.length > REWIND_PREVIEW_WIDTH ? `${line.slice(0, REWIND_PREVIEW_WIDTH - 1)}\u2026` : line;
16778
17000
  return [{ index, preview, content: m.content, timestamp: m.timestamp }];
@@ -16899,8 +17121,10 @@ var PlannerConversation = class {
16899
17121
  return this.userTurn("supersede", goal, signal, async (userTurn) => {
16900
17122
  const opening = await prepare();
16901
17123
  this.assertCurrent(userTurn);
17124
+ userTurn.checkpoint = this.snapshot();
16902
17125
  this.recordUser(goal, (/* @__PURE__ */ new Date()).toISOString());
16903
17126
  this.toldAutorun = opening.autorun ?? false;
17127
+ this.toldScope = this.host.assignmentScope();
16904
17128
  const turn = await this.host.aiService().startConversation({
16905
17129
  ...opening,
16906
17130
  // The system prompt's goal is the text as typed; skill bodies ride the
@@ -16910,7 +17134,9 @@ var PlannerConversation = class {
16910
17134
  onProgress: userTurn.stream.sink(),
16911
17135
  signal: userTurn.signal
16912
17136
  });
16913
- 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);
16914
17140
  });
16915
17141
  }
16916
17142
  /**
@@ -16943,13 +17169,16 @@ var PlannerConversation = class {
16943
17169
  // anything the turn appends as it runs goes after this point.
16944
17170
  at: priorHistory.length
16945
17171
  };
16946
- 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");
16947
17174
  const said2 = fromUser ? plannerMessage(message.text, message.skills) : "";
16948
17175
  const outgoing = [contextBlock, ...wakeUps, userTurn.wakeUp?.brief, said2].filter(Boolean).join("\n\n");
16949
17176
  const ai = this.host.aiService();
16950
17177
  try {
16951
17178
  const turn = this.canContinueLive() ? await ai.continueConversation(outgoing, userTurn.stream.sink(), signal) : await this.resume(outgoing, priorHistory, signal, userTurn.stream.sink());
16952
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);
16953
17182
  if (userTurn.wakeUp) this.assertWakeUpAnswered(settleable, userTurn);
16954
17183
  this.assertCurrent(userTurn, settleable);
16955
17184
  return await this.settle(settleable, userTurn);
@@ -16968,6 +17197,34 @@ var PlannerConversation = class {
16968
17197
  if (userTurn.signal.aborted) throw new PlannerTurnStoppedError();
16969
17198
  if (turn.kind === "message" && turn.failed) throw new PlannerTurnFailedError(turn.text);
16970
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
+ }
16971
17228
  assertIdle(operation) {
16972
17229
  if (this.isTurnInFlight) throw new ConversationBusyError(operation);
16973
17230
  }
@@ -17109,6 +17366,7 @@ var PlannerConversation = class {
17109
17366
  const goal = opener || this.host.goal() || message;
17110
17367
  this.reset();
17111
17368
  this.toldAutorun = opening.autorun ?? false;
17369
+ this.toldScope = this.host.assignmentScope();
17112
17370
  return this.host.aiService().startConversation({
17113
17371
  ...opening,
17114
17372
  runners,
@@ -17347,6 +17605,24 @@ The plan is unchanged. Rephrase the request, or adjust the tasks manually.`,
17347
17605
  this.toldAutorun = now;
17348
17606
  return now ? AUTORUN_ON : AUTORUN_OFF;
17349
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
+ }
17350
17626
  runUpdatesBlock() {
17351
17627
  const updates = this.host.takeRunUpdates();
17352
17628
  return updates.length > 0 ? ["Run update:", ...updates.map((update) => `- ${update}`)].join("\n") : null;
@@ -17427,7 +17703,6 @@ var WAKE_DEBOUNCE_MS = 5e3;
17427
17703
  var WAKE_UP_TURN_TIMEOUT_MS = 10 * 6e4;
17428
17704
  var HUNG_TURN_GRACE_MS = 3e4;
17429
17705
  var DEFAULT_WAKE_UP_READ_BUDGET = 12;
17430
- var RESET_MARGIN_MS = 6e4;
17431
17706
  var DETAIL_CHARS = 240;
17432
17707
  var WakeUpTimeoutError = class extends Error {
17433
17708
  constructor(timeoutMs) {
@@ -17453,8 +17728,6 @@ var Supervisor = class {
17453
17728
  /** The run's end was reported: the scheduler can announce it more than once. */
17454
17729
  finished = false;
17455
17730
  runEndPending = false;
17456
- /** While the planner's usage limit holds wake-ups back: when it lets them go. */
17457
- limited = null;
17458
17731
  /** When each wake-up since the last pass was delivered, for the wake-up rate guard. */
17459
17732
  wakeUps = [];
17460
17733
  debounceMs;
@@ -17475,9 +17748,9 @@ var Supervisor = class {
17475
17748
  get awaitsRunEnd() {
17476
17749
  return this.runEndPending;
17477
17750
  }
17478
- /** 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. */
17479
17752
  mayWake() {
17480
- return this.watching && this.limited === null;
17753
+ return this.watching;
17481
17754
  }
17482
17755
  /**
17483
17756
  * What a wake-up turn is held to under the run's level. The brief is the
@@ -17544,8 +17817,6 @@ var Supervisor = class {
17544
17817
  /** The plan these events were about is gone: nothing gathered or waited on carries over. */
17545
17818
  reset() {
17546
17819
  this.disarm();
17547
- if (this.limited) clearTimeout(this.limited.timer);
17548
- this.limited = null;
17549
17820
  this.failures = 0;
17550
17821
  this.finished = false;
17551
17822
  this.runEndPending = false;
@@ -17596,9 +17867,13 @@ var Supervisor = class {
17596
17867
  }
17597
17868
  failed(err) {
17598
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
+ }
17599
17874
  const reason = err instanceof Error ? err.message : String(err);
17600
- if (!(err instanceof WakeUpTimeoutError) && classifyRunnerStop(reason) === "usage-limit") {
17601
- 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");
17602
17877
  return;
17603
17878
  }
17604
17879
  this.failures++;
@@ -17609,22 +17884,6 @@ var Supervisor = class {
17609
17884
  }
17610
17885
  this.giveUp(`its wake-up turn failed twice (${brief(reason)})`);
17611
17886
  }
17612
- waitForReset(said2) {
17613
- const runner = this.host.plannerRunner();
17614
- const reset = runner ? readUsageLimitReset(runner, said2, new Date(this.now())) : null;
17615
- if (!reset) {
17616
- this.giveUp("the planner hit its usage limit and did not say when it resets");
17617
- return;
17618
- }
17619
- if (this.limited) clearTimeout(this.limited.timer);
17620
- const until = reset.getTime() + RESET_MARGIN_MS;
17621
- const timer = setTimeout(() => {
17622
- this.limited = null;
17623
- this.host.pump();
17624
- }, Math.max(0, until - this.now()));
17625
- this.limited = { until, timer };
17626
- 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.`);
17627
- }
17628
17887
  /** The run goes on without a supervisor, and nothing that happened is lost (see {@link standDown}). */
17629
17888
  giveUp(reason) {
17630
17889
  this.host.goUnsupervised(reason);
@@ -17638,8 +17897,8 @@ var Supervisor = class {
17638
17897
  standDown() {
17639
17898
  this.disarm();
17640
17899
  this.runEndPending = false;
17641
- const held = this.policy.poll(Number.POSITIVE_INFINITY);
17642
- if (held) this.deliver(held);
17900
+ const held2 = this.policy.poll(Number.POSITIVE_INFINITY);
17901
+ if (held2) this.deliver(held2);
17643
17902
  }
17644
17903
  arm() {
17645
17904
  const due = this.policy.dueAt();
@@ -17830,78 +18089,224 @@ function runSettingsOf(settings) {
17830
18089
  }
17831
18090
  return out;
17832
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
+ }
17833
18169
  var SettingsService = class {
17834
18170
  filePath;
17835
18171
  cache = null;
17836
- cachedMtimeMs = null;
18172
+ cachedStamp = null;
17837
18173
  constructor(filePath = getSettingsPath()) {
17838
18174
  this.filePath = filePath;
17839
18175
  }
17840
18176
  // Several processes share the settings file (VS Code extension host, web
17841
- // 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
17842
18178
  // other process invalidates it. A stale cache here previously meant an
17843
18179
  // allowlist set in one surface never reached another's planner.
17844
18180
  getAll() {
17845
- const mtime = this.fileMtimeMs();
17846
- if (this.cache && mtime === this.cachedMtimeMs) return this.cache;
18181
+ const stamp = this.fileStamp();
18182
+ if (this.cache && stamp === this.cachedStamp) return this.cache;
17847
18183
  this.cache = this.load();
17848
- this.cachedMtimeMs = mtime;
18184
+ this.cachedStamp = stamp;
17849
18185
  return this.cache;
17850
18186
  }
17851
- 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) {
17852
18193
  try {
17853
- return fs10.statSync(this.filePath).mtimeMs;
18194
+ const stat = fs10.statSync(file);
18195
+ return `${stat.ino}:${stat.mtimeMs}:${stat.size}`;
17854
18196
  } catch {
17855
18197
  return null;
17856
18198
  }
17857
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
+ }
17858
18241
  getModelAllowlist(runner) {
17859
18242
  return this.getAll().modelAllowlist?.[runner];
17860
18243
  }
17861
18244
  setModelAllowlist(runner, ids) {
17862
- this.getAll();
17863
- if (ids === void 0) {
17864
- if (this.cache.modelAllowlist) {
17865
- delete this.cache.modelAllowlist[runner];
17866
- if (Object.keys(this.cache.modelAllowlist).length === 0) {
17867
- 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
+ }
17868
18252
  }
18253
+ } else {
18254
+ if (!this.cache.modelAllowlist) {
18255
+ this.cache.modelAllowlist = {};
18256
+ }
18257
+ this.cache.modelAllowlist[runner] = ids;
17869
18258
  }
17870
- } else {
17871
- if (!this.cache.modelAllowlist) {
17872
- this.cache.modelAllowlist = {};
17873
- }
17874
- this.cache.modelAllowlist[runner] = ids;
17875
- }
17876
- this.persist();
18259
+ });
17877
18260
  }
17878
18261
  getPlannerModel(provider) {
17879
18262
  return this.getAll().plannerModels?.[provider];
17880
18263
  }
17881
18264
  setPlannerModel(provider, entry) {
17882
- this.getAll();
17883
- if (entry === void 0) {
17884
- if (this.cache.plannerModels) {
17885
- delete this.cache.plannerModels[provider];
17886
- if (Object.keys(this.cache.plannerModels).length === 0) {
17887
- 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
+ }
17888
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 };
17889
18278
  }
17890
- } else {
17891
- if (!this.cache.plannerModels) {
17892
- this.cache.plannerModels = {};
17893
- }
17894
- this.cache.plannerModels[provider] = entry.effort ? { model: entry.model, effort: entry.effort } : { model: entry.model };
17895
- }
17896
- this.persist();
18279
+ });
17897
18280
  }
17898
18281
  getEnabledRunners() {
17899
18282
  return this.getAll().enabledRunners;
17900
18283
  }
17901
18284
  setEnabledRunners(ids) {
17902
- this.getAll();
17903
- this.cache.enabledRunners = [...ids];
17904
- 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
+ });
17905
18310
  }
17906
18311
  /**
17907
18312
  * Full or Guarded (ADR-0001), as saved here (ADR-0030). A value set in the
@@ -17919,9 +18324,9 @@ var SettingsService = class {
17919
18324
  return env !== void 0 && env.trim() !== "";
17920
18325
  }
17921
18326
  setAutonomousMode(enabled) {
17922
- this.getAll();
17923
- this.cache.autonomousMode = enabled;
17924
- this.persist();
18327
+ this.save(() => {
18328
+ this.cache.autonomousMode = enabled;
18329
+ });
17925
18330
  }
17926
18331
  getRunSettings() {
17927
18332
  return runSettingsOf(this.getAll());
@@ -17933,11 +18338,11 @@ var SettingsService = class {
17933
18338
  throw new RunSettingsError(RUN_SETTING_CHECKS[key].refusal);
17934
18339
  }
17935
18340
  }
17936
- this.getAll();
17937
- for (const key of RUN_SETTING_KEYS) {
17938
- if (changes[key] !== void 0) this.cache[key] = changes[key];
17939
- }
17940
- 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
+ });
17941
18346
  return this.getRunSettings();
17942
18347
  }
17943
18348
  load() {
@@ -17966,13 +18371,43 @@ var SettingsService = class {
17966
18371
  }
17967
18372
  return {};
17968
18373
  }
17969
- 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) {
17970
18393
  const dir = path25.dirname(this.filePath);
17971
18394
  if (!fs10.existsSync(dir)) {
17972
18395
  fs10.mkdirSync(dir, { recursive: true });
17973
18396
  }
17974
- fs10.writeFileSync(this.filePath, JSON.stringify(this.cache, null, 2));
17975
- 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;
17976
18411
  }
17977
18412
  };
17978
18413
 
@@ -18456,7 +18891,7 @@ var EnvConfig = class extends BaseConfig {
18456
18891
  };
18457
18892
 
18458
18893
  // src/services/mergeResultNotice.ts
18459
- function blockNotice({ repo, reason, files }) {
18894
+ function blockNotice({ repo, reason, files, expected, actual, rebasing }) {
18460
18895
  switch (reason) {
18461
18896
  case "merge-in-progress":
18462
18897
  return `${repo} has a merge in progress`;
@@ -18466,10 +18901,22 @@ function blockNotice({ repo, reason, files }) {
18466
18901
  return `${repo} has uncommitted changes to ${files.join(", ")}`;
18467
18902
  case "partial-landing":
18468
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}`;
18469
18906
  case "git-error":
18470
18907
  return `git could not check ${repo}`;
18471
18908
  }
18472
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
+ }
18473
18920
  function landedNotice(landed2) {
18474
18921
  if (landed2.length === 0) return "Nothing was merged.";
18475
18922
  return landed2.length === 1 ? `${landed2[0]} was merged already and stays merged.` : `${landed2.join(", ")} were merged already and stay merged.`;
@@ -18486,7 +18933,7 @@ function describeMergeResult(result, branch, group, repaired = []) {
18486
18933
  return { level: "info", message: repaired.length > 0 ? `${base} ${repairedNotice(repaired)}` : base };
18487
18934
  }
18488
18935
  case "blocked":
18489
- 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)}` };
18490
18937
  case "conflict":
18491
18938
  return {
18492
18939
  level: "warn",
@@ -18850,7 +19297,7 @@ var GitWorktreeIsolation = class {
18850
19297
  path: repoPath,
18851
19298
  root,
18852
19299
  baseRef,
18853
- ...branch ? { baseBranch: branch } : {},
19300
+ ...branch ? { baseBranch: branch } : { startedDetached: true },
18854
19301
  integrationBranch: integrationBranchFor(run.id)
18855
19302
  };
18856
19303
  if (!(await this.tryGit(root, ["branch", repo.integrationBranch, baseRef])).ok) return null;
@@ -19203,7 +19650,9 @@ var GitWorktreeIsolation = class {
19203
19650
  async mergeAll(run) {
19204
19651
  const partial = await this.settleLanding(run);
19205
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);
19206
19654
  const repos = await this.reposWithWork(run);
19655
+ if (misplaced.length > 0) return { outcome: "blocked", blocked: misplaced };
19207
19656
  if (run.repos.length === 1 || !await this.canPreflight()) return this.mergeInTurn(repos);
19208
19657
  const blocked = [];
19209
19658
  for (const repo of repos) {
@@ -19221,6 +19670,27 @@ var GitWorktreeIsolation = class {
19221
19670
  }
19222
19671
  return withWork;
19223
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
+ }
19224
19694
  /** `git merge-tree --write-tree`, which merges without touching a tree, arrived in git 2.38. */
19225
19695
  async canPreflight() {
19226
19696
  const version = await this.tryGit(void 0, ["--version"]);
@@ -19708,6 +20178,14 @@ var GitWorktreeIsolation = class {
19708
20178
  if ((await this.tryGit(cwd, ["rev-parse", "-q", "--verify", "MERGE_HEAD"])).ok) return true;
19709
20179
  return (await this.tryGit(cwd, ["ls-files", "-u"])).stdout.trim() !== "";
19710
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
+ }
19711
20189
  async abortMerge(cwd) {
19712
20190
  if (!(await this.tryGit(cwd, ["merge", "--abort"])).ok) await this.tryGit(cwd, ["reset", "--hard"]);
19713
20191
  }
@@ -19728,6 +20206,29 @@ function createWorktreeIsolation(deps) {
19728
20206
  return new GitWorktreeIsolation(deps);
19729
20207
  }
19730
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
+
19731
20232
  // src/interfaces/ILogger.ts
19732
20233
  var ConsoleLogger = class {
19733
20234
  warn(scope, message, err) {
@@ -19780,7 +20281,10 @@ async function settleReply(opts) {
19780
20281
  opts.onProgress({ type: "interrupted" });
19781
20282
  return { done: turn };
19782
20283
  }
19783
- 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
+ }
19784
20288
  if (!attempt.text.trim()) return { done: message(emptyReplyReport(deniedStep(attempt))) };
19785
20289
  const reply2 = opts.classify ? classifyPlannerReply(attempt.text, opts.classify) : { kind: "prose" };
19786
20290
  switch (reply2.kind) {
@@ -20711,7 +21215,7 @@ var CONVERSATION_TOOL_DEFINITIONS = toGeminiToolDeclarations(true);
20711
21215
  function tokenCount(value) {
20712
21216
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
20713
21217
  }
20714
- function usageRecordFrom(usage) {
21218
+ function usageRecordFrom(usage, model) {
20715
21219
  if (!usage) return void 0;
20716
21220
  const inputTokens = tokenCount(usage.promptTokenCount);
20717
21221
  const answerTokens = tokenCount(usage.candidatesTokenCount);
@@ -20719,6 +21223,7 @@ function usageRecordFrom(usage) {
20719
21223
  const reportedOutput = [answerTokens, thoughtTokens].filter((t) => t !== void 0);
20720
21224
  const record = {
20721
21225
  source: "google",
21226
+ model,
20722
21227
  ...inputTokens !== void 0 ? { inputTokens } : {},
20723
21228
  ...reportedOutput.length > 0 ? { outputTokens: reportedOutput.reduce((a, b) => a + b, 0) } : {},
20724
21229
  ...tokenCount(usage.cachedContentTokenCount) !== void 0 ? { cachedInputTokens: tokenCount(usage.cachedContentTokenCount) } : {}
@@ -20727,15 +21232,31 @@ function usageRecordFrom(usage) {
20727
21232
  return anyReported ? record : void 0;
20728
21233
  }
20729
21234
  var GeminiResearchChat = class {
20730
- constructor(chat, hooks = {}) {
20731
- this.chat = chat;
21235
+ constructor(getModel, open, hooks = {}) {
21236
+ this.getModel = getModel;
21237
+ this.open = open;
20732
21238
  this.hooks = hooks;
21239
+ this.model = getModel();
21240
+ this.chat = open(this.model);
20733
21241
  }
20734
- chat;
21242
+ getModel;
21243
+ open;
20735
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
+ }
20736
21257
  async sendMessage(text, signal) {
20737
21258
  if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
20738
- const result = await this.chat.sendMessageStream(text);
21259
+ const result = await (await this.current()).sendMessageStream(text);
20739
21260
  return this.consume(result.stream);
20740
21261
  }
20741
21262
  async sendToolResults(results, signal) {
@@ -20746,7 +21267,7 @@ var GeminiResearchChat = class {
20746
21267
  response: { output: r.output, truncated: r.truncated, totalChars: r.totalChars }
20747
21268
  }
20748
21269
  }));
20749
- const result = await this.chat.sendMessageStream(funcResponses);
21270
+ const result = await (await this.current()).sendMessageStream(funcResponses);
20750
21271
  return this.consume(result.stream);
20751
21272
  }
20752
21273
  /**
@@ -20786,7 +21307,7 @@ var GeminiResearchChat = class {
20786
21307
  }
20787
21308
  }
20788
21309
  }
20789
- const usageRecord3 = usageRecordFrom(usage);
21310
+ const usageRecord3 = usageRecordFrom(usage, this.model);
20790
21311
  if (usageRecord3) this.hooks.onUsage?.(usageRecord3);
20791
21312
  return {
20792
21313
  text,
@@ -20802,6 +21323,8 @@ var GeminiService = class extends BaseAiService {
20802
21323
  genAI = null;
20803
21324
  genAIKey = "";
20804
21325
  model = null;
21326
+ /** The id `model` was made for; the SDK's own `model.model` is its `models/…` resource path. */
21327
+ modelId = "";
20805
21328
  constructor(config) {
20806
21329
  super(config);
20807
21330
  }
@@ -20815,13 +21338,18 @@ var GeminiService = class extends BaseAiService {
20815
21338
  }
20816
21339
  return true;
20817
21340
  }
20818
- 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()) {
20819
21346
  this.init();
20820
- if (!this.model || this.model.model !== this.config.planningModel) {
21347
+ if (!this.model || this.modelId !== model) {
20821
21348
  this.model = this.genAI.getGenerativeModel({
20822
- model: this.config.planningModel,
21349
+ model,
20823
21350
  generationConfig: { temperature: 0.3, topP: 0.95, maxOutputTokens: 16384 }
20824
21351
  });
21352
+ this.modelId = model;
20825
21353
  }
20826
21354
  return this.model;
20827
21355
  }
@@ -20836,7 +21364,6 @@ var GeminiService = class extends BaseAiService {
20836
21364
  }
20837
21365
  async startConversation(req) {
20838
21366
  this.ensureInit();
20839
- const genModel = this.getPlanningModel();
20840
21367
  const contextStr = await BaseAiService.collectResearchContext(req.fs, req.runners);
20841
21368
  const systemPrompt = buildConversationSystemPrompt(
20842
21369
  req.goal,
@@ -20865,13 +21392,14 @@ ${m.content}` });
20865
21392
  if ((req.priorHistory?.length ?? 0) > 0 && seededHistory[seededHistory.length - 1].role === "user") {
20866
21393
  seededHistory.push({ role: "model", parts: [{ text: "Understood." }] });
20867
21394
  }
20868
- const chat = genModel.startChat({
20869
- history: seededHistory,
20870
- 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,
20871
21399
  generationConfig: { temperature: 0.3, topP: 0.95, maxOutputTokens: 16384 }
20872
21400
  });
20873
21401
  let currentProgress = req.onProgress;
20874
- const researchChat = new GeminiResearchChat(chat, {
21402
+ const researchChat = new GeminiResearchChat(() => this.plannerModel(), open, {
20875
21403
  onReasoning: (text, segmentId) => currentProgress({ type: "thinking", text, segmentId }),
20876
21404
  onContent: (text, segmentId) => currentProgress({ type: "text_delta", text, segmentId }),
20877
21405
  onUsage: (record) => currentProgress({ type: "usage", record })
@@ -20944,18 +21472,17 @@ ${repairHint}` }] : parts;
20944
21472
  async researchAndPlan(userDescription, runners, modelsByRunner, fs18, onProgress, fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
20945
21473
  this.ensureInit();
20946
21474
  const { autonomousDefault } = modes;
20947
- const genModel = this.getPlanningModel();
20948
21475
  const contextStr = await BaseAiService.collectResearchContext(fs18, runners);
20949
21476
  const systemPrompt = buildResearchPrompt(userDescription, contextStr, modelsByRunner, runners, runnerModes, modes);
20950
21477
  const firstMessage = `${buildResearchToolsPrompt()}
20951
21478
 
20952
21479
  Explore the workspace to understand the codebase, then generate the plan.`;
20953
- const chat = genModel.startChat({
20954
- history: [{ role: "user", parts: [{ text: systemPrompt }] }],
21480
+ const open = (model, history = [{ role: "user", parts: [{ text: systemPrompt }] }]) => this.getPlanningModel(model).startChat({
21481
+ history,
20955
21482
  tools: [{ functionDeclarations: TOOL_DEFINITIONS }],
20956
21483
  generationConfig: { temperature: 0.3, topP: 0.95, maxOutputTokens: 16384 }
20957
21484
  });
20958
- const researchChat = new GeminiResearchChat(chat, {
21485
+ const researchChat = new GeminiResearchChat(() => this.plannerModel(), open, {
20959
21486
  onUsage: (record) => onProgress({ type: "usage", record })
20960
21487
  });
20961
21488
  const result = await this.runResearchLoop(researchChat, firstMessage, fs18, onProgress, runners, void 0, fetcher, userDescription, runnerModes, autonomousDefault, signal);
@@ -20991,7 +21518,7 @@ import OpenAI from "openai";
20991
21518
  import { v4 as uuidv44 } from "uuid";
20992
21519
  var KEYLESS_PLACEHOLDER = "not-needed";
20993
21520
  var OpenAiResearchChat = class {
20994
- constructor(messages, getClient, getModel, tools, onReasoning, onContent, getSource = () => "openai", onUsage, contextWindow) {
21521
+ constructor(messages, getClient, getModel, tools, onReasoning, onContent, getSource = () => "openai", onUsage) {
20995
21522
  this.messages = messages;
20996
21523
  this.getClient = getClient;
20997
21524
  this.getModel = getModel;
@@ -21000,7 +21527,6 @@ var OpenAiResearchChat = class {
21000
21527
  this.onContent = onContent;
21001
21528
  this.getSource = getSource;
21002
21529
  this.onUsage = onUsage;
21003
- this.contextWindow = contextWindow;
21004
21530
  }
21005
21531
  messages;
21006
21532
  getClient;
@@ -21010,7 +21536,6 @@ var OpenAiResearchChat = class {
21010
21536
  onContent;
21011
21537
  getSource;
21012
21538
  onUsage;
21013
- contextWindow;
21014
21539
  async sendMessage(text, signal) {
21015
21540
  this.answerAbandonedCalls();
21016
21541
  this.messages.push({ role: "user", content: text });
@@ -21125,7 +21650,6 @@ var OpenAiResearchChat = class {
21125
21650
  record.reportedCost = { amount: reported.cost, currency: "USD" };
21126
21651
  hasMeasure = true;
21127
21652
  }
21128
- if (this.contextWindow && this.contextWindow > 0) record.contextWindow = this.contextWindow;
21129
21653
  return hasMeasure ? record : void 0;
21130
21654
  }
21131
21655
  };
@@ -21252,8 +21776,7 @@ ${buildResearchToolsPrompt()}` }
21252
21776
  (delta, segmentId) => currentProgress({ type: "thinking", text: delta, segmentId }),
21253
21777
  (delta, segmentId) => currentProgress({ type: "text_delta", text: delta, segmentId }),
21254
21778
  () => this.config.aiProvider,
21255
- (record) => currentProgress({ type: "usage", record }),
21256
- req.contextWindow
21779
+ (record) => currentProgress({ type: "usage", record })
21257
21780
  );
21258
21781
  const ctx = {
21259
21782
  chat,
@@ -21815,6 +22338,10 @@ var VerdictEngine = class {
21815
22338
  this.generations.clear();
21816
22339
  }
21817
22340
  };
22341
+ var EXIT_CHECK = "exit_code";
22342
+ function endedByExit(verdict) {
22343
+ return verdict.checks.some((check2) => check2.name === EXIT_CHECK);
22344
+ }
21818
22345
  function exitVerdict(exitCode) {
21819
22346
  const normalized2 = exitCode == null ? 0 : exitCode;
21820
22347
  const exitOk = normalized2 === 0;
@@ -21823,7 +22350,7 @@ function exitVerdict(exitCode) {
21823
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.`,
21824
22351
  checks: [
21825
22352
  { name: "task_complete", passed: false, skipped: false, detail: "the runner exited before calling task_complete" },
21826
- { 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}` }
21827
22354
  ],
21828
22355
  decidedAt: (/* @__PURE__ */ new Date()).toISOString()
21829
22356
  };
@@ -22022,6 +22549,7 @@ var PlanStore = class {
22022
22549
  this.rebuild();
22023
22550
  for (const task of this._allTasks) {
22024
22551
  if (task.status === "failed") task.status = "pending";
22552
+ if (task.runnerSessionId && !task.runnerSessionOwner) task.runnerSessionOwner = sessionOwner(task) ?? task.assignedRunner;
22025
22553
  }
22026
22554
  this.validateAssignedRunners();
22027
22555
  }
@@ -22151,9 +22679,17 @@ var PlanStore = class {
22151
22679
  const task = this._taskMap.get(id);
22152
22680
  if (task) task.outputSummary = summary;
22153
22681
  }
22154
- 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) {
22155
22691
  const task = this._taskMap.get(id);
22156
- if (task) task.runnerSessionId = runnerSessionId;
22692
+ if (task) task.carriedFrom = runner;
22157
22693
  }
22158
22694
  clearCarriedFrom(id) {
22159
22695
  const task = this._taskMap.get(id);
@@ -22167,9 +22703,9 @@ var PlanStore = class {
22167
22703
  const task = this._taskMap.get(id);
22168
22704
  if (task?.attemptUsage) task.attemptUsage = task.attemptUsage.map((usage) => usage.attempt === attempt ? addAttemptUsage(usage, record, now) : usage);
22169
22705
  }
22170
- endAttemptUsage(id, attempt, now) {
22706
+ endAttemptUsage(id, attempt, now, runnerSessionCost, sessionId) {
22171
22707
  const task = this._taskMap.get(id);
22172
- 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);
22173
22709
  }
22174
22710
  setTaskAttemptSkills(id, skills) {
22175
22711
  const task = this._taskMap.get(id);
@@ -23027,6 +23563,8 @@ function conflictResolverTask(task, run, plan) {
23027
23563
  }
23028
23564
 
23029
23565
  // src/services/UsageLimitWaits.ts
23566
+ var RESUME_MARGIN_MS = 6e4;
23567
+ var RELAPSE_MS = 5 * 6e4;
23030
23568
  var UsageLimitWaits = class {
23031
23569
  constructor(onDue) {
23032
23570
  this.onDue = onDue;
@@ -23149,6 +23687,9 @@ function selectReadyTasks(input) {
23149
23687
  gated
23150
23688
  };
23151
23689
  }
23690
+ function isBlocked(task, store) {
23691
+ return blockedIn(task, blockingFailures(store.allTasks));
23692
+ }
23152
23693
  var blockedIn = (task, blocked) => task.status === "blocked" || blocked.has(task.id);
23153
23694
  function dependencyMet(depId, store, runs) {
23154
23695
  return waitingBehind([depId], store, runs).length === 0;
@@ -23278,6 +23819,12 @@ var TaskOrchestrator = class _TaskOrchestrator {
23278
23819
  * task whose spawn always throws.
23279
23820
  */
23280
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();
23281
23828
  /**
23282
23829
  * Tasks Mark complete is landing after it ended their attempt: in progress
23283
23830
  * with no attempt for as long as that takes, and not orphans for it.
@@ -23299,6 +23846,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
23299
23846
  observers = [];
23300
23847
  skillsAt;
23301
23848
  previousAttemptFromLog;
23849
+ lastLoggedAttempt;
23302
23850
  runSettings;
23303
23851
  isApproved;
23304
23852
  constructor(deps) {
@@ -23314,6 +23862,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
23314
23862
  this.workspaceEnv = deps.workspaceEnv;
23315
23863
  this.skillsAt = deps.skillsAt;
23316
23864
  this.previousAttemptFromLog = deps.previousAttemptFromLog;
23865
+ this.lastLoggedAttempt = deps.lastLoggedAttempt;
23317
23866
  this.runSettings = deps.runSettings;
23318
23867
  this.isApproved = deps.isApproved;
23319
23868
  this.store.onMutate = () => this.emit("onTaskChanged");
@@ -23378,6 +23927,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
23378
23927
  workspaceEnv: options.workspaceEnv ?? ((cwd) => resolveWorkspaceEnv(cwd)),
23379
23928
  skillsAt: options.skillsAt ?? ((roots) => new SkillsService(roots)),
23380
23929
  previousAttemptFromLog: options.previousAttemptFromLog ?? (() => null),
23930
+ lastLoggedAttempt: options.lastLoggedAttempt ?? (() => 0),
23381
23931
  runSettings: options.runSettings ?? (() => void 0),
23382
23932
  isApproved: options.isApproved ?? (() => true)
23383
23933
  });
@@ -23634,10 +24184,10 @@ var TaskOrchestrator = class _TaskOrchestrator {
23634
24184
  */
23635
24185
  holdStarts() {
23636
24186
  this.startHolds++;
23637
- let held = true;
24187
+ let held2 = true;
23638
24188
  return () => {
23639
- if (!held) return;
23640
- held = false;
24189
+ if (!held2) return;
24190
+ held2 = false;
23641
24191
  this.startHolds--;
23642
24192
  };
23643
24193
  }
@@ -23648,24 +24198,25 @@ var TaskOrchestrator = class _TaskOrchestrator {
23648
24198
  */
23649
24199
  admitStart() {
23650
24200
  const generation2 = this.stopGeneration;
23651
- let held = true;
24201
+ let held2 = true;
23652
24202
  this.admissions++;
23653
24203
  return {
23654
24204
  current: () => generation2 === this.stopGeneration,
23655
24205
  release: () => {
23656
- if (!held) return;
23657
- held = false;
24206
+ if (!held2) return;
24207
+ held2 = false;
23658
24208
  this.admissions--;
23659
24209
  }
23660
24210
  };
23661
24211
  }
23662
24212
  /**
23663
24213
  * Something can still start that nobody has to ask for again: a start
23664
- * admitted and not yet claimed, or a task that resumes on its own at its
23665
- * 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.
23666
24217
  */
23667
24218
  get startsPending() {
23668
- return this.admissions > 0 || this.store.allTasks.some(waitsForReset);
24219
+ return this.admissions > 0 || this.queuedStarts.size > 0 || this.store.allTasks.some(waitsForReset);
23669
24220
  }
23670
24221
  /** Run `work`, which awaits before it claims, under an admission it holds throughout; it is handed the generation it was admitted under. */
23671
24222
  async whileAdmitted(work) {
@@ -23764,6 +24315,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
23764
24315
  this.retryCounts.clear();
23765
24316
  this.spawnCounts.clear();
23766
24317
  this.onHold.clear();
24318
+ this.queuedStarts.clear();
23767
24319
  this.recoverOrphans();
23768
24320
  this.rearmLimitWaits();
23769
24321
  }
@@ -23851,6 +24403,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
23851
24403
  }
23852
24404
  this.runs.interrupt();
23853
24405
  this.onHold.clear();
24406
+ if (reason === "user") this.queuedStarts.clear();
23854
24407
  for (const t of this.store.allTasks.filter(waitsForReset)) this.cancelScheduledResume(t.id);
23855
24408
  this.sayStopped(stopped);
23856
24409
  this.recoverOrphans();
@@ -23950,6 +24503,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
23950
24503
  const attempt = this.attempts.get(taskId);
23951
24504
  if (!task || !attempt) return;
23952
24505
  attempt.decided = true;
24506
+ const stoppedMidTurn = endedByExit(verdict) ? saidInFlight(attempt) : null;
23953
24507
  console.error(`[TaskOrchestrator] Task ${taskRefOf(this.store.planTasks, task.id) ?? `#${task.order}`} "${task.title}" verdict=${verdict.outcome}`);
23954
24508
  console.error(`[TaskOrchestrator] Runner: ${task.assignedRunner}, Model: ${task.assignedModel?.modelId ?? "default"}`);
23955
24509
  console.error(`[TaskOrchestrator] Prompt preview: ${(task.prompt ?? "").slice(0, 200)}`);
@@ -23971,8 +24525,8 @@ ${summary || "(empty: no output captured)"}`);
23971
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.`);
23972
24526
  } else if (landing) {
23973
24527
  await this.applyLanding(task, landing, () => this.notifications.info(`Task "${task.title}" completed.`));
23974
- } else if (classifyRunnerStop(attempt.session?.getOutput() ?? "") === "usage-limit") {
23975
- this.pauseOnLimit(task, attempt, attempt.session?.getOutput() ?? "");
24528
+ } else if (stoppedMidTurn !== null && classifyRunnerStop(stoppedMidTurn) === "usage-limit") {
24529
+ this.pauseOnLimit(task, attempt, stoppedMidTurn);
23976
24530
  if (attempt.worktree) await this.runs.release(taskId, { keep: true });
23977
24531
  } else {
23978
24532
  this.store.markFailed(taskId);
@@ -23987,16 +24541,17 @@ ${summary || "(empty: no output captured)"}`);
23987
24541
  const status = this.store.get(taskId)?.status;
23988
24542
  if (status === "completed" || status === "failed" || status === "awaiting_user") this.emit("onTaskSettled", { taskId });
23989
24543
  this.emit("onTaskChanged");
23990
- if (!this.running) {
23991
- if (this.attempts.size === 0) {
23992
- if (this.store.isFinished()) this.planStatus = "completed";
23993
- this.emit("onTick");
23994
- if (this.settled) await this.closeRun();
23995
- this.emit("onExecutionComplete");
23996
- }
24544
+ if (this.running) {
24545
+ await this.tick();
23997
24546
  return;
23998
24547
  }
23999
- 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
+ }
24000
24555
  }
24001
24556
  /**
24002
24557
  * A repair's verdict never replaces the verdict the task's own work earned,
@@ -24264,11 +24819,17 @@ ${summary || "(empty: no output captured)"}`);
24264
24819
  const resumes = this.awaitsResume;
24265
24820
  this.retryCounts.set(taskId, (this.retryCounts.get(taskId) ?? 0) + 1);
24266
24821
  const ended = this.endAttempt(taskId, "retry");
24822
+ const movedFrom = this.movedFrom(task);
24267
24823
  this.store.retry(taskId);
24824
+ if (movedFrom) this.store.setCarriedFrom(taskId, movedFrom);
24268
24825
  this.store.unblockDependents(taskId);
24269
24826
  this.onHold.delete(taskId);
24270
24827
  this.emit("onTaskChanged");
24271
- 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
+ }
24272
24833
  if (admittedAt !== this.stopGeneration) return;
24273
24834
  if (resumes) await this.start(origin);
24274
24835
  else await this.tick();
@@ -24319,6 +24880,16 @@ ${summary || "(empty: no output captured)"}`);
24319
24880
  this.emit("onTaskChanged");
24320
24881
  return true;
24321
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
+ }
24322
24893
  /**
24323
24894
  * Run before writing assignments so cancellation sees the old runner's
24324
24895
  * shared wait. A session cannot cross runners, so the next attempt is a
@@ -24376,7 +24947,7 @@ ${summary || "(empty: no output captured)"}`);
24376
24947
  const summary = this.output.finalText(taskId);
24377
24948
  this.endAttempt(taskId, "usage-limit");
24378
24949
  this.pauseOnLimit(task, attempt, attempt.turnError ?? "");
24379
- 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));
24380
24951
  if (attempt.worktree) await this.runs.release(taskId, { keep: true });
24381
24952
  await this.afterVerdict(taskId);
24382
24953
  }
@@ -24475,10 +25046,14 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
24475
25046
  return false;
24476
25047
  }
24477
25048
  /**
24478
- * Manually start a single AI task right now, bypassing dependency/readiness
24479
- * gating (the "force start" affordance on a task card) but never the claim:
24480
- * it still needs the user's approval, a free slot, and no Stop since it was
24481
- * 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.
24482
25057
  * Reuses the scheduler's own claim so a force-started task gets the same
24483
25058
  * augmented prompt, session tracking, and exit handling — callers must not
24484
25059
  * re-spawn the runner themselves. No-op if the task is unknown, not an AI
@@ -24489,8 +25064,12 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
24489
25064
  if (!task || task.type !== "ai") return;
24490
25065
  if (this.attempts.has(taskId)) return;
24491
25066
  this.refuseOpsDuringMerge(task);
25067
+ const queued2 = this.queuedBehindDependencies(task);
25068
+ if (queued2) return queued2;
24492
25069
  await this.whileAdmitted(async (admittedAt) => {
24493
- 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;
24494
25073
  if (this.attempts.has(taskId)) return;
24495
25074
  this.refuseOpsDuringMerge(task);
24496
25075
  this.refuseClaim(task, admittedAt);
@@ -24499,6 +25078,68 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
24499
25078
  await this.claimed(task, admittedAt);
24500
25079
  });
24501
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
+ }
24502
25143
  refuseOpsDuringMerge(task) {
24503
25144
  if (this.merging && mergeExcludes(this.nextAttemptKind(task.id))) {
24504
25145
  throw new TaskControlError(`A Merge all is under way: ops task "${task.title}" can start once it has finished.`);
@@ -24563,6 +25204,7 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
24563
25204
  async tick() {
24564
25205
  const admittedAt = this.stopGeneration;
24565
25206
  this.recoverOrphans();
25207
+ await this.startQueued();
24566
25208
  if (!this.running) {
24567
25209
  if (this.attempts.size === 0 && this.runs.isOpen && this.settled) await this.closeRun();
24568
25210
  return;
@@ -24719,7 +25361,7 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
24719
25361
  if (refused3) return { refused: refused3 };
24720
25362
  const attempt = {
24721
25363
  taskId: task.id,
24722
- 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,
24723
25365
  phase: "starting",
24724
25366
  session: null,
24725
25367
  runner: this.store.resolveTaskRunner(task),
@@ -24733,6 +25375,7 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
24733
25375
  skills: [],
24734
25376
  autoResumed: opts.autoResumed === true,
24735
25377
  turnError: null,
25378
+ turnOutputFrom: 0,
24736
25379
  reminded: false
24737
25380
  };
24738
25381
  this.spawnCounts.set(task.id, attempt.attempt);
@@ -24806,6 +25449,8 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
24806
25449
  title: task.title,
24807
25450
  env,
24808
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,
24809
25454
  attempt: attempt.attempt,
24810
25455
  ...attempt.skills.length > 0 ? { skills: attempt.skills } : {},
24811
25456
  onNotice: (message) => this.tell("warn", `Task "${task.title}": ${message}`)
@@ -24830,8 +25475,21 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
24830
25475
  this.output.attach(task.id, session);
24831
25476
  this.verifier.watch(task, session);
24832
25477
  session.onEvent((event) => {
24833
- if (event.type === "turn_start") attempt.turnError = null;
24834
- 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
+ }
24835
25493
  });
24836
25494
  session.onTurnEnd((reason) => this.onTurnEnd(task.id, attempt, session, reason));
24837
25495
  return true;
@@ -24841,6 +25499,16 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
24841
25499
  return false;
24842
25500
  }
24843
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
+ }
24844
25512
  if (attempt.kind.kind === "continuation") {
24845
25513
  const refused3 = err instanceof RunnerStartupExit;
24846
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)}`);
@@ -24960,12 +25628,13 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
24960
25628
  endAttempt(taskId, reason) {
24961
25629
  const attempt = this.attempts.get(taskId);
24962
25630
  this.attempts.delete(taskId);
25631
+ const session = attempt?.session ?? null;
25632
+ const nativeSessionId = session?.nativeSessionId() ?? null;
24963
25633
  if (attempt) {
24964
- 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);
24965
25635
  this.output.detach(taskId);
24966
25636
  }
24967
- const session = attempt?.session ?? null;
24968
- if (session) this.saveNativeSession(taskId, session.nativeSessionId());
25637
+ if (attempt && session) this.saveNativeSession(taskId, nativeSessionId, attempt.runner);
24969
25638
  if (stopsRunner(reason)) {
24970
25639
  const task = this.store.get(taskId);
24971
25640
  if (task) this.verifier.clear(task);
@@ -24974,8 +25643,8 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
24974
25643
  return attempt;
24975
25644
  }
24976
25645
  /** Kept on the task once the attempt ends, so a continue can resume it after a reload (ADR-0018, K1). */
24977
- saveNativeSession(taskId, nativeSessionId) {
24978
- if (nativeSessionId) this.store.setTaskRunnerSessionId(taskId, nativeSessionId);
25646
+ saveNativeSession(taskId, nativeSessionId, runner) {
25647
+ if (nativeSessionId) this.store.setTaskRunnerSessionId(taskId, nativeSessionId, runner);
24979
25648
  }
24980
25649
  endAllAttempts(reason) {
24981
25650
  const ended = [...this.attempts.values()];
@@ -24985,13 +25654,17 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
24985
25654
  return ended;
24986
25655
  }
24987
25656
  };
24988
- var RESUME_MARGIN_MS = 6e4;
24989
- var RELAPSE_MS = 5 * 6e4;
24990
25657
  var LIMIT_RESET_MESSAGE = "Your usage limit has reset. Carry on with the task from where you stopped.";
24991
25658
  var OPS_RETRY_TAIL_LINES = 60;
24992
25659
  function unresumedMessage(task, why) {
24993
25660
  return `Could not continue task "${task.title}": ${why}. Retry starts it afresh.`;
24994
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
+ }
24995
25668
 
24996
25669
  // src/services/Planner.ts
24997
25670
  function plannerModesWith(req) {
@@ -25149,21 +25822,18 @@ var ModelResolver = class {
25149
25822
  return this.cachedPickerOptions ?? [];
25150
25823
  }
25151
25824
  /**
25152
- * The planner model's context window, from whatever catalog is already
25153
- * cached (#49): a vendor model comes from the picker catalog, a harness
25154
- * planner's model from the runner's own discovered models. Reads only —
25155
- * never triggers a fetch or discovery of its own, so an unknown window stays
25156
- * 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.
25157
25832
  */
25158
- contextWindowFor(modelId) {
25159
- if (!modelId) return void 0;
25160
- const picker = this.cachedPickerOptions?.find((o) => o.id === modelId)?.contextWindow;
25161
- if (picker && picker > 0) return picker;
25162
- for (const runner of this.config.enabledRunners ?? []) {
25163
- const found = this.discovery.getCached(runner)?.find((m) => m.modelId === modelId)?.contextWindow;
25164
- if (found && found > 0) return found;
25165
- }
25166
- 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;
25167
25837
  }
25168
25838
  invalidate() {
25169
25839
  this.discovery.clear();
@@ -25667,6 +26337,8 @@ var PlannerSession = class {
25667
26337
  * outlives the session — nothing from one goal may reach the next.
25668
26338
  */
25669
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;
25670
26342
  /** Set once the first spawn attached. */
25671
26343
  start = null;
25672
26344
  /**
@@ -25857,6 +26529,7 @@ var CliAgentAiService = class {
25857
26529
  // hit its rate limit, or lost its login must say so in the chat rather
25858
26530
  // than leave an empty planner bubble.
25859
26531
  failure: turn.error,
26532
+ usageLimit: turn.usageLimit,
25860
26533
  fullText: [...carried, turn.text].filter((part) => part.trim()).join("\n\n")
25861
26534
  };
25862
26535
  };
@@ -25923,6 +26596,7 @@ var CliAgentAiService = class {
25923
26596
  };
25924
26597
  const subagentUsage = /* @__PURE__ */ new Map();
25925
26598
  let error;
26599
+ let usageLimit;
25926
26600
  let stepIndex = 0;
25927
26601
  let backgroundAgents = 0;
25928
26602
  const truncate = (value) => value.length > LOG_MAX_CHARS ? `${value.slice(0, LOG_MAX_CHARS)}
@@ -26031,6 +26705,7 @@ var CliAgentAiService = class {
26031
26705
  }
26032
26706
  case "error":
26033
26707
  error = event.message;
26708
+ usageLimit = event.usageLimit;
26034
26709
  return;
26035
26710
  case "turn_end":
26036
26711
  return;
@@ -26049,12 +26724,13 @@ var CliAgentAiService = class {
26049
26724
  settle(id, "The agent ended the turn without reporting this call's result.", false, "not_executed");
26050
26725
  }
26051
26726
  session.nativeSessionId = adapter.nativeSessionId() ?? session.nativeSessionId;
26727
+ session.sessionCost = adapter.sessionCost?.() ?? session.sessionCost;
26052
26728
  if (error || signal?.aborted) session.stop();
26053
26729
  if (error) {
26054
26730
  const failure = failureAfterReply(text.slice(finalRunStart), Math.max(blockStart, finalRunStart) - finalRunStart, error);
26055
26731
  onProgress({ type: "text_retracted", segmentId: finalSegment });
26056
26732
  text = text.slice(0, finalRunStart);
26057
- return { text, researchLog, error: failure, aborted: signal?.aborted, backgroundAgents };
26733
+ return { text, researchLog, error: failure, usageLimit, aborted: signal?.aborted, backgroundAgents };
26058
26734
  }
26059
26735
  return { text, researchLog, error, aborted: signal?.aborted, backgroundAgents };
26060
26736
  }
@@ -26110,7 +26786,7 @@ var CliAgentAiService = class {
26110
26786
  session.stop();
26111
26787
  const start = session.start;
26112
26788
  if (!start) throw new Error("No active planner conversation.");
26113
- 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);
26114
26790
  }
26115
26791
  /**
26116
26792
  * What the planner gets of the repository's config, its runner being
@@ -26122,9 +26798,9 @@ var CliAgentAiService = class {
26122
26798
  const reader = connectorFor(this.runner)?.repoConfig;
26123
26799
  if (!reader) return { systemPrompt };
26124
26800
  const found = reader.projectServers(cwd);
26125
- 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 });
26126
26802
  return {
26127
- 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"),
26128
26804
  ...started.length > 0 ? { projectServers: started } : {}
26129
26805
  };
26130
26806
  }
@@ -26289,6 +26965,10 @@ var SessionCatalog = class {
26289
26965
  allowlist() {
26290
26966
  return this.deps.settings().modelAllowlist ?? {};
26291
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
+ }
26292
26972
  /** The models the user allows on one runner; unset means no restriction. */
26293
26973
  allowlistFor(runner) {
26294
26974
  return this.allowlist()[runner];
@@ -26303,7 +26983,7 @@ var SessionCatalog = class {
26303
26983
  */
26304
26984
  models() {
26305
26985
  const out = { ...this.discovered };
26306
- 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()])) {
26307
26987
  const cached2 = this.deps.modelResolver.getCachedRunnerModels(runner);
26308
26988
  if (cached2.length > 0) out[runner] = cached2;
26309
26989
  }
@@ -26340,9 +27020,16 @@ var SessionCatalog = class {
26340
27020
  * asked for.
26341
27021
  */
26342
27022
  async live() {
26343
- const runners = this.enabledRunners();
26344
- await this.discover(runners);
26345
- 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());
26346
27033
  }
26347
27034
  queryCatalog(runners) {
26348
27035
  const registered = runners.filter((runner) => this.deps.registry.getManifest(runner) !== void 0);
@@ -26368,7 +27055,8 @@ var SessionCatalog = class {
26368
27055
  modelsByRunner: this.models(),
26369
27056
  runnerModes: this.modes(runners),
26370
27057
  perRunnerAllowlist: this.allowlist(),
26371
- autonomousDefault: this.deps.config.autonomousMode
27058
+ autonomousDefault: this.deps.config.autonomousMode,
27059
+ ...this.deps.liveRunner ? { liveRunner: this.deps.liveRunner } : {}
26372
27060
  };
26373
27061
  }
26374
27062
  /** What a runner offers, as `runnerAssignment` needs it. Spawns the runner's CLI to list models. */
@@ -26433,6 +27121,108 @@ function forkPlanState(plan, tasks, dialogue, now) {
26433
27121
  };
26434
27122
  }
26435
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
+
26436
27226
  // src/services/PlanTurns.ts
26437
27227
  var START_HOLD_TIMEOUT_MS = 5 * 6e4;
26438
27228
  function turnPrompt(batch) {
@@ -26449,6 +27239,16 @@ var PlanTurns = class {
26449
27239
  constructor(host, holdTimeoutMs = START_HOLD_TIMEOUT_MS) {
26450
27240
  this.host = host;
26451
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
+ });
26452
27252
  }
26453
27253
  host;
26454
27254
  holdTimeoutMs;
@@ -26461,6 +27261,33 @@ var PlanTurns = class {
26461
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. */
26462
27262
  quiet = false;
26463
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
+ }
26464
27291
  /** Whether a turn this module opened is in flight. */
26465
27292
  get busy() {
26466
27293
  return this.running !== null;
@@ -26502,25 +27329,33 @@ var PlanTurns = class {
26502
27329
  admit(input, forced) {
26503
27330
  const plan = this.host.plan();
26504
27331
  if (!plan) return Promise.reject(new Error("No planning conversation to continue"));
26505
- const entry = {
26506
- id: `turn-${Date.now()}-${++this.seq}`,
26507
- source: input.source,
26508
- text: input.text,
26509
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
26510
- ...input.skills && input.skills.length > 0 ? { skills: [...input.skills] } : {},
26511
- ...forced ? { forced: true } : {}
26512
- };
27332
+ const entry = this.pending(input, forced);
26513
27333
  this.quiet = false;
27334
+ if (this.resend.arrived()) this.host.announce();
26514
27335
  const pending = plan.pendingTurns ?? [];
26515
27336
  const at = forced ? pending.findIndex((t) => !t.forced && !this.running?.ids.has(t.id)) : -1;
26516
27337
  plan.pendingTurns = at === -1 ? [...pending, entry] : [...pending.slice(0, at), entry, ...pending.slice(at)];
26517
27338
  this.host.persist();
27339
+ if (input.source === "user" && this.resend.holds()) {
27340
+ this.pump();
27341
+ return Promise.resolve(plan);
27342
+ }
26518
27343
  const settled = new Promise((resolve9, reject) => {
26519
27344
  this.waiters.set(entry.id, { resolve: resolve9, reject, signal: input.signal });
26520
27345
  });
26521
27346
  this.pump();
26522
27347
  return settled;
26523
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
+ }
26524
27359
  /**
26525
27360
  * Start the next turn if one is due: nothing of ours is in flight, the
26526
27361
  * conversation is free, and inputs wait. The host calls it when any planner
@@ -26531,7 +27366,7 @@ var PlanTurns = class {
26531
27366
  * the plan, what still waits, and the planner in use at that moment.
26532
27367
  */
26533
27368
  pump() {
26534
- if (this.running || this.detecting) return;
27369
+ if (this.running || this.detecting || this.resend.holds()) return;
26535
27370
  const plan = this.host.plan();
26536
27371
  if (!plan || this.host.conversationBusy()) return;
26537
27372
  const batch = this.liveBatch(plan);
@@ -26611,6 +27446,7 @@ var PlanTurns = class {
26611
27446
  * conversation's to abandon; whatever it settles lands nothing here.
26612
27447
  */
26613
27448
  abandon() {
27449
+ this.resend.reset();
26614
27450
  this.running?.release();
26615
27451
  this.running = null;
26616
27452
  this.quiet = false;
@@ -26654,8 +27490,10 @@ var PlanTurns = class {
26654
27490
  clearTimeout(timeout);
26655
27491
  if (this.running !== running) return;
26656
27492
  const forceStopped = !outcome.ok && running.forceStopped === true;
27493
+ const limited = !outcome.ok && outcome.err instanceof PlannerUsageLimitError;
27494
+ if (outcome.ok) this.resend.landed();
26657
27495
  try {
26658
- 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));
26659
27497
  const took = this.take(plan, dropped);
26660
27498
  if (forceStopped) plan.pendingTurns = [...(plan.pendingTurns ?? []).filter((t) => t.forced), ...(plan.pendingTurns ?? []).filter((t) => !t.forced)];
26661
27499
  if (took || forceStopped) this.host.persist();
@@ -26669,7 +27507,11 @@ var PlanTurns = class {
26669
27507
  const ended = outcome;
26670
27508
  const waiting = new Set((plan.pendingTurns ?? []).map((t) => t.id));
26671
27509
  for (const t of batch) {
26672
- 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;
26673
27515
  const delivered = this.settle(t.id, (w) => ended.ok ? w.resolve(ended.plan) : w.reject(ended.err));
26674
27516
  if (!delivered && !ended.ok && !isInterruption(ended.err) && t.source === "user") this.host.notice(replayFailed(t, ended.err));
26675
27517
  }
@@ -26728,7 +27570,6 @@ var RunSupervision = class {
26728
27570
  blockedBy: (failedId) => [...blockingFailures([...this.host.tasks()])].filter(([, failures]) => failures.includes(failedId)).map(([taskId]) => taskId),
26729
27571
  readBudget: () => this.settings()?.wakeUpReadBudget ?? host.userSettings().wakeUpReadBudget,
26730
27572
  retryLimit: () => this.retryLimit(),
26731
- plannerRunner: () => host.plannerRunner(),
26732
27573
  restartPlanner: () => host.restartPlanner(),
26733
27574
  abandonTurn: () => host.abandonTurn(),
26734
27575
  goUnsupervised: (reason) => this.goUnsupervised(reason),
@@ -27209,8 +28050,8 @@ function dropLegacyFile(sessionsDir, meta) {
27209
28050
  const legacy = path30.join(sessionsDir, legacySessionFilename(meta));
27210
28051
  if (legacy === path30.join(sessionsDir, sessionFilename(meta)) || !fs14.existsSync(legacy)) return;
27211
28052
  try {
27212
- const held = JSON.parse(fs14.readFileSync(legacy, "utf-8")).meta?.id;
27213
- 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);
27214
28055
  } catch {
27215
28056
  }
27216
28057
  }
@@ -27343,24 +28184,25 @@ function listTaskLogAttempts(location, taskId) {
27343
28184
  }
27344
28185
  return names.map((name) => /^([1-9]\d*)\.jsonl$/.exec(name)?.[1]).filter((n) => n !== void 0).map(Number).sort((a, b) => a - b);
27345
28186
  }
27346
- function openTaskLog(location, taskId) {
28187
+ function openTaskLog(location, taskId, attempt) {
27347
28188
  ensureStateDirIgnored(location.baseDir);
27348
28189
  const dir = taskDir(location, taskId);
27349
28190
  fs15.mkdirSync(dir, { recursive: true });
27350
- 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;
27351
28193
  for (; ; ) {
27352
- const file = path31.join(dir, `${attempt}${EXTENSION}`);
28194
+ const file = path31.join(dir, `${number}${EXTENSION}`);
27353
28195
  try {
27354
28196
  fs15.writeFileSync(file, "", { flag: "wx" });
27355
28197
  } catch (err) {
27356
28198
  if (err.code === "EEXIST") {
27357
- attempt += 1;
28199
+ number = Math.max(number + 1, next);
27358
28200
  continue;
27359
28201
  }
27360
28202
  throw err;
27361
28203
  }
27362
28204
  return {
27363
- attempt,
28205
+ attempt: number,
27364
28206
  append(events) {
27365
28207
  if (events.length === 0) return;
27366
28208
  fs15.appendFileSync(file, events.map((e) => `${JSON.stringify(e)}
@@ -27406,11 +28248,13 @@ var TaskLogRecorder = class {
27406
28248
  open;
27407
28249
  flushMs;
27408
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();
27409
28253
  wrap(runner) {
27410
28254
  return {
27411
28255
  spawn: async (opts) => {
27412
28256
  const session = await runner.spawn(opts);
27413
- this.record(opts.taskId, session, opts.skills);
28257
+ this.record(opts.taskId, session, opts.skills, opts.attempt);
27414
28258
  return session;
27415
28259
  },
27416
28260
  stop: (sessionId) => runner.stop(sessionId),
@@ -27420,11 +28264,20 @@ var TaskLogRecorder = class {
27420
28264
  }
27421
28265
  };
27422
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
+ }
27423
28276
  /** Start a new attempt's log for `session`, opening with the skills it was given, and keep it until the session exits. */
27424
- record(taskId, session, skills = []) {
28277
+ record(taskId, session, skills = [], attemptNumber) {
27425
28278
  let file;
27426
28279
  try {
27427
- file = this.open(this.deps.location(), taskId);
28280
+ file = this.open(this.deps.location(), taskId, attemptNumber);
27428
28281
  } catch (err) {
27429
28282
  this.logger.warn("taskLog", `could not create the log of task ${taskId}; it streams but is not saved`, err);
27430
28283
  file = null;
@@ -27451,6 +28304,10 @@ var TaskLogRecorder = class {
27451
28304
  }
27452
28305
  this.deps.broadcast({ type: "task_log", taskId, attempt, events: taskLogForSurface(events) });
27453
28306
  };
28307
+ this.latest.set(taskId, { sessionId: session.id, note: (entry) => {
28308
+ pending.push(entry);
28309
+ flush();
28310
+ } });
27454
28311
  const openCalls = /* @__PURE__ */ new Set();
27455
28312
  session.onEvent((event) => {
27456
28313
  const entry = toTaskLogEvent(event);
@@ -27471,21 +28328,82 @@ var TaskLogRecorder = class {
27471
28328
  }
27472
28329
  };
27473
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
+
27474
28383
  // src/services/PlannerUsage.ts
27475
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;
27476
28391
  usage = { totals: {} };
28392
+ windows;
27477
28393
  /** Fold one model call's record into the totals; returns the running ledger. */
27478
28394
  record(record) {
27479
- this.usage = addPlannerUsage(this.usage, record);
28395
+ this.usage = addPlannerUsage(this.usage, this.windows.fill(record));
27480
28396
  return this.usage;
27481
28397
  }
27482
28398
  /** Adopt a persisted ledger (a reopened session) or start from zero. */
27483
28399
  restore(usage) {
27484
28400
  this.usage = usage ?? { totals: {} };
28401
+ this.windows = new ContextWindowFill(this.catalogWindow, this.usage);
27485
28402
  }
27486
28403
  /** Start a fresh session's ledger from zero. */
27487
28404
  clear() {
27488
28405
  this.usage = { totals: {} };
28406
+ this.windows = new ContextWindowFill(this.catalogWindow);
27489
28407
  }
27490
28408
  /** Whether anything has been recorded — a plan with no usage says nothing. */
27491
28409
  get hasUsage() {
@@ -27580,19 +28498,54 @@ var PlanEditor = class {
27580
28498
  * between AI and MAN, are the patches that can leave a task incoherent
27581
28499
  * (unschedulable, or carrying fields that mean nothing for its new type),
27582
28500
  * so both go through the same {@link validateTaskEdit} guard the planner's
27583
- * task-ops applier uses (as the 'direct' actor, which skips the lock rule)
27584
- * rather than a second copy of the rules — and throws, so the surface can
27585
- * 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
27586
28504
  * falls through to the no-op `store.update` below instead of throwing.
27587
28505
  * Attached skills meet the rule a planner's do: a planner skill is refused,
27588
- * 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.
27589
28509
  *
27590
28510
  * Edits to one task commit in call order: the skill check waits on git, so
27591
28511
  * two quick chip edits (`[a]` then `[a, b]`) could otherwise land the first
27592
28512
  * one last.
27593
28513
  */
27594
28514
  updateTask(taskId, changes) {
27595
- 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);
27596
28549
  const settled = edit.catch(() => {
27597
28550
  });
27598
28551
  this.taskEdits.set(taskId, settled);
@@ -27608,22 +28561,31 @@ var PlanEditor = class {
27608
28561
  const target = this.store.get(taskId);
27609
28562
  const skills = "skills" in changes && target ? await checkTaskSkillsEdit(target, typedSkills, this.taskSkills()) : void 0;
27610
28563
  if (skills?.errors.length) throw new PlanEditError(skills.errors.map((e) => e.message).join(" "));
27611
- 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) {
27612
28565
  const check2 = validateTaskEdit("direct", this.store.planTasks, taskId, changes, this.catalog.edit());
27613
28566
  if (!check2.ok) throw new PlanEditError(check2.error ?? "Those changes are not valid");
27614
28567
  if (check2.clear?.length || check2.set) {
27615
28568
  changes = { ...changes, ...Object.fromEntries((check2.clear ?? []).map((f) => [f, void 0])), ...check2.set };
27616
28569
  }
27617
28570
  }
27618
- 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(
27619
28583
  () => {
27620
28584
  changes = this.scheduler.releaseMovedResume(taskId, changes);
27621
28585
  return Boolean(this.store.update(taskId, changes));
27622
28586
  },
27623
28587
  () => this.broadcast({ type: "task_updated", taskId, changes })
27624
28588
  );
27625
- if (plan) for (const warning of skills?.warnings ?? []) this.notice("warn", warning);
27626
- return plan;
27627
28589
  }
27628
28590
  /**
27629
28591
  * Replace one task's dependency list — the named entry point the surfaces'
@@ -27640,7 +28602,8 @@ var PlanEditor = class {
27640
28602
  * because a runner change is never a single-field edit: the task's model,
27641
28603
  * thinking effort and mode are all scoped to its runner, so they are
27642
28604
  * re-derived from the new runner's catalog (see {@link retargetTaskRunner}).
27643
- * 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.
27644
28607
  *
27645
28608
  * The runner is also admitted into `plan.runners` — see {@link admitRunner}.
27646
28609
  */
@@ -27650,11 +28613,13 @@ var PlanEditor = class {
27650
28613
  const task = this.store.get(taskId);
27651
28614
  if (!task) return null;
27652
28615
  if (task.assignedRunner === runner || task.type === "user") return plan;
28616
+ this.refuseLiveRetarget(task);
27653
28617
  const catalog = await this.catalog.runner(runner);
27654
28618
  const from = this.catalog.edit([task.assignedRunner]).runnerModes[task.assignedRunner];
27655
28619
  const changes = retargetTaskRunner(task, runner, this.allowedCatalog(catalog, runner), from);
27656
28620
  if (Object.keys(changes).length === 0) return this.plan();
27657
28621
  this.refuseUnavailableMode(runner, changes.taskMode, catalog.modes);
28622
+ this.refuseLiveRetarget(task);
27658
28623
  return this.commit(() => {
27659
28624
  if (!this.store.update(taskId, this.scheduler.releaseMovedResume(taskId, changes))) return false;
27660
28625
  this.admitRunner(runner, catalog.models);
@@ -27688,6 +28653,9 @@ var PlanEditor = class {
27688
28653
  return true;
27689
28654
  });
27690
28655
  }
28656
+ refuseLiveRetarget(task) {
28657
+ if (this.catalog.edit().liveRunner?.(task.id)) throw new PlanEditError(liveRunnerRefusal(task));
28658
+ }
27691
28659
  /**
27692
28660
  * A mode the host cannot run is refused, with its reason, not swapped for
27693
28661
  * another: the plan is the source of truth for what runs (ADR-0001), and a
@@ -28017,6 +28985,10 @@ var ExecutionCommands = class {
28017
28985
  const all = this.all(tasks);
28018
28986
  const live = all.filter(({ task }) => this.host.running(task.id));
28019
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
+ }
28020
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.` };
28021
28993
  }
28022
28994
  const toRun = all.filter(({ task }) => task.type === "ai" && task.status !== "completed" && task.status !== "skipped");
@@ -28442,8 +29414,11 @@ function mergeStopped(result) {
28442
29414
  switch (result.outcome) {
28443
29415
  case "merged":
28444
29416
  return "it merged";
28445
- case "blocked":
28446
- 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
+ }
28447
29422
  case "conflict":
28448
29423
  return `it conflicts in ${result.repo}${result.files?.length ? `: ${result.files.slice(0, 5).join(", ")}` : ""}`;
28449
29424
  case "failed":
@@ -28568,11 +29543,17 @@ function liveAiService(config, workspaceRoot, mcpServer) {
28568
29543
  function isPlannerApproval(request) {
28569
29544
  return !isRunnerApproval(request);
28570
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
+ }
28571
29551
  function createSession(deps) {
28572
29552
  const pinnedAiService = deps.aiService;
28573
29553
  const mcpServer = deps.mcpServer ?? sharedMcpServer();
28574
29554
  const aiService = pinnedAiService ? () => pinnedAiService : liveAiService(deps.config, deps.workspaceRoot, mcpServer);
28575
29555
  const store = new PlanStore();
29556
+ const catalogWindow = (source, model) => deps.modelResolver.contextWindowFor?.(source, model);
28576
29557
  const taskLogs = new TaskLogRecorder({
28577
29558
  broadcast: deps.broadcast,
28578
29559
  // Read through the Session, whose id and workspace are the ones it saves
@@ -28606,7 +29587,7 @@ function createSession(deps) {
28606
29587
  const orchestrator = TaskOrchestrator.compose({
28607
29588
  config: deps.config,
28608
29589
  notifications: deps.notifications,
28609
- runner: taskLogs.wrap(runnerApprovals.wrap(deps.runner)),
29590
+ runner: taskLogs.wrap(runnerApprovals.wrap(withContextWindows(deps.runner, catalogWindow))),
28610
29591
  store,
28611
29592
  output: deps.taskOutput,
28612
29593
  isolation: deps.isolation,
@@ -28614,10 +29595,11 @@ function createSession(deps) {
28614
29595
  workspaceRoot: deps.workspaceRoot,
28615
29596
  skillsAt: (roots) => (deps.skillsService ?? new SkillsService(roots)).forRoot(roots),
28616
29597
  previousAttemptFromLog: (taskId) => lastAttemptDigest(session.taskLogLocation, taskId),
29598
+ lastLoggedAttempt: (taskId) => session.taskLogAttempts(taskId).at(-1) ?? 0,
28617
29599
  runSettings: () => session.planState?.runSettings,
28618
29600
  isApproved: (taskId) => session.isApproved(taskId)
28619
29601
  });
28620
- const usage = new PlannerUsageLedger();
29602
+ const usage = new PlannerUsageLedger(catalogWindow);
28621
29603
  const events = new SessionEventRelay({
28622
29604
  broadcast: deps.broadcast,
28623
29605
  onNotice: deps.onNotice,
@@ -28666,7 +29648,8 @@ function createSession(deps) {
28666
29648
  modelResolver: deps.modelResolver,
28667
29649
  settings: deps.settings,
28668
29650
  // Read through the Session, which holds the plan; the catalog is built first.
28669
- planRunners: () => session.planState?.runners ?? []
29651
+ planRunners: () => session.planState?.runners ?? [],
29652
+ liveRunner: (taskId) => orchestrator.getAttempt(taskId) !== void 0
28670
29653
  });
28671
29654
  const session = new Session({
28672
29655
  config: deps.config,
@@ -28688,6 +29671,7 @@ function createSession(deps) {
28688
29671
  usage,
28689
29672
  approvals,
28690
29673
  approvalPolicy,
29674
+ taskLogs,
28691
29675
  // Every planner's web reads, carried here or natively, are decided by the
28692
29676
  // one policy, which asks through the same channel as paths and commands.
28693
29677
  // The response cache and search provider are process-wide, so parallel
@@ -28738,6 +29722,7 @@ var Session = class {
28738
29722
  fsAdapter;
28739
29723
  approvals;
28740
29724
  approvalPolicy;
29725
+ taskLogs;
28741
29726
  fetcher;
28742
29727
  settingsFn;
28743
29728
  save;
@@ -28773,7 +29758,7 @@ var Session = class {
28773
29758
  turnId: () => this.conversation.currentTurnId,
28774
29759
  recordSkillLoad: (skill) => this.conversation.recordPlannerSkill(skill),
28775
29760
  liveCatalog: () => this.liveCatalog(),
28776
- coerce: (tasks) => coerceAssignments(tasks, this.catalog.allowlist(), this.catalog.models()),
29761
+ coerce: (tasks) => coerceAssignments(tasks, this.catalog.allowlist(), this.catalog.models(), this.store.planTasks),
28777
29762
  submitPlan: (tasks) => this.submitPlanFromTool(tasks),
28778
29763
  editPlan: (ops) => this.editPlanFromTool(ops),
28779
29764
  tasks: () => this.store.planTasks,
@@ -28818,6 +29803,7 @@ var Session = class {
28818
29803
  this.usage = parts.usage;
28819
29804
  this.approvals = parts.approvals;
28820
29805
  this.approvalPolicy = parts.approvalPolicy;
29806
+ this.taskLogs = parts.taskLogs;
28821
29807
  this.fetcher = parts.fetcher;
28822
29808
  this.skillsService = parts.skillsService;
28823
29809
  this.save = parts.saveSession;
@@ -28827,7 +29813,8 @@ var Session = class {
28827
29813
  aiService: () => this.aiService(),
28828
29814
  onProgress: (p) => this.events.progress(p),
28829
29815
  opening: (runners) => this.conversationOpening(runners),
28830
- catalog: () => this.catalog.queryCatalog(this.plan?.runners ?? []),
29816
+ catalog: () => this.catalog.current(),
29817
+ assignmentScope: () => this.catalog.scope(),
28831
29818
  autorun: () => this.userRunSettings().autorun,
28832
29819
  takeRunUpdates: () => this.untoldRunUpdates.splice(0),
28833
29820
  tasks: () => this.store.planTasks,
@@ -28847,13 +29834,18 @@ var Session = class {
28847
29834
  turnAborted: () => this.approvals.clear(isPlannerApproval),
28848
29835
  // After the turn has announced its end, so the next one never starts
28849
29836
  // ahead of it on a surface.
28850
- turnReleased: () => queueMicrotask(() => this.turns.pump())
29837
+ turnReleased: () => queueMicrotask(() => this.turns.pump()),
29838
+ usageLimit: (said2) => this.turns.usageLimit(said2)
28851
29839
  });
28852
29840
  this.turns = new PlanTurns({
28853
29841
  plan: () => this.closed ? null : this.plan,
28854
29842
  conversationBusy: () => this.conversation.isTurnInFlight,
28855
29843
  mayWake: () => this.supervision.mayWake(),
28856
29844
  persist: () => this.persist({ background: true }),
29845
+ announce: () => {
29846
+ this.persist({ background: true });
29847
+ this.events.planGenerated(this.plan, this.goal);
29848
+ },
28857
29849
  interrupt: () => {
28858
29850
  this.conversation.stopTurn("force_send");
28859
29851
  },
@@ -28870,7 +29862,8 @@ var Session = class {
28870
29862
  },
28871
29863
  notice: (message) => this.notice("error", message),
28872
29864
  plannerDetecting: () => installedPlannerAnswered() ? null : installedPlannerDetected(),
28873
- assertPlannerReady: () => this.assertPlannerReady()
29865
+ assertPlannerReady: () => this.assertPlannerReady(),
29866
+ plannerRunner: () => runnerForProvider(this.config.aiProvider)
28874
29867
  });
28875
29868
  this.supervision = new RunSupervision({
28876
29869
  plan: () => this.plan,
@@ -28888,7 +29881,6 @@ var Session = class {
28888
29881
  },
28889
29882
  deliver: (digest) => this.turns.enqueue({ source: "supervisor", text: digest }),
28890
29883
  pump: () => this.turns.pump(),
28891
- plannerRunner: () => runnerForProvider(this.config.aiProvider),
28892
29884
  restartPlanner: () => this.aiService().restart?.(),
28893
29885
  abandonTurn: () => this.conversation.abandonHungTurn(),
28894
29886
  notice: (level, message) => this.notice(level, message)
@@ -28970,6 +29962,7 @@ var Session = class {
28970
29962
  tasks: () => this.plan ? this.store.planTasks : null,
28971
29963
  planApproved: () => this.plan?.approved === true,
28972
29964
  taskApproved: (taskId) => this.isApproved(taskId),
29965
+ waitsForApproval: (taskId) => this.waitsForApproval(taskId),
28973
29966
  approve: (taskIds) => this.approve(taskIds),
28974
29967
  autonomous: () => this.config.autonomousMode,
28975
29968
  autorun: () => this.userRunSettings().autorun,
@@ -28991,6 +29984,7 @@ var Session = class {
28991
29984
  blockedBy: (taskId) => this.orchestrator.getBlockedBy(taskId),
28992
29985
  waitingRequests: (taskId) => this.approvals.outstanding().filter(({ request }) => isRunnerApproval(request) && request.taskId === taskId).map(({ id, request }) => ({ id, subject: request.subject })),
28993
29986
  runPlan: (actor) => this.runWholePlan(runOrigin(actor)),
29987
+ joinRun: (approved) => this.joinRun(approved),
28994
29988
  startTask: (taskId, actor) => this.startOneTask(taskId, runOrigin(actor)),
28995
29989
  stopTask: (taskId) => this.cancelTask(taskId),
28996
29990
  stopRun: () => this.stopExecution(),
@@ -29019,8 +30013,8 @@ var Session = class {
29019
30013
  * one: nothing it would start is approved until the user executes it.
29020
30014
  */
29021
30015
  heldEdit(taskId) {
29022
- const held = this.conversation.pendingOps();
29023
- 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;
29024
30018
  if (taskId === void 0) return true;
29025
30019
  const before = this.store.get(taskId);
29026
30020
  const after = this.asEdited(taskId);
@@ -29033,10 +30027,10 @@ var Session = class {
29033
30027
  /** {@link editedTasks}, with the id each of the held batch's handles names in them. */
29034
30028
  editedPlan() {
29035
30029
  const unedited = { tasks: this.store.planTasks, handles: /* @__PURE__ */ new Map() };
29036
- const held = this.conversation.pendingOps();
29037
- 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;
29038
30032
  const runners = this.editRunners();
29039
- 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));
29040
30034
  return result.ok ? { tasks: result.tasks, handles: result.handles ?? /* @__PURE__ */ new Map() } : unedited;
29041
30035
  }
29042
30036
  /** The task as the open turn's held edit leaves it; undefined when that edit removes it. */
@@ -29060,9 +30054,9 @@ var Session = class {
29060
30054
  * rules, then, for the supervisor, the oscillation guard. `held` are the
29061
30055
  * ops the turn already holds, which `ops` join; errors are numbered by `ops`.
29062
30056
  */
29063
- checkPlannerOps(ops, held = []) {
30057
+ checkPlannerOps(ops, held2 = []) {
29064
30058
  const runners = this.editRunners();
29065
- 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);
29066
30060
  if (!result.ok || !this.supervisorEditing()) return result;
29067
30061
  const trip = this.supervision.checkEdit(this.store.planTasks, result.tasks);
29068
30062
  if (!trip) return result;
@@ -29232,6 +30226,9 @@ var Session = class {
29232
30226
  onUsageLimitWait: ({ taskId, runner, resumeAt, cancelled }) => {
29233
30227
  if (!cancelled) this.supervision.observe({ kind: "usage_limit", taskId, runner, resumeAt });
29234
30228
  },
30229
+ onModelSwitched: (switched) => {
30230
+ void this.followRunnerModel(switched);
30231
+ },
29235
30232
  onTaskIdle: ({ taskId, idleSince }) => {
29236
30233
  this.supervision.observe({ kind: "task_idle", taskId, detail: `no output since ${idleSince}` });
29237
30234
  },
@@ -29352,10 +30349,12 @@ var Session = class {
29352
30349
  * boundary rewrites `in_progress` to `pending` — nothing is running when a
29353
30350
  * session comes off a file — so a surface that re-reads the plan mid-run must
29354
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.
29355
30354
  */
29356
30355
  get currentPlanState() {
29357
30356
  if (!this.plan) return null;
29358
- const executionLog = this.store.getExecutionLog();
30357
+ const executionLog = this.store.getExecutionLog().map((s) => ({ ...s, ...this.store.get(s.id) }));
29359
30358
  const logged = new Set(executionLog.map((s) => s.id));
29360
30359
  const pendingTasks = this.store.planTasks.filter((t) => !logged.has(t.id));
29361
30360
  const record = approvalRecordOf(this.plan);
@@ -29366,7 +30365,7 @@ var Session = class {
29366
30365
  phase: "executing",
29367
30366
  history: [],
29368
30367
  message: "",
29369
- executionLog: [...executionLog],
30368
+ executionLog,
29370
30369
  pendingTasks,
29371
30370
  goal: this.goal,
29372
30371
  runners: this.plan.runners,
@@ -29536,16 +30535,20 @@ var Session = class {
29536
30535
  opened = this.openTurn(() => this.conversation.start(this.resolveSkillInvocation(goal), () => this.conversationOpening(chosenRunners), options?.signal));
29537
30536
  } catch (err) {
29538
30537
  if (this.opening === pending) this.opening = null;
29539
- for (const held2 of pending.held ?? []) held2.reject(err);
30538
+ for (const held3 of pending.held ?? []) held3.reject(err);
29540
30539
  throw err;
29541
30540
  }
29542
- const held = pending.held ?? [];
30541
+ const held2 = pending.held ?? [];
29543
30542
  pending.held = null;
29544
- 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);
29545
30544
  try {
29546
30545
  const { landed: landed2, effects } = await opened;
29547
30546
  this.autorunSubmitted(effects);
29548
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;
29549
30552
  } finally {
29550
30553
  if (this.opening === pending) this.opening = null;
29551
30554
  }
@@ -29560,10 +30563,10 @@ var Session = class {
29560
30563
  behindOpening(input, forced = false) {
29561
30564
  const pending = this.opening;
29562
30565
  if (!pending) return null;
29563
- const held = pending.held;
29564
- if (!held) return this.turns.follow(input, forced);
30566
+ const held2 = pending.held;
30567
+ if (!held2) return this.turns.follow(input, forced);
29565
30568
  return new Promise((resolve9, reject) => {
29566
- held.push({ input, forced, resolve: resolve9, reject });
30569
+ held2.push({ input, forced, resolve: resolve9, reject });
29567
30570
  });
29568
30571
  }
29569
30572
  /**
@@ -29662,12 +30665,12 @@ var Session = class {
29662
30665
  await this.liveCatalog();
29663
30666
  if (this.conversation.currentTurnId !== turn) return refuse(PLANNER_TURN_ENDED);
29664
30667
  if (!this.plan || this.store.planTasks.length === 0) return refuse("There is no plan to edit yet: submit one with submit_plan.");
29665
- const held = this.conversation.pendingOps();
29666
- 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.");
29667
- 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);
29668
30671
  if (!result.ok) return { ok: false, errors: result.errors };
29669
- if (!this.conversation.submit({ kind: "task_ops", ops: [...held, ...ops] })) return refuse("No planning turn is open to take the edit.");
29670
- 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);
29671
30674
  return { ok: true, summary: result.summary, coerced: coerced.map(({ taskId: _id, ...rest }) => rest) };
29672
30675
  }
29673
30676
  /** Whether the planner conversation is live (started and not yet committed to a plan). */
@@ -29704,9 +30707,6 @@ var Session = class {
29704
30707
  autonomousDefault: this.config.autonomousMode,
29705
30708
  autorun: this.userRunSettings().autorun,
29706
30709
  isolatedExecution: await this.runs.plannerLayout(),
29707
- // The planner's own model window, when a cached catalog knows it, so the
29708
- // usage line can show context fill (#49). Unknown stays absent.
29709
- contextWindow: this.modelResolver.contextWindowFor?.(this.config.orchestratorModel),
29710
30710
  fs: this.fsAdapter,
29711
30711
  fetcher: this.fetcher,
29712
30712
  approval: this.approvalPolicy,
@@ -29733,7 +30733,7 @@ var Session = class {
29733
30733
  for (const runner of runnersOf(tasks)) this.editor.admitRunner(runner, this.catalog.known(runner));
29734
30734
  const runners = this.plan.runners;
29735
30735
  const before = this.store.planTasks;
29736
- let coerced = coerceAssignments(tasks, this.catalog.allowlist(), this.catalog.models()).tasks;
30736
+ let coerced = coerceAssignments(tasks, this.catalog.allowlist(), this.catalog.models(), before).tasks;
29737
30737
  if (how === "commit") coerced = keepExecutionState(before, carryPlannedModes(before, coerced, this.catalog.edit(runners)));
29738
30738
  const bySupervisor = this.supervisorEditing();
29739
30739
  const { tasks: stamped, added } = stampOrigin(before, coerced, bySupervisor);
@@ -29746,6 +30746,7 @@ var Session = class {
29746
30746
  this.store.resetForRun({ keepScheduledResumes: true });
29747
30747
  }
29748
30748
  if (bySupervisor) this.approve(added);
30749
+ else if (this.conversation.currentWakeUp === void 0 && this.userRunSettings().autorun) this.approve(writtenTasks(before, coerced));
29749
30750
  return coerced.length;
29750
30751
  }
29751
30752
  preparePlannerTaskUpdates(tasks) {
@@ -29780,6 +30781,11 @@ var Session = class {
29780
30781
  if (changes.mergeAtGatesAutomatically) this.orchestrator.runSettingsChanged();
29781
30782
  }
29782
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
+ }
29783
30789
  this.idlePlan();
29784
30790
  this.approve(flattenTasks(this.store.planTasks).map((t) => t.id));
29785
30791
  await this.runWholePlan("user");
@@ -29801,6 +30807,25 @@ var Session = class {
29801
30807
  await this.orchestrator.approveReview(origin);
29802
30808
  });
29803
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
+ }
29804
30829
  async approveReview() {
29805
30830
  if (!this.plan) throw new NoPlanError("review");
29806
30831
  const plan = this.plan;
@@ -29809,9 +30834,12 @@ var Session = class {
29809
30834
  await this.withSave(() => this.orchestrator.approveReview("user"));
29810
30835
  return plan;
29811
30836
  }
30837
+ /** Answers the line saying the task waits for its dependencies, when it was queued behind them rather than started. */
29812
30838
  async forceStartTask(taskId) {
29813
30839
  this.approveTask(taskId);
29814
- await this.startOneTask(taskId, "user");
30840
+ if (!this.plan) return;
30841
+ this.requireTask(taskId);
30842
+ return this.withSave(() => this.orchestrator.forceStartTask(taskId, "user"));
29815
30843
  }
29816
30844
  async startOneTask(taskId, origin) {
29817
30845
  if (!this.plan) return;
@@ -29868,10 +30896,51 @@ var Session = class {
29868
30896
  async resumeNow(taskId) {
29869
30897
  await this.withSave(() => this.orchestrator.resumeNow(taskId, "user"));
29870
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
+ }
29871
30907
  /** Keep a task paused on a usage limit from resuming on its own; see {@link TaskOrchestrator.cancelScheduledResume}. */
29872
30908
  cancelScheduledResume(taskId) {
29873
30909
  return this.saved(() => this.orchestrator.cancelScheduledResume(taskId));
29874
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
+ }
29875
30944
  /** Said through both channels: a host shows one or the other, never both (the daemon's toasts are no-ops). */
29876
30945
  notice(level, message) {
29877
30946
  this.notifications[level](message);
@@ -29943,8 +31012,8 @@ var Session = class {
29943
31012
  if (!this.plan) return EMPTY_RUN;
29944
31013
  const planned = reduceRun(EMPTY_RUN, { type: "plan_snapshot", plan: surfacePlan({ ...this.plan, tasks: this.store.snapshot() }) });
29945
31014
  const view = reduceRun(planned, this.events.statusMessage());
29946
- const held = this.sentHeld ? reduceRun(view, { type: "plan_edit_held", held: this.sentHeld }) : view;
29947
- 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;
29948
31017
  }
29949
31018
  /** The ancestors a task waits on at its merge gate (ADR-0020); empty when it waits for no Merge all. */
29950
31019
  mergeGate(taskId) {
@@ -30103,6 +31172,7 @@ var Session = class {
30103
31172
  if (adopting) this.dropPlanScope();
30104
31173
  this.plan = plan;
30105
31174
  if (adopting) this.supervision.restore(plan.supervision);
31175
+ if (adopting) this.turns.restoreResend();
30106
31176
  this.goal = goal;
30107
31177
  this.workspace = workspace;
30108
31178
  plan.approved ??= plan.status !== "draft" && plan.status !== "rejected";
@@ -30310,6 +31380,7 @@ var StructuredSession = class extends AbstractRunnerSession {
30310
31380
  messageCount = 0;
30311
31381
  interrupting = null;
30312
31382
  lastSessionId = null;
31383
+ lastSessionCost = null;
30313
31384
  structuredEmitter = new EventEmitter2();
30314
31385
  permissionCount = 0;
30315
31386
  /**
@@ -30514,6 +31585,9 @@ var StructuredSession = class extends AbstractRunnerSession {
30514
31585
  nativeSessionId() {
30515
31586
  return this.adapter?.nativeSessionId() ?? this.lastSessionId;
30516
31587
  }
31588
+ sessionCost() {
31589
+ return this.adapter?.sessionCost?.() ?? this.lastSessionCost;
31590
+ }
30517
31591
  answerPermission(id, decision) {
30518
31592
  const open = this.permissions.get(id);
30519
31593
  if (!open) return false;
@@ -30561,6 +31635,7 @@ var StructuredSession = class extends AbstractRunnerSession {
30561
31635
  */
30562
31636
  async restartInterrupted(turn) {
30563
31637
  const resumeSessionId = this.nativeSessionId() ?? this.startOptions.resumeSessionId;
31638
+ const resumeCost = this.sessionCost() ?? this.startOptions.resumeCost;
30564
31639
  this.generation += 1;
30565
31640
  turn.abort.abort();
30566
31641
  if (this.adapter) {
@@ -30570,7 +31645,7 @@ var StructuredSession = class extends AbstractRunnerSession {
30570
31645
  for (const message of this.queue) this.requeue(message);
30571
31646
  try {
30572
31647
  this.revokeTools();
30573
- await this.startAttached({ ...this.startOptions, resumeSessionId });
31648
+ await this.startAttached({ ...this.startOptions, resumeSessionId, resumeCost });
30574
31649
  } catch (err) {
30575
31650
  this.text.line(`Could not restart ${this.launch.runner} after the interrupt: ${err instanceof Error ? err.message : String(err)}`);
30576
31651
  this.withdrawPermissions();
@@ -30597,6 +31672,7 @@ var StructuredSession = class extends AbstractRunnerSession {
30597
31672
  return;
30598
31673
  }
30599
31674
  this.lastSessionId = adapter.nativeSessionId() ?? this.lastSessionId;
31675
+ this.lastSessionCost = adapter.sessionCost?.() ?? this.lastSessionCost;
30600
31676
  this.withdrawPermissions();
30601
31677
  this.baseHandleExit(code);
30602
31678
  });
@@ -30634,6 +31710,7 @@ var StructuredSession = class extends AbstractRunnerSession {
30634
31710
  this.handleEvent({ type: "error", message: err instanceof Error ? err.message : String(err) });
30635
31711
  }).then(() => {
30636
31712
  this.lastSessionId = adapter.nativeSessionId() ?? this.lastSessionId;
31713
+ this.lastSessionCost = adapter.sessionCost?.() ?? this.lastSessionCost;
30637
31714
  if (generation2 === this.generation) this.endTurn(turn, turn.reason);
30638
31715
  });
30639
31716
  this.offerNext();
@@ -30760,6 +31837,11 @@ var StructuredSession = class extends AbstractRunnerSession {
30760
31837
  case "error":
30761
31838
  this.text.failure(event.message);
30762
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;
30763
31845
  default:
30764
31846
  break;
30765
31847
  }
@@ -30840,7 +31922,8 @@ var StructuredRunner = class extends AbstractRunner {
30840
31922
  model: opts.modelId,
30841
31923
  mode: opts.mode ?? "default",
30842
31924
  flags: resolveTaskRunnerFlags(manifest, { mode: opts.mode ?? "default", model: opts.modelId, thinkingEffort: opts.thinkingEffort }),
30843
- resumeSessionId: opts.resumeSessionId
31925
+ resumeSessionId: opts.resumeSessionId,
31926
+ resumeCost: opts.resumeCost
30844
31927
  },
30845
31928
  interruptGraceMs: this.interruptGraceMs,
30846
31929
  tools: { server: this.mcp, scope: { sessionId: opts.planSessionId ?? "", taskId: opts.taskId, attempt: opts.attempt ?? 1 } },
@@ -31142,6 +32225,7 @@ export {
31142
32225
  PlannerNotReadyError,
31143
32226
  PlannerTurnDiscardedError,
31144
32227
  PlannerTurnStoppedError,
32228
+ PlannerUsageLimitError,
31145
32229
  REFUSED_COMMANDS,
31146
32230
  RUN_SETTINGS_DEFAULTS,
31147
32231
  RunSettingsError,
@@ -31198,6 +32282,8 @@ export {
31198
32282
  approvalScopes,
31199
32283
  assertWorkspaceExists,
31200
32284
  assertWorkspaceIsProject,
32285
+ assignmentIssue,
32286
+ assignmentIssueText,
31201
32287
  augmentedPath,
31202
32288
  automergeSummary,
31203
32289
  autonomyLevelLabel,
@@ -31214,6 +32300,7 @@ export {
31214
32300
  canSetDependencies,
31215
32301
  canSplitTask,
31216
32302
  capConflictFiles,
32303
+ checkoutRule,
31217
32304
  classifyCommand,
31218
32305
  classifyOutcome,
31219
32306
  clearAugmentedPathCache,
@@ -31227,6 +32314,7 @@ export {
31227
32314
  commandAllowedBy,
31228
32315
  configuredProviders,
31229
32316
  continuability,
32317
+ countedSessionCost,
31230
32318
  createAiService,
31231
32319
  createEmptyPlan,
31232
32320
  createSession,
@@ -31290,6 +32378,7 @@ export {
31290
32378
  isMeasured,
31291
32379
  isOpenAiProvider,
31292
32380
  isPlannerBackend,
32381
+ isRepoGroup,
31293
32382
  isRunnerApproval,
31294
32383
  isSupervisorLevel,
31295
32384
  isTaskRunning,
@@ -31312,6 +32401,7 @@ export {
31312
32401
  markdownHtml,
31313
32402
  markdownTokens,
31314
32403
  mcpClientConfig,
32404
+ mergeTarget,
31315
32405
  migrateOldConfigDir,
31316
32406
  migratePlanIsolation,
31317
32407
  migratePlanState,
@@ -31361,6 +32451,7 @@ export {
31361
32451
  removedPluginNotice,
31362
32452
  renderTaskQueryAnswer,
31363
32453
  replayTaskLog,
32454
+ reposWithWork,
31364
32455
  researchShellWarning,
31365
32456
  researchToolsPath,
31366
32457
  resetInstalledPlanner,
@@ -31374,6 +32465,7 @@ export {
31374
32465
  resolveWithin,
31375
32466
  resumeLabel,
31376
32467
  resumeTime,
32468
+ resumedSessionWindow,
31377
32469
  runSettingsOf,
31378
32470
  runnerForProvider,
31379
32471
  runnerModesFrom,
@@ -31414,6 +32506,7 @@ export {
31414
32506
  taskRowView,
31415
32507
  taskStartedNotice,
31416
32508
  taskStatusKind,
32509
+ taskUsageSummary,
31417
32510
  textHasTaskOps,
31418
32511
  textHasTaskQuery,
31419
32512
  titledRefs,