@ordewell/core 0.10.3 → 0.10.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/{IFileSystem-DCd-rkeH.d.ts → IFileSystem-ClBdp59f.d.ts} +1 -1
  2. package/dist/{IFileSystem-B8Nkgg4e.d.mts → IFileSystem-Db34maxC.d.mts} +1 -1
  3. package/dist/{IRunner-CXdYyIZo.d.mts → IRunner-Crkt3tHD.d.ts} +64 -3
  4. package/dist/{IRunner-QBdYcatb.d.ts → IRunner-lMQIa5Wc.d.mts} +64 -3
  5. package/dist/{ModeResolver-geqjKkLY.d.mts → ModeResolver-B4hhABNf.d.mts} +1 -1
  6. package/dist/{ModeResolver-BOU90Nn-.d.ts → ModeResolver-CUR5jCSU.d.ts} +1 -1
  7. package/dist/{Task-CRVIqSfy.d.mts → Task-D4QHjdGv.d.mts} +93 -11
  8. package/dist/{Task-CRVIqSfy.d.ts → Task-D4QHjdGv.d.ts} +93 -11
  9. package/dist/{chunk-A57ITQE7.mjs → chunk-4QLFM73A.mjs} +34 -5
  10. package/dist/chunk-4QLFM73A.mjs.map +1 -0
  11. package/dist/{chunk-FH6M5QZG.mjs → chunk-6LEA6WAM.mjs} +88 -7
  12. package/dist/chunk-6LEA6WAM.mjs.map +1 -0
  13. package/dist/{chunk-645FALZX.mjs → chunk-F3KIX7WR.mjs} +3 -3
  14. package/dist/{chunk-OYUSADFM.mjs → chunk-NJGID6BG.mjs} +3 -1
  15. package/dist/chunk-NJGID6BG.mjs.map +1 -0
  16. package/dist/{chunk-SM6BV6XS.mjs → chunk-RT4NQ2BZ.mjs} +3 -1
  17. package/dist/chunk-RT4NQ2BZ.mjs.map +1 -0
  18. package/dist/index.d.mts +307 -66
  19. package/dist/index.d.ts +307 -66
  20. package/dist/index.js +1688 -402
  21. package/dist/index.js.map +1 -1
  22. package/dist/index.mjs +1567 -388
  23. package/dist/index.mjs.map +1 -1
  24. package/dist/parsing.d.mts +2 -2
  25. package/dist/parsing.d.ts +2 -2
  26. package/dist/parsing.js.map +1 -1
  27. package/dist/parsing.mjs +3 -3
  28. package/dist/{plan-utils-oC8bO4pN.d.ts → plan-utils-BVJCzNvm.d.ts} +204 -19
  29. package/dist/{plan-utils-KU1Rse82.d.mts → plan-utils-wI8MuDBC.d.mts} +204 -19
  30. package/dist/plan-utils.d.mts +4 -4
  31. package/dist/plan-utils.d.ts +4 -4
  32. package/dist/plan-utils.js +94 -5
  33. package/dist/plan-utils.js.map +1 -1
  34. package/dist/plan-utils.mjs +13 -3
  35. package/dist/testing.d.mts +6 -3
  36. package/dist/testing.d.ts +6 -3
  37. package/dist/testing.js +7 -0
  38. package/dist/testing.js.map +1 -1
  39. package/dist/testing.mjs +7 -2
  40. package/dist/testing.mjs.map +1 -1
  41. package/package.json +1 -1
  42. package/dist/chunk-A57ITQE7.mjs.map +0 -1
  43. package/dist/chunk-FH6M5QZG.mjs.map +0 -1
  44. package/dist/chunk-OYUSADFM.mjs.map +0 -1
  45. package/dist/chunk-SM6BV6XS.mjs.map +0 -1
  46. /package/dist/{chunk-645FALZX.mjs.map → chunk-F3KIX7WR.mjs.map} +0 -0
package/dist/index.js CHANGED
@@ -114,6 +114,7 @@ __export(index_exports, {
114
114
  PlannerNotReadyError: () => PlannerNotReadyError,
115
115
  PlannerTurnDiscardedError: () => PlannerTurnDiscardedError,
116
116
  PlannerTurnStoppedError: () => PlannerTurnStoppedError,
117
+ PlannerUsageLimitError: () => PlannerUsageLimitError,
117
118
  REFUSED_COMMANDS: () => REFUSED_COMMANDS,
118
119
  RUN_SETTINGS_DEFAULTS: () => RUN_SETTINGS_DEFAULTS,
119
120
  RunSettingsError: () => RunSettingsError,
@@ -170,6 +171,8 @@ __export(index_exports, {
170
171
  approvalScopes: () => approvalScopes,
171
172
  assertWorkspaceExists: () => assertWorkspaceExists,
172
173
  assertWorkspaceIsProject: () => assertWorkspaceIsProject,
174
+ assignmentIssue: () => assignmentIssue,
175
+ assignmentIssueText: () => assignmentIssueText,
173
176
  augmentedPath: () => augmentedPath,
174
177
  automergeSummary: () => automergeSummary,
175
178
  autonomyLevelLabel: () => autonomyLevelLabel,
@@ -186,6 +189,7 @@ __export(index_exports, {
186
189
  canSetDependencies: () => canSetDependencies,
187
190
  canSplitTask: () => canSplitTask,
188
191
  capConflictFiles: () => capConflictFiles,
192
+ checkoutRule: () => checkoutRule,
189
193
  classifyCommand: () => classifyCommand,
190
194
  classifyOutcome: () => classifyOutcome,
191
195
  clearAugmentedPathCache: () => clearAugmentedPathCache,
@@ -199,6 +203,7 @@ __export(index_exports, {
199
203
  commandAllowedBy: () => commandAllowedBy,
200
204
  configuredProviders: () => configuredProviders,
201
205
  continuability: () => continuability,
206
+ countedSessionCost: () => countedSessionCost,
202
207
  createAiService: () => createAiService,
203
208
  createEmptyPlan: () => createEmptyPlan,
204
209
  createSession: () => createSession,
@@ -262,6 +267,7 @@ __export(index_exports, {
262
267
  isMeasured: () => isMeasured,
263
268
  isOpenAiProvider: () => isOpenAiProvider,
264
269
  isPlannerBackend: () => isPlannerBackend,
270
+ isRepoGroup: () => isRepoGroup,
265
271
  isRunnerApproval: () => isRunnerApproval,
266
272
  isSupervisorLevel: () => isSupervisorLevel,
267
273
  isTaskRunning: () => isTaskRunning,
@@ -284,6 +290,7 @@ __export(index_exports, {
284
290
  markdownHtml: () => markdownHtml,
285
291
  markdownTokens: () => markdownTokens,
286
292
  mcpClientConfig: () => mcpClientConfig,
293
+ mergeTarget: () => mergeTarget,
287
294
  migrateOldConfigDir: () => migrateOldConfigDir,
288
295
  migratePlanIsolation: () => migratePlanIsolation,
289
296
  migratePlanState: () => migratePlanState,
@@ -333,6 +340,7 @@ __export(index_exports, {
333
340
  removedPluginNotice: () => removedPluginNotice,
334
341
  renderTaskQueryAnswer: () => renderTaskQueryAnswer,
335
342
  replayTaskLog: () => replayTaskLog,
343
+ reposWithWork: () => reposWithWork,
336
344
  researchShellWarning: () => researchShellWarning,
337
345
  researchToolsPath: () => researchToolsPath,
338
346
  resetInstalledPlanner: () => resetInstalledPlanner,
@@ -346,6 +354,7 @@ __export(index_exports, {
346
354
  resolveWithin: () => resolveWithin,
347
355
  resumeLabel: () => resumeLabel,
348
356
  resumeTime: () => resumeTime,
357
+ resumedSessionWindow: () => resumedSessionWindow,
349
358
  runSettingsOf: () => runSettingsOf,
350
359
  runnerForProvider: () => runnerForProvider,
351
360
  runnerModesFrom: () => runnerModesFrom,
@@ -386,6 +395,7 @@ __export(index_exports, {
386
395
  taskRowView: () => taskRowView,
387
396
  taskStartedNotice: () => taskStartedNotice,
388
397
  taskStatusKind: () => taskStatusKind,
398
+ taskUsageSummary: () => taskUsageSummary,
389
399
  textHasTaskOps: () => textHasTaskOps,
390
400
  textHasTaskQuery: () => textHasTaskQuery,
391
401
  titledRefs: () => titledRefs,
@@ -488,6 +498,7 @@ function migrateLoadedTasks(tasks) {
488
498
  for (const task of tasks) {
489
499
  if (typeof task.runnerSessionId !== "string") delete task.runnerSessionId;
490
500
  if (typeof task.carriedFrom !== "string") delete task.carriedFrom;
501
+ if (typeof task.runnerSessionOwner !== "string") delete task.runnerSessionOwner;
491
502
  const legacy = task;
492
503
  if (!task.runnerSessionId && legacy.transport?.kind === "structured" && typeof legacy.transport.nativeSessionId === "string") {
493
504
  task.runnerSessionId = legacy.transport.nativeSessionId;
@@ -747,6 +758,7 @@ function keepExecutionState(current, rewrite) {
747
758
  verdict: prior?.verdict,
748
759
  outputSummary: prior?.outputSummary,
749
760
  runnerSessionId: prior?.runnerSessionId,
761
+ runnerSessionOwner: prior?.runnerSessionOwner,
750
762
  carriedFrom: prior?.carriedFrom,
751
763
  attemptSkills: prior?.attemptSkills,
752
764
  attemptUsage: prior?.attemptUsage,
@@ -835,8 +847,21 @@ function addAttemptUsage(usage, record, now) {
835
847
  const folded = addPlannerUsage(usage, record);
836
848
  return { ...usage, ...folded, lastPromptTokens: folded.lastPromptTokens, contextWindow: folded.contextWindow, wallTimeMs: Math.max(0, Date.parse(now) - Date.parse(usage.startedAt)) };
837
849
  }
838
- function endAttemptUsage(usage, now) {
839
- return { ...usage, endedAt: now, wallTimeMs: Math.max(0, Date.parse(now) - Date.parse(usage.startedAt)) };
850
+ function endAttemptUsage(usage, now, runnerSessionCost, sessionId) {
851
+ return {
852
+ ...usage,
853
+ endedAt: now,
854
+ wallTimeMs: Math.max(0, Date.parse(now) - Date.parse(usage.startedAt)),
855
+ ...sessionId ? { sessionId } : {},
856
+ ...runnerSessionCost ? { runnerSessionCost } : {}
857
+ };
858
+ }
859
+ function countedSessionCost(attempts, sessionId) {
860
+ return [...attempts ?? []].reverse().find((usage) => usage.runnerSessionCost?.sessionId === sessionId)?.runnerSessionCost;
861
+ }
862
+ function resumedSessionWindow(attempts, runner, attempt, sessionId) {
863
+ const last = [...attempts ?? []].reverse().find((usage) => usage.attempt !== attempt && usage.runner === runner && usage.sessionId === sessionId && usage.contextWindow !== void 0);
864
+ return last ? { source: last.source, model: last.model, contextWindow: last.contextWindow } : void 0;
840
865
  }
841
866
  function addUsageTotals(totals, addition) {
842
867
  let next = addUsage(totals, { source: "", inputTokens: addition.inputTokens, outputTokens: addition.outputTokens, cachedInputTokens: addition.cachedInputTokens });
@@ -865,6 +890,16 @@ function planUsage(tasks) {
865
890
  }
866
891
  return result;
867
892
  }
893
+ function taskUsageSummary(task) {
894
+ const attempts = task.attemptUsage ?? [];
895
+ const latest = attempts.at(-1);
896
+ const contextFill = latest ? plannerContextFill(latest) : void 0;
897
+ return {
898
+ totals: attempts.reduce((sum, attempt) => addUsageTotals(sum, attempt.totals), {}),
899
+ attempts: attempts.length,
900
+ ...contextFill ? { contextFill } : {}
901
+ };
902
+ }
868
903
 
869
904
  // src/models/TaskLog.ts
870
905
  var HEAD_LINES = 60;
@@ -966,6 +1001,9 @@ function toTaskLogEvent(event) {
966
1001
  };
967
1002
  case "command_output":
968
1003
  return { type: "command_output", text: event.text };
1004
+ // Logged once the plan's write-back settles: see `TaskLogRecorder.note`.
1005
+ case "model_switched":
1006
+ case "model_drifted":
969
1007
  case "background_agent":
970
1008
  return null;
971
1009
  }
@@ -5417,8 +5455,8 @@ function forms(text3) {
5417
5455
  function sensitiveEnvValues(env) {
5418
5456
  return Object.entries(env).filter(([name, value]) => SENSITIVE_ENV_NAME.test(name) && !/_(?:URL|URI|PATH|FILE|DIR)$/i.test(name) && value !== void 0).map(([, value]) => (value ?? "").trim());
5419
5457
  }
5420
- function carriesSecret(texts, held) {
5421
- const secrets = [...held].map((s) => s.trim()).filter((s) => s.length >= MIN_SECRET_LENGTH);
5458
+ function carriesSecret(texts, held2) {
5459
+ const secrets = [...held2].map((s) => s.trim()).filter((s) => s.length >= MIN_SECRET_LENGTH);
5422
5460
  return texts.flatMap(forms).some((text3) => secrets.some((secret) => text3.includes(secret)) || TOKEN_SHAPES.some((shape) => shape.test(text3)) || [...text3.matchAll(SK_KEY)].some((m) => looksRandom(m[0].slice(3))));
5423
5461
  }
5424
5462
 
@@ -7239,6 +7277,12 @@ function commandOutput(view, { text: text3 }) {
7239
7277
  return { ...view, blocks: replaceAt(view.blocks, i, { ...block, output: block.output ? `${block.output}
7240
7278
  ${text3}` : text3 }) };
7241
7279
  }
7280
+ function modelSwitchNotice({ from, to, plan, kept }) {
7281
+ if (!plan) return `Model changed in the runner: ${from.label ?? from.model} \u2192 ${to.label ?? to.model}${kept ? ` (${kept})` : ""}`;
7282
+ const planned = plan.model ? `${plan.model.label ?? plan.model.model}${plan.effort ? `, ${plan.effort}` : ""}` : void 0;
7283
+ if (!kept) return `Model changed in the runner: ${from.label ?? from.model} \u2192 ${to.label ?? to.model}; the plan now says ${planned ?? to.label ?? to.model}`;
7284
+ return `The runner now answers on ${to.label ?? to.model}; ${planned ? `the plan still says ${planned}` : "the plan names no model"} (${kept})`;
7285
+ }
7242
7286
  function appendStep(view, make) {
7243
7287
  const next = append(view, make);
7244
7288
  return sealLane(next, null, findLastIndex(next.blocks, (b) => b.type !== "usage"));
@@ -7300,6 +7344,8 @@ function reduceTaskLog(view, event) {
7300
7344
  return startCommand(view, event);
7301
7345
  case "command_output":
7302
7346
  return commandOutput(view, event);
7347
+ case "model_switched":
7348
+ return append(view, (id) => settledMessage(id, "system", modelSwitchNotice(event)));
7303
7349
  case "compaction":
7304
7350
  return appendStep(view, (id) => ({
7305
7351
  type: "compaction",
@@ -7780,7 +7826,7 @@ var CODEX_MANIFEST = {
7780
7826
  }
7781
7827
  },
7782
7828
  modelDiscovery: {
7783
- // Codex has no `codex models` subcommand (see docs/adr/0004). Discovery
7829
+ // Codex has no `codex models` subcommand (see ADR-0004). Discovery
7784
7830
  // tries three sources in order, each falling through to the next:
7785
7831
  //
7786
7832
  // 1. `codex app-server` stdio JSON-RPC (`initialize` → `model/list`) —
@@ -7851,6 +7897,10 @@ function parseOpenCodeModels(stdout, manifest) {
7851
7897
  }
7852
7898
  return models;
7853
7899
  }
7900
+ function contextLimit(limit) {
7901
+ const context = limit?.context;
7902
+ return typeof context === "number" && Number.isFinite(context) && context > 0 ? { contextWindow: context } : {};
7903
+ }
7854
7904
  function parseOpenCodeModelsVerbose(stdout, manifest) {
7855
7905
  const normalized2 = stdout.endsWith("\n") ? stdout : stdout + "\n";
7856
7906
  const parts = normalized2.split(/^([A-Za-z0-9._-]+\/[^\s"{}]+)\n/m);
@@ -7866,12 +7916,14 @@ function parseOpenCodeModelsVerbose(stdout, manifest) {
7866
7916
  const jsonText = parts[i + 1].trim();
7867
7917
  if (seen.has(fullId)) continue;
7868
7918
  let variantIds = [];
7919
+ let window = {};
7869
7920
  let modelLabel = prefLabels.get(fullId) || fullId.split("/").pop() || fullId;
7870
7921
  const runnerProvider = fullId.split("/")[0];
7871
7922
  try {
7872
7923
  const data = JSON.parse(jsonText);
7873
7924
  if (data.name) modelLabel = prefLabels.get(fullId) || data.name;
7874
7925
  variantIds = Object.keys(data.variants ?? {});
7926
+ window = contextLimit(data.limit);
7875
7927
  } catch {
7876
7928
  }
7877
7929
  seen.add(fullId);
@@ -7879,12 +7931,15 @@ function parseOpenCodeModelsVerbose(stdout, manifest) {
7879
7931
  modelId: fullId,
7880
7932
  modelLabel,
7881
7933
  runnerProvider,
7882
- variants: variantIds.map((id) => ({ id, label: id.charAt(0).toUpperCase() + id.slice(1) }))
7934
+ variants: variantIds.map((id) => ({ id, label: id.charAt(0).toUpperCase() + id.slice(1) })),
7935
+ ...window
7883
7936
  });
7884
7937
  }
7885
- if (models.length === 0) return [];
7886
- const prefOrder = preferred?.map((p) => p.id) ?? [];
7887
- models.sort((a, b) => {
7938
+ return preferredFirst(models, manifest);
7939
+ }
7940
+ function preferredFirst(models, manifest) {
7941
+ const prefOrder = manifest.modelDiscovery.preferredPatterns?.map((p) => p.id) ?? [];
7942
+ return [...models].sort((a, b) => {
7888
7943
  const ai = prefOrder.indexOf(a.modelId);
7889
7944
  const bi = prefOrder.indexOf(b.modelId);
7890
7945
  if (ai >= 0 && bi >= 0) return ai - bi;
@@ -7892,7 +7947,37 @@ function parseOpenCodeModelsVerbose(stdout, manifest) {
7892
7947
  if (bi >= 0) return 1;
7893
7948
  return a.modelId.localeCompare(b.modelId);
7894
7949
  });
7895
- return models;
7950
+ }
7951
+ function parseOpenCodeModelsApi(stdout, manifest) {
7952
+ let data;
7953
+ try {
7954
+ data = JSON.parse(stdout)?.data;
7955
+ } catch {
7956
+ return [];
7957
+ }
7958
+ if (!Array.isArray(data)) return [];
7959
+ const prefLabels = new Map((manifest.modelDiscovery.preferredPatterns ?? []).map((p) => [p.id, p.label]));
7960
+ const seen = /* @__PURE__ */ new Set();
7961
+ const models = [];
7962
+ for (const entry of data) {
7963
+ if (!entry || typeof entry.providerID !== "string" || typeof entry.modelID !== "string") continue;
7964
+ if (entry.enabled === false) continue;
7965
+ const fullId = `${entry.providerID}/${entry.modelID}`;
7966
+ if (seen.has(fullId)) continue;
7967
+ seen.add(fullId);
7968
+ const variantIds = Array.isArray(entry.variants) ? entry.variants.flatMap((v) => {
7969
+ const id = v?.id;
7970
+ return typeof id === "string" && id ? [id] : [];
7971
+ }) : [];
7972
+ models.push({
7973
+ modelId: fullId,
7974
+ modelLabel: prefLabels.get(fullId) || typeof entry.name === "string" && entry.name || entry.modelID,
7975
+ runnerProvider: entry.providerID,
7976
+ variants: variantIds.map((id) => ({ id, label: id.charAt(0).toUpperCase() + id.slice(1) })),
7977
+ ...contextLimit(entry.limit)
7978
+ });
7979
+ }
7980
+ return preferredFirst(models, manifest);
7896
7981
  }
7897
7982
 
7898
7983
  // src/plugins/builtin/opencode.manifest.ts
@@ -7908,13 +7993,17 @@ var OPENCODE_MANIFEST = {
7908
7993
  modelDiscovery: {
7909
7994
  method: "command",
7910
7995
  discoveryCommands: [
7996
+ // v1 prints per-model variants under --verbose; v2 rejects the flag (a
7997
+ // fast non-zero exit) and serves the same catalog, variants included, over
7998
+ // `api model.list`. v1 is asked first so it never reaches the `api` form.
7911
7999
  { command: "opencode", args: ["models", "--verbose"], parser: parseOpenCodeModelsVerbose },
8000
+ { command: "opencode", args: ["api", "model.list"], parser: parseOpenCodeModelsApi },
7912
8001
  { command: "opencode", args: ["models"], parser: parseOpenCodeModels }
7913
8002
  ],
7914
8003
  // No fallbackModels: the list must always reflect what `opencode models`
7915
8004
  // actually reports for THIS user (their configured providers only). A
7916
8005
  // hardcoded fallback previously leaked models the user didn't have.
7917
- // No static variants — per-model variants are fetched from opencode models --verbose
8006
+ // No static variants — per-model variants come from the first two commands above
7918
8007
  providerQualifiedIds: true
7919
8008
  },
7920
8009
  contextFile: "AGENTS.md",
@@ -9769,6 +9858,7 @@ var LOG_KINDS = {
9769
9858
  turn_start: true,
9770
9859
  command: true,
9771
9860
  command_output: true,
9861
+ model_switched: true,
9772
9862
  compaction: true,
9773
9863
  runner_loaded: true,
9774
9864
  turn_end: true,
@@ -9804,6 +9894,11 @@ ${s.content}` : ""}`).join("\n");
9804
9894
  return `${event.messageId ? `${event.messageId} ` : ""}/${event.name}${event.args ? ` ${event.args}` : ""}`;
9805
9895
  case "command_output":
9806
9896
  return event.text;
9897
+ case "model_switched": {
9898
+ const { plan } = event;
9899
+ const planned = plan ? `; plan: ${plan.model?.model ?? "no model"}${plan.effort ? ` (${plan.effort})` : ""}` : "";
9900
+ return `${event.from.model} \u2192 ${event.to.model}${planned}${event.kept ? `, kept: ${event.kept}` : ""}`;
9901
+ }
9807
9902
  case "compaction":
9808
9903
  return `${event.trigger}${event.preTokens !== void 0 && event.postTokens !== void 0 ? `, ${event.preTokens} \u2192 ${event.postTokens} tokens` : ""}`;
9809
9904
  case "runner_loaded":
@@ -11363,6 +11458,7 @@ function canUseToolAnswer({ input, suggestions }, decision) {
11363
11458
  ...decision.decision === "allowForTask" && suggestions.length > 0 ? { updatedPermissions: suggestions } : {}
11364
11459
  };
11365
11460
  }
11461
+ var MODEL_COMMAND = "model";
11366
11462
  var SUBAGENT_TOOLS = /* @__PURE__ */ new Set(["Agent", "Task"]);
11367
11463
  function usageRecord(usage, model, subagentId) {
11368
11464
  const record = {
@@ -11424,9 +11520,9 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
11424
11520
  /** The model answering the planner's current message, as its `message_start` named it. */
11425
11521
  plannerModel;
11426
11522
  /**
11427
- * The session's `total_cost_usd` as last reported. Undefined after a resume:
11428
- * the CLI restores the resumed session's running total, and what Ordewell
11429
- * already counted of it is not ours to know here.
11523
+ * The session's `total_cost_usd` as last counted. A resume starts from what
11524
+ * was counted of that session before, and is undefined without it: the CLI
11525
+ * restores the session's running total, which holds turns counted earlier.
11430
11526
  */
11431
11527
  reportedCostUsd = 0;
11432
11528
  reportedModelCostUsd = /* @__PURE__ */ new Map();
@@ -11467,6 +11563,19 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
11467
11563
  terminalOnly = /* @__PURE__ */ new Set();
11468
11564
  /** Whether this process's `runner_loaded` went out: its first `init` sends it. */
11469
11565
  loadedReported = false;
11566
+ /** The models `initialize` listed, which turn the ids the CLI reports back into the ones a plan holds. */
11567
+ listedModels = [];
11568
+ /** The id the task's main thread last reported answering on. */
11569
+ mainModel;
11570
+ /** What the plan should say the task runs: its spawn's model, then each switch followed. */
11571
+ followedModel;
11572
+ /**
11573
+ * Model commands sent through the chat whose switch has not been seen yet,
11574
+ * oldest first, each with the turns still to end before its own has. Several
11575
+ * can be pending: a command's switch is first reported by the next turn's
11576
+ * `init`, which can be another `/model`'s.
11577
+ */
11578
+ modelCommands = [];
11470
11579
  spawnSpec(opts, workspace) {
11471
11580
  if (opts.kind === "task") return this.taskSpawnSpec(opts);
11472
11581
  const args = [
@@ -11492,12 +11601,23 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
11492
11601
  if (opts.effort && opts.effort !== "adaptive") args.push("--effort", opts.effort);
11493
11602
  if (opts.resumeSessionId) {
11494
11603
  args.push("--resume", opts.resumeSessionId);
11495
- this.reportedCostUsd = void 0;
11604
+ this.countFrom(opts.resumeSessionId, opts.resumeCost);
11496
11605
  this.pendingResume = opts.resumeSessionId;
11497
11606
  }
11498
11607
  this.configDir = workspace.CLAUDE_CONFIG_DIR ?? process.env.CLAUDE_CONFIG_DIR ?? path13.join(os6.homedir(), ".claude");
11499
11608
  return { command: "claude", args, env: NO_AUTO_MEMORY };
11500
11609
  }
11610
+ sessionCost() {
11611
+ const sessionId = this.sessionId ?? this.pendingResume;
11612
+ if (!sessionId || this.reportedCostUsd === void 0) return null;
11613
+ return { sessionId, usd: this.reportedCostUsd, modelUsd: Object.fromEntries(this.reportedModelCostUsd) };
11614
+ }
11615
+ /** A count kept for another session says nothing of this one's. */
11616
+ countFrom(sessionId, counted) {
11617
+ const known = counted?.sessionId === sessionId ? counted : void 0;
11618
+ this.reportedCostUsd = known?.usd;
11619
+ this.reportedModelCostUsd = new Map(Object.entries(known?.modelUsd ?? {}));
11620
+ }
11501
11621
  savedOutput(abs) {
11502
11622
  return this.configDir !== null && this.sessionId !== null && inSavedOutput(abs, this.configDir, this.sessionId);
11503
11623
  }
@@ -11512,6 +11632,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
11512
11632
  */
11513
11633
  taskSpawnSpec(opts) {
11514
11634
  this.requestedMode = opts.flags.permissionMode;
11635
+ this.followedModel = opts.model;
11515
11636
  const args = [
11516
11637
  ...TASK_PROTOCOL_ARGS,
11517
11638
  "--permission-prompt-tool",
@@ -11525,7 +11646,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
11525
11646
  if (opts.model) args.push("--model", opts.model);
11526
11647
  if (opts.resumeSessionId) {
11527
11648
  args.push("--resume", opts.resumeSessionId);
11528
- this.reportedCostUsd = void 0;
11649
+ this.countFrom(opts.resumeSessionId, opts.resumeCost);
11529
11650
  this.pendingResume = opts.resumeSessionId;
11530
11651
  }
11531
11652
  if (opts.mcp) args.push("--append-system-prompt", TASK_COMPLETION_CONTRACT, ...this.ordewellServerArgs(opts.mcp, "task"));
@@ -11567,6 +11688,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
11567
11688
  if (opts.kind !== "task") return;
11568
11689
  const initialized = (await this.control({ subtype: "initialize" }, MODEL_PIN_TIMEOUT_MS))?.response;
11569
11690
  this.listedCommands = initialized?.commands ?? [];
11691
+ this.listedModels = initialized?.models ?? [];
11570
11692
  if (opts.flags.permissionMode !== "plan" || !opts.model) return;
11571
11693
  const resolved = initialized?.models?.find((m) => m.value === opts.model)?.resolvedModel;
11572
11694
  if (resolved && resolved !== opts.model) await this.control({ subtype: "set_model", model: resolved }, MODEL_PIN_TIMEOUT_MS);
@@ -11620,6 +11742,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
11620
11742
  if (this.role !== "task" || !this.process || this.interruptRequested || this.pendingResume) return false;
11621
11743
  const uuid = (0, import_crypto4.randomUUID)();
11622
11744
  this.steers.set(uuid, id);
11745
+ this.noteModelCommand(text3, 2);
11623
11746
  this.writeLine({ type: "user", uuid, message: { role: "user", content: [{ type: "text", text: text3 }] } });
11624
11747
  return true;
11625
11748
  }
@@ -11671,12 +11794,23 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
11671
11794
  }
11672
11795
  turnPayload(message) {
11673
11796
  this.openTurnState();
11797
+ this.noteModelCommand(message, 1);
11674
11798
  return `${JSON.stringify({
11675
11799
  type: "user",
11676
11800
  message: { role: "user", content: [{ type: "text", text: message }] }
11677
11801
  })}
11678
11802
  `;
11679
11803
  }
11804
+ /**
11805
+ * The CLI runs a message that starts with a slash command as that command,
11806
+ * whether the surface sent it as one or as plain text, so the message
11807
+ * itself says a model switch was asked for: every way into the chat counts.
11808
+ */
11809
+ noteModelCommand(text3, turnsLeft) {
11810
+ const [, name, args = ""] = /^\/(\S+)\s*([\s\S]*)$/.exec(text3) ?? [];
11811
+ if (this.role !== "task" || !name || (findRunnerCommand(this.commands(), name)?.name ?? name) !== MODEL_COMMAND) return;
11812
+ this.modelCommands.push({ args: args.trim(), turnsLeft });
11813
+ }
11680
11814
  openTurnState() {
11681
11815
  this.turnHasText = false;
11682
11816
  this.interruptRequested = false;
@@ -11788,6 +11922,7 @@ ${block.text}` : block.text });
11788
11922
  this.clearFollowOnTimer();
11789
11923
  if (this.refuseOtherMode(msg, emit)) return;
11790
11924
  this.readLoaded(msg, emit);
11925
+ this.followModel(msg.model, emit);
11791
11926
  }
11792
11927
  if (msg.subtype === "commands_changed" && Array.isArray(msg.commands)) this.listedCommands = msg.commands;
11793
11928
  if (msg.subtype === "compact_boundary") {
@@ -11810,6 +11945,7 @@ ${block.text}` : block.text });
11810
11945
  return;
11811
11946
  case "result":
11812
11947
  this.reportSessionCost(msg, emit);
11948
+ for (const command of this.modelCommands) command.turnsLeft = Math.max(0, command.turnsLeft - 1);
11813
11949
  if (this.interruptRequested && (msg.is_error || msg.subtype !== "success")) {
11814
11950
  this.interruptRequested = false;
11815
11951
  this.closeTurn({ type: "turn_end", interrupted: true }, emit);
@@ -11829,6 +11965,55 @@ ${block.text}` : block.text });
11829
11965
  return;
11830
11966
  }
11831
11967
  }
11968
+ /**
11969
+ * A task's main thread reported the model it answers on. A change is the
11970
+ * user's own switch when a model command went out before it: Ordewell never
11971
+ * passes `--fallback-model`, so nothing else should move it. A change with
11972
+ * no command before it is reported as drift, and the plan does not follow
11973
+ * it. A command whose turn ended with the model unchanged switched nothing,
11974
+ * and is let go.
11975
+ *
11976
+ * Effort is never reported (recorded on 2.1.302, `task-effort-command`):
11977
+ * `init` names the model alone, its `per_turn_effort_active` says effort is
11978
+ * on, not which; `/effort low` only prints that it set it; and `/model`
11979
+ * takes no effort (`/model haiku high` is "not found"). So a switch carries
11980
+ * no effort and the plan's stays as planned.
11981
+ */
11982
+ followModel(reported, emit) {
11983
+ if (this.role !== "task" || !reported || reported === "<synthetic>") return;
11984
+ const previous = this.mainModel;
11985
+ this.mainModel = reported;
11986
+ if (previous === void 0 || previous === reported) {
11987
+ this.modelCommands = this.modelCommands.filter((c) => c.turnsLeft > 0);
11988
+ return;
11989
+ }
11990
+ const matched = this.modelCommands.findIndex((c) => this.runsAs(c.args, reported));
11991
+ const command = this.modelCommands[Math.max(matched, 0)];
11992
+ this.modelCommands = this.modelCommands.slice(Math.max(matched, 0) + 1);
11993
+ const from = this.listedModel(previous, this.followedModel);
11994
+ if (!command) {
11995
+ emit({ type: "model_drifted", from, to: this.listedModel(reported, void 0) });
11996
+ return;
11997
+ }
11998
+ const to = this.listedModel(reported, command.args);
11999
+ this.followedModel = to.model;
12000
+ emit({ type: "model_switched", from, to });
12001
+ }
12002
+ /** Whether the model value `asked` runs as the id `resolved`, by what `initialize` lists. */
12003
+ runsAs(asked, resolved) {
12004
+ return asked === resolved || this.listedModels.some((m) => m.value === asked && m.resolvedModel === resolved);
12005
+ }
12006
+ /**
12007
+ * The value `initialize` lists for the id a model runs as. Several can share
12008
+ * one (`sonnet`, `default`); the one asked for wins, and `default` only as a
12009
+ * last resort, since it names whatever the CLI's default becomes.
12010
+ */
12011
+ listedModel(resolved, asked) {
12012
+ const listed3 = this.listedModels.filter((m) => m.value && (m.resolvedModel === resolved || m.value === resolved));
12013
+ const pick = listed3.find((m) => m.value === asked && asked !== "default") ?? listed3.find((m) => m.value !== "default") ?? listed3[0];
12014
+ if (!pick?.value) return { model: resolved };
12015
+ return { model: pick.value, ...pick.displayName ? { label: pick.displayName } : {} };
12016
+ }
11832
12017
  /**
11833
12018
  * `--permission-mode auto` on a model or account without auto mode is not
11834
12019
  * refused: the CLI says nothing and starts in `default`, then asks about every
@@ -11912,8 +12097,10 @@ ${block.text}` : block.text });
11912
12097
  * {@link AgentEvent}), so nothing here has to reconcile with it.
11913
12098
  */
11914
12099
  handleStreamEvent(event, emit) {
11915
- if (event.type === "message_start") this.plannerModel = event.message?.model;
11916
- else if (event.type === "message_delta" && event.usage) emit({ type: "usage", record: usageRecord(event.usage, this.plannerModel) });
12100
+ if (event.type === "message_start") {
12101
+ this.plannerModel = event.message?.model;
12102
+ this.followModel(this.plannerModel, emit);
12103
+ } else if (event.type === "message_delta" && event.usage) emit({ type: "usage", record: usageRecord(event.usage, this.plannerModel) });
11917
12104
  else if (event.type === "content_block_start" && event.content_block?.type === "text") {
11918
12105
  this.pendingBreak = this.turnHasText;
11919
12106
  } else if (event.type === "content_block_delta" && event.delta?.type === "text_delta" && event.delta.text) {
@@ -11928,7 +12115,8 @@ ${block.text}` : block.text });
11928
12115
  * What only the result line knows: the cost, which covers every call the
11929
12116
  * session made — subagents included, since none of their lines carries one —
11930
12117
  * and the planner model's window. `total_cost_usd` is a running total, so a
11931
- * turn reports its growth. The first turn after a resume only sets the
12118
+ * turn reports its growth. After a resume it grows from what was counted of
12119
+ * the session before; with no count kept, the first turn only sets the
11932
12120
  * baseline: its total includes turns counted before, and a turn's own share
11933
12121
  * cannot be told apart from them.
11934
12122
  */
@@ -12508,6 +12696,97 @@ var CodexRpc = class {
12508
12696
  }
12509
12697
  };
12510
12698
 
12699
+ // src/utils/wallTime.ts
12700
+ var MONTHS = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
12701
+ function lastMatch(re, text3) {
12702
+ let last = null;
12703
+ for (const m of text3.matchAll(re)) last = m;
12704
+ return last;
12705
+ }
12706
+ function hour24(hour, meridiem) {
12707
+ const h = Number(hour) % 12;
12708
+ return meridiem.toLowerCase() === "pm" ? h + 12 : h;
12709
+ }
12710
+ function month(name) {
12711
+ if (!name) return void 0;
12712
+ const at = MONTHS.indexOf(name.toLowerCase());
12713
+ return at < 0 ? void 0 : at;
12714
+ }
12715
+ function placeWallTime(wall, zone, now) {
12716
+ const today = wallFields(now, zone);
12717
+ const year = wall.year ?? today.year;
12718
+ const monthIndex = wall.month ?? today.month;
12719
+ const day = wall.day ?? today.day;
12720
+ const at = fromWall(year, monthIndex, day, wall.hour, wall.minute, zone);
12721
+ if (wall.day !== void 0 || at.getTime() >= now.getTime() - HOUR_MS) return at;
12722
+ return fromWall(year, monthIndex, day + 1, wall.hour, wall.minute, zone);
12723
+ }
12724
+ var HOUR_MS = 60 * 6e4;
12725
+ function wallFields(instant, zone) {
12726
+ if (!zone) {
12727
+ return { year: instant.getFullYear(), month: instant.getMonth(), day: instant.getDate(), hour: instant.getHours(), minute: instant.getMinutes(), second: instant.getSeconds() };
12728
+ }
12729
+ const parts = new Intl.DateTimeFormat("en-US", {
12730
+ timeZone: zone,
12731
+ hourCycle: "h23",
12732
+ year: "numeric",
12733
+ month: "numeric",
12734
+ day: "numeric",
12735
+ hour: "numeric",
12736
+ minute: "numeric",
12737
+ second: "numeric"
12738
+ }).formatToParts(instant);
12739
+ const field = (type) => Number(parts.find((p) => p.type === type)?.value ?? 0);
12740
+ return { year: field("year"), month: field("month") - 1, day: field("day"), hour: field("hour"), minute: field("minute"), second: field("second") };
12741
+ }
12742
+ function zoneOffsetMs(instant, zone) {
12743
+ const f = wallFields(new Date(instant), zone);
12744
+ return Date.UTC(f.year, f.month, f.day, f.hour, f.minute, f.second) - Math.floor(instant / 1e3) * 1e3;
12745
+ }
12746
+ function fromWall(year, monthIndex, day, hour, minute, zone) {
12747
+ if (!zone) return new Date(year, monthIndex, day, hour, minute);
12748
+ const asUtc = Date.UTC(year, monthIndex, day, hour, minute);
12749
+ let instant = asUtc - zoneOffsetMs(asUtc, zone);
12750
+ instant = asUtc - zoneOffsetMs(instant, zone);
12751
+ return new Date(instant);
12752
+ }
12753
+
12754
+ // src/services/harness/codexUsageLimit.ts
12755
+ var CODEX_RESET_RE = /[Tt]ry again at (?:([A-Z][a-z]{2}) (\d{1,2})(?:st|nd|rd|th)?, (\d{4}) )?(\d{1,2}):(\d{2}) ([AP]M)\./g;
12756
+ function codexUsageLimitReset(text3, now) {
12757
+ const m = lastMatch(CODEX_RESET_RE, text3);
12758
+ if (!m) return null;
12759
+ const [, mon, day, year, hour, minute, meridiem] = m;
12760
+ const wall = { hour: hour24(hour, meridiem), minute: Number(minute) };
12761
+ if (mon) {
12762
+ const at = month(mon);
12763
+ if (at === void 0) return null;
12764
+ Object.assign(wall, { month: at, day: Number(day), year: Number(year) });
12765
+ }
12766
+ return placeWallTime(wall, void 0, now);
12767
+ }
12768
+ function mergeCodexRateLimits(known, params) {
12769
+ const snapshot = params?.rateLimits;
12770
+ if (!snapshot || typeof snapshot !== "object") return known;
12771
+ const id = snapshot.limitId ?? "";
12772
+ const before = known.get(id);
12773
+ const next = new Map(known);
12774
+ next.set(id, { limitId: id, primary: snapshot.primary ?? before?.primary, secondary: snapshot.secondary ?? before?.secondary });
12775
+ return next;
12776
+ }
12777
+ function isCodexUsageLimit(error) {
12778
+ const info = error?.codexErrorInfo;
12779
+ if (info === void 0 || info === null || info === "other") return void 0;
12780
+ if (info === "usageLimitExceeded") return true;
12781
+ return typeof info === "object" && "usageLimitExceeded" in info;
12782
+ }
12783
+ function codexUsageLimitSignal(error, limits, now) {
12784
+ const limited = isCodexUsageLimit(error);
12785
+ if (limited !== true) return limited;
12786
+ const resets = [...limits.values()].flatMap((snapshot) => [snapshot.primary, snapshot.secondary]).flatMap((window) => window && (window.usedPercent ?? 0) >= 100 && typeof window.resetsAt === "number" ? [window.resetsAt * 1e3] : []).filter((at) => at > now.getTime());
12787
+ return resets.length > 0 ? { resetsAt: new Date(Math.max(...resets)).toISOString() } : {};
12788
+ }
12789
+
12511
12790
  // src/services/harness/CodexAdapter.ts
12512
12791
  var HANDSHAKE_TIMEOUT_MS = 3e4;
12513
12792
  var MCP_ATTACH_TIMEOUT_MS2 = 1e4;
@@ -12651,6 +12930,11 @@ var CodexAdapter = class extends StdioAgentAdapter {
12651
12930
  turn = null;
12652
12931
  /** The last turn this adapter ended — so its second end signal, landing late, ends nothing. */
12653
12932
  endedTurnId = null;
12933
+ /**
12934
+ * The account's limit windows as Codex last reported them. Only read when a
12935
+ * turn fails on the usage limit; an update on its own acts on nothing.
12936
+ */
12937
+ rateLimits = /* @__PURE__ */ new Map();
12654
12938
  /** An interrupt was asked for during this turn, so however it ends, it was cut short. */
12655
12939
  interruptRequested = false;
12656
12940
  /**
@@ -13046,6 +13330,9 @@ ${this.exitMessage()}`);
13046
13330
  case "thread/tokenUsage/updated":
13047
13331
  this.emitUsage(msg.params, emit);
13048
13332
  return;
13333
+ case "account/rateLimits/updated":
13334
+ this.rateLimits = mergeCodexRateLimits(this.rateLimits, msg.params);
13335
+ return;
13049
13336
  case "turn/started": {
13050
13337
  if (this.subagentOf(msg.params?.threadId)) return;
13051
13338
  this.turnHasText = false;
@@ -13086,7 +13373,7 @@ ${this.exitMessage()}`);
13086
13373
  const failure = msg.params;
13087
13374
  if (failure?.willRetry) return;
13088
13375
  this.closeTurn(emit);
13089
- emit({ type: "error", message: failure?.error?.message || "Codex ended the turn with an error." });
13376
+ emit(this.turnFailed(failure?.error));
13090
13377
  return;
13091
13378
  }
13092
13379
  case "turn/completed": {
@@ -13097,7 +13384,7 @@ ${this.exitMessage()}`);
13097
13384
  if (this.interruptRequested || turn?.status === "interrupted") {
13098
13385
  emit({ type: "turn_end", interrupted: true });
13099
13386
  } else if (turn?.status === "failed") {
13100
- emit({ type: "error", message: turn.error?.message || "Codex ended the turn with an error." });
13387
+ emit(this.turnFailed(turn.error));
13101
13388
  } else {
13102
13389
  emit({ type: "turn_end" });
13103
13390
  }
@@ -13109,6 +13396,11 @@ ${this.exitMessage()}`);
13109
13396
  return;
13110
13397
  }
13111
13398
  }
13399
+ turnFailed(error) {
13400
+ const message = error?.message || "Codex ended the turn with an error.";
13401
+ const usageLimit = codexUsageLimitSignal(error, this.rateLimits, /* @__PURE__ */ new Date());
13402
+ return usageLimit === void 0 ? { type: "error", message } : { type: "error", message, usageLimit };
13403
+ }
13112
13404
  turnStarted(id) {
13113
13405
  const turn = this.turn;
13114
13406
  if (!turn || turn.id || typeof id !== "string") return;
@@ -13566,9 +13858,9 @@ function recordUnusable(servers, approval) {
13566
13858
  for (const server4 of servers) approval?.record?.(startRequest(server4), false, "unsupported");
13567
13859
  }
13568
13860
  var NOT_APPROVED = "not approved for planning";
13569
- function heldServersNote(held, unreadable = []) {
13861
+ function heldServersNote(held2, unreadable = []) {
13570
13862
  const lines2 = [
13571
- ...held.map(({ server: server4, reason }) => `- ${server4.name} (${server4.source}): ${reason}`),
13863
+ ...held2.map(({ server: server4, reason }) => `- ${server4.name} (${server4.source}): ${reason}`),
13572
13864
  ...unreadable.map((file) => `- ${file.source}: Ordewell could not read it (${file.reason})`)
13573
13865
  ];
13574
13866
  if (lines2.length === 0) return "";
@@ -13705,8 +13997,8 @@ var PendingSteers = class {
13705
13997
  * False when it was already delivered, so a late failure does not recall it.
13706
13998
  */
13707
13999
  forget(id) {
13708
- for (const [messageId, held] of this.pending) {
13709
- if (held !== id) continue;
14000
+ for (const [messageId, held2] of this.pending) {
14001
+ if (held2 !== id) continue;
13710
14002
  this.pending.delete(messageId);
13711
14003
  return true;
13712
14004
  }
@@ -13784,9 +14076,9 @@ var ChildSessions = class {
13784
14076
  adopt(child, callId) {
13785
14077
  if (this.owners.get(child)) return null;
13786
14078
  this.owners.set(child, callId);
13787
- const held = this.held.get(child) ?? [];
14079
+ const held2 = this.held.get(child) ?? [];
13788
14080
  this.held.delete(child);
13789
- return held;
14081
+ return held2;
13790
14082
  }
13791
14083
  owns(callId) {
13792
14084
  return [...this.owners.values()].includes(callId);
@@ -14310,11 +14602,11 @@ var OpenCodeV2 = class {
14310
14602
  adoptChild(data, state, onEvent, turn) {
14311
14603
  const child = data.metadata?.sessionID;
14312
14604
  const callId = data.id;
14313
- const held = child && callId ? state.children.adopt(child, callId) : null;
14314
- if (!held || !callId) return;
14605
+ const held2 = child && callId ? state.children.adopt(child, callId) : null;
14606
+ if (!held2 || !callId) return;
14315
14607
  const model = state.stepModels.get(data.assistantMessageID ?? "");
14316
14608
  onEvent({ type: "subagent_started", subagentId: callId, brief: state.briefs.get(callId) ?? "", ...model ? { model } : {} });
14317
- for (const frame of held) this.onFrame(frame, state, turn, onEvent);
14609
+ for (const frame of held2) this.onFrame(frame, state, turn, onEvent);
14318
14610
  }
14319
14611
  finishChild(callId, success, output, state, onEvent) {
14320
14612
  if (!state.children.owns(callId)) return;
@@ -15026,13 +15318,13 @@ var OpenCodeAdapter = class {
15026
15318
  trackSubagent(part, callId, turn, onEvent) {
15027
15319
  const state = part.state;
15028
15320
  const child = state?.metadata?.sessionId;
15029
- const held = child ? turn.children.adopt(child, callId) : null;
15030
- if (held) {
15321
+ const held2 = child ? turn.children.adopt(child, callId) : null;
15322
+ if (held2) {
15031
15323
  const input = state?.input ?? {};
15032
15324
  const brief2 = typeof input.description === "string" ? input.description : typeof input.prompt === "string" ? input.prompt : "";
15033
15325
  const model = flatModelId(state?.metadata?.model?.providerID, state?.metadata?.model?.modelID);
15034
15326
  onEvent({ type: "subagent_started", subagentId: callId, brief: brief2, ...model ? { model } : {} });
15035
- for (const frame of held) this.onFrame(frame, turn, onEvent);
15327
+ for (const frame of held2) this.onFrame(frame, turn, onEvent);
15036
15328
  }
15037
15329
  const status = state?.status;
15038
15330
  if (status === "completed" || status === "error") {
@@ -15262,61 +15554,6 @@ var OPENCODE_REPO_CONFIG = {
15262
15554
  }
15263
15555
  };
15264
15556
 
15265
- // src/utils/wallTime.ts
15266
- var MONTHS = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
15267
- function lastMatch(re, text3) {
15268
- let last = null;
15269
- for (const m of text3.matchAll(re)) last = m;
15270
- return last;
15271
- }
15272
- function hour24(hour, meridiem) {
15273
- const h = Number(hour) % 12;
15274
- return meridiem.toLowerCase() === "pm" ? h + 12 : h;
15275
- }
15276
- function month(name) {
15277
- if (!name) return void 0;
15278
- const at = MONTHS.indexOf(name.toLowerCase());
15279
- return at < 0 ? void 0 : at;
15280
- }
15281
- function placeWallTime(wall, zone, now) {
15282
- const today = wallFields(now, zone);
15283
- const year = wall.year ?? today.year;
15284
- const monthIndex = wall.month ?? today.month;
15285
- const day = wall.day ?? today.day;
15286
- const at = fromWall(year, monthIndex, day, wall.hour, wall.minute, zone);
15287
- if (wall.day !== void 0 || at.getTime() >= now.getTime() - HOUR_MS) return at;
15288
- return fromWall(year, monthIndex, day + 1, wall.hour, wall.minute, zone);
15289
- }
15290
- var HOUR_MS = 60 * 6e4;
15291
- function wallFields(instant, zone) {
15292
- if (!zone) {
15293
- return { year: instant.getFullYear(), month: instant.getMonth(), day: instant.getDate(), hour: instant.getHours(), minute: instant.getMinutes(), second: instant.getSeconds() };
15294
- }
15295
- const parts = new Intl.DateTimeFormat("en-US", {
15296
- timeZone: zone,
15297
- hourCycle: "h23",
15298
- year: "numeric",
15299
- month: "numeric",
15300
- day: "numeric",
15301
- hour: "numeric",
15302
- minute: "numeric",
15303
- second: "numeric"
15304
- }).formatToParts(instant);
15305
- const field = (type) => Number(parts.find((p) => p.type === type)?.value ?? 0);
15306
- return { year: field("year"), month: field("month") - 1, day: field("day"), hour: field("hour"), minute: field("minute"), second: field("second") };
15307
- }
15308
- function zoneOffsetMs(instant, zone) {
15309
- const f = wallFields(new Date(instant), zone);
15310
- return Date.UTC(f.year, f.month, f.day, f.hour, f.minute, f.second) - Math.floor(instant / 1e3) * 1e3;
15311
- }
15312
- function fromWall(year, monthIndex, day, hour, minute, zone) {
15313
- if (!zone) return new Date(year, monthIndex, day, hour, minute);
15314
- const asUtc = Date.UTC(year, monthIndex, day, hour, minute);
15315
- let instant = asUtc - zoneOffsetMs(asUtc, zone);
15316
- instant = asUtc - zoneOffsetMs(instant, zone);
15317
- return new Date(instant);
15318
- }
15319
-
15320
15557
  // src/services/harness/claudeUsageLimit.ts
15321
15558
  var CLAUDE_RESET_RE = /You['’]ve hit your [^\n]*?· (?:your [^·\n]*? )?resets (?:([A-Z][a-z]{2}) (\d{1,2}), (?:(\d{4}), )?)?(\d{1,2})(?::(\d{2}))?([ap]m) \(([A-Za-z0-9_+\-/]+)\)/g;
15322
15559
  function claudeUsageLimitReset(text3, now) {
@@ -15332,21 +15569,6 @@ function claudeUsageLimitReset(text3, now) {
15332
15569
  return placeWallTime(wall, zone, now);
15333
15570
  }
15334
15571
 
15335
- // src/services/harness/codexUsageLimit.ts
15336
- 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;
15337
- function codexUsageLimitReset(text3, now) {
15338
- const m = lastMatch(CODEX_RESET_RE, text3);
15339
- if (!m) return null;
15340
- const [, mon, day, year, hour, minute, meridiem] = m;
15341
- const wall = { hour: hour24(hour, meridiem), minute: Number(minute) };
15342
- if (mon) {
15343
- const at = month(mon);
15344
- if (at === void 0) return null;
15345
- Object.assign(wall, { month: at, day: Number(day), year: Number(year) });
15346
- }
15347
- return placeWallTime(wall, void 0, now);
15348
- }
15349
-
15350
15572
  // src/services/harness/connectors.ts
15351
15573
  var CONNECTORS = {
15352
15574
  "claude-code": { create: (deps) => new ClaudeCodeAdapter(deps), ordewellTools: CLAUDE_ORDEWELL, repoConfig: CLAUDE_REPO_CONFIG, usageLimitReset: claudeUsageLimitReset },
@@ -15677,6 +15899,7 @@ function parseCodexModelsCache(content) {
15677
15899
  }
15678
15900
  }
15679
15901
  var GEMINI_MODELS_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
15902
+ var NEGATIVE_TTL_MS = 6e4;
15680
15903
  function geminiDiscoveryCacheKey(apiKey) {
15681
15904
  const dir = globalDataDir();
15682
15905
  const hash = apiKey.slice(-8);
@@ -15744,7 +15967,8 @@ async function discoverGeminiModels(apiKey, baseUrl, fetchImpl) {
15744
15967
  modelId,
15745
15968
  modelLabel: m.displayName || modelId,
15746
15969
  runnerProvider: "gemini",
15747
- variants: []
15970
+ variants: [],
15971
+ ...typeof m.inputTokenLimit === "number" && m.inputTokenLimit > 0 ? { contextWindow: m.inputTokenLimit } : {}
15748
15972
  });
15749
15973
  }
15750
15974
  }
@@ -15806,26 +16030,33 @@ async function discoverFromApi(manifest, config, fetchImpl) {
15806
16030
  return null;
15807
16031
  }
15808
16032
  }
16033
+ function wasKilled(err) {
16034
+ return !!err && typeof err === "object" && err.killed === true;
16035
+ }
15809
16036
  async function discoverFromCommand(manifest, execImpl) {
15810
16037
  const discovery = manifest.modelDiscovery;
15811
16038
  const commands = discovery.discoveryCommands ?? [
15812
16039
  { command: discovery.command, args: discovery.args || [], parser: discovery.parser }
15813
16040
  ];
15814
16041
  const runCommands = async () => {
16042
+ let timedOut2 = false;
15815
16043
  for (const cmd of commands) {
15816
16044
  try {
15817
16045
  const { stdout } = await execImpl(`${cmd.command} ${cmd.args.join(" ")}`, { timeout: 45e3 });
15818
16046
  const models = parseModelOutput(stdout, cmd.parser, manifest);
15819
- if (models.length > 0) return applyVariants(mergeCanonicalAliases(models, manifest), manifest);
15820
- } catch {
16047
+ if (models.length > 0) return { models: applyVariants(mergeCanonicalAliases(models, manifest), manifest), timedOut: timedOut2 };
16048
+ } catch (err) {
16049
+ if (wasKilled(err)) timedOut2 = true;
15821
16050
  }
15822
16051
  }
15823
- return null;
16052
+ return { models: null, timedOut: timedOut2 };
15824
16053
  };
15825
16054
  const first = await runCommands();
15826
- if (first) return { models: first, fromFallback: false };
15827
- const second = await runCommands();
15828
- if (second) return { models: second, fromFallback: false };
16055
+ if (first.models) return { models: first.models, fromFallback: false };
16056
+ if (first.timedOut) {
16057
+ const second = await runCommands();
16058
+ if (second.models) return { models: second.models, fromFallback: false };
16059
+ }
15829
16060
  return { models: applyVariants(buildFallbackModels(manifest), manifest), fromFallback: true };
15830
16061
  }
15831
16062
  function mergeCanonicalAliases(discovered, manifest) {
@@ -15862,16 +16093,33 @@ function parseLineByLine(stdout) {
15862
16093
  }
15863
16094
  var ModelDiscovery = class {
15864
16095
  cache = /* @__PURE__ */ new Map();
16096
+ /**
16097
+ * Degraded results (a fallback list, or empty) remembered for
16098
+ * {@link NEGATIVE_TTL_MS}. They are deliberately not cached — a transient
16099
+ * failure must heal — but re-running a failing CLI on every lookup is the
16100
+ * other extreme.
16101
+ */
16102
+ misses = /* @__PURE__ */ new Map();
16103
+ /**
16104
+ * Discoveries still running, so a second lookup of the same runner joins the
16105
+ * first instead of spawning the CLI again. A slow CLI is the case this
16106
+ * exists for: a planner switch and a runner-list refresh land inside one
16107
+ * another's wait, and two `opencode models` calls race each other for the
16108
+ * same background service.
16109
+ */
16110
+ inflight = /* @__PURE__ */ new Map();
15865
16111
  registry;
15866
16112
  execImpl;
15867
16113
  fetchImpl;
15868
16114
  appServerClient;
15869
16115
  readFileImpl;
15870
- constructor(registry, execImpl = defaultExec2, fetchImpl, appServerClient = defaultAppServerClient, readFileImpl) {
16116
+ now;
16117
+ constructor(registry, execImpl = defaultExec2, fetchImpl, appServerClient = defaultAppServerClient, readFileImpl, now = Date.now) {
15871
16118
  this.registry = registry;
15872
16119
  this.execImpl = execImpl;
15873
16120
  this.fetchImpl = fetchImpl;
15874
16121
  this.appServerClient = appServerClient;
16122
+ this.now = now;
15875
16123
  this.readFileImpl = readFileImpl ?? ((filePath) => {
15876
16124
  try {
15877
16125
  return fsSync3.readFileSync(filePath.replace(/^~/, osMod.homedir()), "utf8");
@@ -15915,10 +16163,20 @@ var ModelDiscovery = class {
15915
16163
  const runnerLabel = manifest.displayName || runner;
15916
16164
  return models.map((m) => ({ ...m, runnerId: runner, runnerLabel }));
15917
16165
  }
15918
- async discoverRaw(runner) {
15919
- if (this.cache.has(runner)) {
15920
- return this.cache.get(runner);
15921
- }
16166
+ discoverRaw(runner) {
16167
+ const cached2 = this.cache.get(runner);
16168
+ if (cached2) return Promise.resolve(cached2);
16169
+ const remembered = this.misses.get(runner);
16170
+ if (remembered && this.now() - remembered.at < NEGATIVE_TTL_MS) return Promise.resolve(remembered.models);
16171
+ const running2 = this.inflight.get(runner);
16172
+ if (running2) return running2;
16173
+ const started = this.discoverUncached(runner).finally(() => {
16174
+ if (this.inflight.get(runner) === started) this.inflight.delete(runner);
16175
+ });
16176
+ this.inflight.set(runner, started);
16177
+ return started;
16178
+ }
16179
+ async discoverUncached(runner) {
15922
16180
  const manifest = this.registry.getManifest(runner);
15923
16181
  if (!manifest) {
15924
16182
  return [];
@@ -15950,10 +16208,13 @@ var ModelDiscovery = class {
15950
16208
  models = buildFallbackModels(manifest);
15951
16209
  }
15952
16210
  if (cacheable && models.length > 0) this.cache.set(runner, models);
16211
+ else this.misses.set(runner, { models, at: this.now() });
15953
16212
  return models;
15954
16213
  }
15955
16214
  clear() {
15956
16215
  this.cache.clear();
16216
+ this.misses.clear();
16217
+ this.inflight.clear();
15957
16218
  }
15958
16219
  /** Whatever `discover()` has already cached for `runner` — never triggers discovery. */
15959
16220
  getCached(runner) {
@@ -16115,6 +16376,7 @@ function parseMaxParallel(value) {
16115
16376
  // src/services/SettingsService.ts
16116
16377
  var fs10 = __toESM(require("fs"));
16117
16378
  var path26 = __toESM(require("path"));
16379
+ var import_crypto10 = require("crypto");
16118
16380
 
16119
16381
  // src/services/TaskOps.ts
16120
16382
  var import_uuid3 = require("uuid");
@@ -16168,7 +16430,14 @@ function clampThinkingEffort(effort, variants) {
16168
16430
  }
16169
16431
  return void 0;
16170
16432
  }
16171
- function coerceAssignments(tasks, perRunnerAllowlist, modelsByRunner) {
16433
+ function coerceAssignments(tasks, perRunnerAllowlist, modelsByRunner, planned = []) {
16434
+ const prior = new Map(flattenTasks(planned).map((t) => [t.id, t]));
16435
+ const unwritten = (task) => {
16436
+ const was = prior.get(task.id);
16437
+ if (!was?.assignedModel || !task.assignedModel || was.assignedRunner !== task.assignedRunner) return void 0;
16438
+ if (was.assignedModel.modelId !== task.assignedModel.modelId || was.assignedModel.thinkingEffort !== task.assignedModel.thinkingEffort) return void 0;
16439
+ return was;
16440
+ };
16172
16441
  const clampVariant = (task) => {
16173
16442
  if (!task.assignedModel || !modelsByRunner) return task;
16174
16443
  const model = modelsByRunner[task.assignedRunner]?.find(
@@ -16197,6 +16466,11 @@ function coerceAssignments(tasks, perRunnerAllowlist, modelsByRunner) {
16197
16466
  };
16198
16467
  const coerceOne = (task) => {
16199
16468
  if (task.type === "user") return task;
16469
+ const kept = unwritten(task);
16470
+ if (kept) {
16471
+ const left = { ...task, assignedModel: kept.assignedModel };
16472
+ return kept.status === "in_progress" || kept.status === "awaiting_user" ? left : clampVariant(left);
16473
+ }
16200
16474
  const allowlist = allowedFor(task.assignedRunner);
16201
16475
  if (!task.assignedModel) {
16202
16476
  if (allowlist && allowlist.length > 0) {
@@ -16259,7 +16533,7 @@ function blockedByLabel(refs) {
16259
16533
  function skippedLabel(reason) {
16260
16534
  return reason ? `Skipped: ${reason}` : "Skipped";
16261
16535
  }
16262
- var WAITS_FOR_APPROVAL_LABEL = "Waiting for your approval to start";
16536
+ var WAITS_FOR_APPROVAL_LABEL = "Waits for your approval";
16263
16537
  function approvalLabel(count) {
16264
16538
  if (!count) return null;
16265
16539
  return count > 1 ? `Waiting for approval (${count})` : "Waiting for approval";
@@ -16444,6 +16718,15 @@ function lockedTaskRefusal(task, action = "changed") {
16444
16718
  if (task.status === "completed") return `"${task.title}" is completed and cannot be ${action}: rearm it to run it again, or add a new task for further work`;
16445
16719
  return null;
16446
16720
  }
16721
+ function liveModelRefusal(task) {
16722
+ return `"${task.title}" has a live runner, so its model and effort cannot change now: stop the task first, or use the runner's own /model`;
16723
+ }
16724
+ function liveRunnerRefusal(task) {
16725
+ return `"${task.title}" has a live runner, so its runner cannot change now: stop the task first`;
16726
+ }
16727
+ function sameAssignment(a, b) {
16728
+ return a?.modelId === b?.modelId && a?.thinkingEffort === b?.thinkingEffort;
16729
+ }
16447
16730
  var AI_ONLY_FIELDS = ["assignedModel", "taskMode", "autonomy", "ops"];
16448
16731
  function checkModelAndModeValidity(runner, assignedModel, taskMode, catalog) {
16449
16732
  if (!catalog) return { ok: true };
@@ -16503,11 +16786,18 @@ function validateTaskEdit(actor, tasks, taskId, changes, catalog) {
16503
16786
  const { task: target, parent } = found;
16504
16787
  const locked = actor === "planner" ? lockedTaskRefusal(target, "modified") : null;
16505
16788
  if (locked) return { ok: false, error: locked };
16789
+ const coherence = typeCoherenceCheck(target, changes);
16790
+ const landing = coherence?.clear?.includes("assignedModel") ? { ...changes, assignedModel: void 0 } : changes;
16791
+ if ("assignedModel" in landing && !sameAssignment(landing.assignedModel, target.assignedModel) && catalog?.liveRunner?.(taskId)) {
16792
+ return { ok: false, error: liveModelRefusal(target) };
16793
+ }
16794
+ if ("assignedRunner" in changes && changes.assignedRunner !== target.assignedRunner && catalog?.liveRunner?.(taskId)) {
16795
+ return { ok: false, error: liveRunnerRefusal(target) };
16796
+ }
16506
16797
  if (changes.dependencies) {
16507
16798
  const depCheck = canSetDependencies(parent?.subtasks ?? tasks, taskId, changes.dependencies);
16508
16799
  if (!depCheck.ok) return depCheck;
16509
16800
  }
16510
- const coherence = typeCoherenceCheck(target, changes);
16511
16801
  if (coherence && !coherence.ok) return coherence;
16512
16802
  const ops = opsCheck(target, parent !== null, changes);
16513
16803
  if (ops) return ops;
@@ -16745,9 +17035,9 @@ function idsOf(op) {
16745
17035
  return ids[next++];
16746
17036
  };
16747
17037
  }
16748
- function applyTaskOps(currentTasks, newOps, runners, catalog, held = []) {
16749
- const ops = [...held, ...newOps];
16750
- const opLabel = (i) => i < held.length ? `earlier op ${i + 1} (${ops[i].op})` : `op ${i - held.length + 1} (${ops[i].op})`;
17038
+ function applyTaskOps(currentTasks, newOps, runners, catalog, held2 = []) {
17039
+ const ops = [...held2, ...newOps];
17040
+ const opLabel = (i) => i < held2.length ? `earlier op ${i + 1} (${ops[i].op})` : `op ${i - held2.length + 1} (${ops[i].op})`;
16751
17041
  let tasks = JSON.parse(JSON.stringify(currentTasks));
16752
17042
  const errors = [];
16753
17043
  const summary = [];
@@ -17439,23 +17729,34 @@ function createWakePolicy(options) {
17439
17729
  // src/services/runnerExit.ts
17440
17730
  var USAGE_LIMIT_RE = /\b(?:usage|session|weekly|daily|monthly) limit\b|\brate limit (?:exceeded|reached)\b|\brate[- ]limited\b|\blimit (?:will )?reset\b|\bquota (?:exceeded|reached)\b|\btoo many requests\b/i;
17441
17731
  var USAGE_LIMIT_TAIL = 4096;
17442
- function classifyRunnerStop(output) {
17443
- return USAGE_LIMIT_RE.test(output.slice(-USAGE_LIMIT_TAIL)) ? "usage-limit" : "stopped";
17732
+ function classifyRunnerStop(said2) {
17733
+ if (typeof said2 !== "string" && said2.usageLimit !== void 0) return said2.usageLimit ? "usage-limit" : "stopped";
17734
+ return USAGE_LIMIT_RE.test(wordsOf(said2).slice(-USAGE_LIMIT_TAIL)) ? "usage-limit" : "stopped";
17735
+ }
17736
+ function wordsOf(said2) {
17737
+ return typeof said2 === "string" ? said2 : said2.message;
17444
17738
  }
17445
17739
  function stopsRunner(reason) {
17446
17740
  return reason !== "stop" && reason !== "load";
17447
17741
  }
17448
- function readUsageLimitReset(runner, output, now = /* @__PURE__ */ new Date()) {
17742
+ function readUsageLimitReset(runner, said2, now = /* @__PURE__ */ new Date()) {
17743
+ const reset = (typeof said2 === "string" ? null : reportedReset(said2)) ?? readReset(runner, wordsOf(said2), now);
17744
+ if (!reset || reset.getTime() < now.getTime() - DAY_MS) return null;
17745
+ return reset;
17746
+ }
17747
+ function reportedReset(failure) {
17748
+ const at = failure.usageLimit ? failure.usageLimit.resetsAt : void 0;
17749
+ const reset = at === void 0 ? null : new Date(at);
17750
+ return reset && !Number.isNaN(reset.getTime()) ? reset : null;
17751
+ }
17752
+ function readReset(runner, output, now) {
17449
17753
  const read2 = connectorFor(runner)?.usageLimitReset;
17450
17754
  if (!read2) return null;
17451
- let reset;
17452
17755
  try {
17453
- reset = read2(output.slice(-USAGE_LIMIT_TAIL), now);
17756
+ return read2(output.slice(-USAGE_LIMIT_TAIL), now);
17454
17757
  } catch {
17455
17758
  return null;
17456
17759
  }
17457
- if (!reset || reset.getTime() < now.getTime() - DAY_MS) return null;
17458
- return reset;
17459
17760
  }
17460
17761
  var DAY_MS = 24 * 60 * 6e4;
17461
17762
 
@@ -17636,15 +17937,15 @@ var ReplySplitter = class {
17636
17937
  push(segmentId, text3) {
17637
17938
  const state = this.segments.get(segmentId);
17638
17939
  if (state === "text" || state === "plan") return { route: state, text: text3 };
17639
- const held = (state?.held ?? "") + text3;
17640
- const opensWithObject = opensWithJsonObject(held);
17940
+ const held2 = (state?.held ?? "") + text3;
17941
+ const opensWithObject = opensWithJsonObject(held2);
17641
17942
  if (opensWithObject === void 0) {
17642
- this.segments.set(segmentId, { held });
17943
+ this.segments.set(segmentId, { held: held2 });
17643
17944
  return null;
17644
17945
  }
17645
17946
  const route = opensWithObject ? "plan" : "text";
17646
17947
  this.segments.set(segmentId, route);
17647
- return { route, text: held };
17948
+ return { route, text: held2 };
17648
17949
  }
17649
17950
  };
17650
17951
  var TurnStream = class {
@@ -17799,6 +18100,7 @@ function plannerTranscript(history) {
17799
18100
  const loadsOf = /* @__PURE__ */ new Map();
17800
18101
  let owner = -1;
17801
18102
  for (const entry of history) {
18103
+ if (entry.kind === "error") continue;
17802
18104
  if (entry.kind === "skill_load" && entry.skill) {
17803
18105
  const loads = entry.skill.invokedBy === "user" ? loadsOf.get(owner) : void 0;
17804
18106
  if (loads) loads.push(entry.skill);
@@ -17887,6 +18189,8 @@ function handoffOf(run) {
17887
18189
  path: repo.path,
17888
18190
  integrationBranch: repo.integrationBranch,
17889
18191
  baseRef: repo.baseRef,
18192
+ ...repo.baseBranch ? { baseBranch: repo.baseBranch } : {},
18193
+ ...repo.startedDetached ? { startedDetached: true } : {},
17890
18194
  landed: merged.filter((r) => r.repos[repo.path]?.changed).map(entry)
17891
18195
  })),
17892
18196
  landed: merged.map(entry)
@@ -19216,6 +19520,9 @@ ${task.prompt ?? task.description}`;
19216
19520
  }
19217
19521
 
19218
19522
  // src/services/continuation.ts
19523
+ function sessionOwner(task) {
19524
+ return task.runnerSessionOwner ?? task.attemptUsage?.at(-1)?.runner;
19525
+ }
19219
19526
  function continuability(task) {
19220
19527
  const refuse = (reason) => ({ ok: false, reason: `Task "${task.title}" cannot be continued: ${reason}` });
19221
19528
  if (task.type !== "ai") return refuse("it is not run by an agent.");
@@ -19225,6 +19532,10 @@ function continuability(task) {
19225
19532
  const finished = task.status === "completed" || task.status === "failed" || task.status === "skipped";
19226
19533
  if (!finished && !pausedOnLimit(task)) return refuse("only a completed, failed or skipped task, or one paused on a usage limit, can be.");
19227
19534
  if (!task.runnerSessionId) return refuse("its runner left no saved session to resume. Use Retry instead.");
19535
+ const owner = sessionOwner(task);
19536
+ if (owner && owner !== task.assignedRunner) {
19537
+ return refuse(`its saved session belongs to ${owner}; Retry starts fresh on ${task.assignedRunner} and carries over its files.`);
19538
+ }
19228
19539
  return { ok: true, sessionId: task.runnerSessionId };
19229
19540
  }
19230
19541
  function canContinue(task) {
@@ -19348,6 +19659,7 @@ function serializePlan(plan) {
19348
19659
  runners: plan.runners,
19349
19660
  generatedAt: plan.generatedAt,
19350
19661
  runSettings: plan.runSettings,
19662
+ ...plan.plannerResend ? { plannerResend: plan.plannerResend } : {},
19351
19663
  conversationHistory: surfaceConversation(plan.conversationHistory),
19352
19664
  prdMarkdown: plan.prdMarkdown
19353
19665
  };
@@ -19388,6 +19700,70 @@ function markOf(was, now) {
19388
19700
  return editedFields(was, now).length > 0 || was.status !== now.status ? "changed" : void 0;
19389
19701
  }
19390
19702
 
19703
+ // src/services/AssignmentScope.ts
19704
+ var OVER = /* @__PURE__ */ new Set(["completed", "skipped"]);
19705
+ function assignmentIssue(task, scope) {
19706
+ if (task.type !== "ai" || OVER.has(task.status)) return null;
19707
+ if (!scope.enabledRunners.includes(task.assignedRunner)) return { kind: "runner-off", runner: task.assignedRunner };
19708
+ const model = task.assignedModel;
19709
+ const allowed = effectiveAllowlist(scope.allowlist[task.assignedRunner], task.assignedRunner, scope.models);
19710
+ if (model && allowed && !allowed.includes(model.modelId)) {
19711
+ return { kind: "model-off-list", runner: task.assignedRunner, modelId: model.modelId, modelLabel: model.modelLabel || model.modelId };
19712
+ }
19713
+ return null;
19714
+ }
19715
+ function assignmentIssueText(issue) {
19716
+ return issue.kind === "runner-off" ? `${issue.runner} is switched off` : `${issue.modelLabel} is not in the allowlist`;
19717
+ }
19718
+ function assignmentConsequence({ status, awaitingReason, resumeAt }) {
19719
+ if (status === "failed") return "failed; a retry would run it there unless moved";
19720
+ if (status === "awaiting_user" && awaitingReason === "usage-limit") {
19721
+ return resumeAt ? "paused on a usage limit; resumes there at the reset unless moved" : "paused on a usage limit; a retry would run it there unless moved";
19722
+ }
19723
+ if (status === "awaiting_user" && (awaitingReason === "conflict" || awaitingReason === "files-changed")) return "waiting for the user; what runs next runs there unless moved";
19724
+ if (status === "in_progress" || status === "awaiting_user") return "already running as assigned";
19725
+ return "runs as assigned unless moved";
19726
+ }
19727
+
19728
+ // src/services/RunnerChange.ts
19729
+ var sameSet = (a, b) => a === void 0 || b === void 0 ? a === b : a.length === b.length && a.every((id) => b.includes(id));
19730
+ function scopeChange(told, now) {
19731
+ const switchedOn = now.enabledRunners.filter((r) => !told.enabledRunners.includes(r));
19732
+ const switchedOff = told.enabledRunners.filter((r) => !now.enabledRunners.includes(r));
19733
+ const models = now.models;
19734
+ const allowlists = [.../* @__PURE__ */ new Set([...Object.keys(told.allowlist), ...Object.keys(now.allowlist)])].map((runner) => ({
19735
+ runner,
19736
+ before: effectiveAllowlist(told.allowlist[runner], runner, models),
19737
+ after: effectiveAllowlist(now.allowlist[runner], runner, models)
19738
+ })).filter((c) => !sameSet(c.before, c.after));
19739
+ return switchedOn.length || switchedOff.length || allowlists.length ? { switchedOn, switchedOff, allowlists } : null;
19740
+ }
19741
+ function flaggedTasks(rows, scope) {
19742
+ return rows.flatMap(({ ref, task }) => {
19743
+ const issue = assignmentIssue(task, scope);
19744
+ return issue ? [{ ref, title: task.title, task, text: assignmentIssueText(issue), status: task.status, awaitingReason: task.awaitingReason, resumeAt: task.resumeAt }] : [];
19745
+ });
19746
+ }
19747
+ function allowlistLine({ runner, before, after }) {
19748
+ if (!after) return `${runner} is no longer limited: any of its models may be assigned.`;
19749
+ if (!before) return `${runner} is now limited to: ${after.join(", ")}.`;
19750
+ const added = after.filter((id) => !before.includes(id));
19751
+ const removed = before.filter((id) => !after.includes(id));
19752
+ return `${runner} models: ${[added.length ? `now allowed ${added.join(", ")}` : "", removed.length ? `no longer allowed ${removed.join(", ")}` : ""].filter(Boolean).join("; ")}.`;
19753
+ }
19754
+ function runnersChangedBlock(change, flagged, tools) {
19755
+ return [
19756
+ "<runners_changed>",
19757
+ "The user changed which runners and models you may assign since your last turn.",
19758
+ change.switchedOn.length ? `Runners switched on: ${change.switchedOn.join(", ")}.` : "",
19759
+ change.switchedOff.length ? `Runners switched off: ${change.switchedOff.join(", ")}.` : "",
19760
+ ...change.allowlists.map(allowlistLine),
19761
+ flagged.length ? ["These tasks are now outside that:", ...flagged.map((f) => `- ${f.ref} "${f.title}" (${f.text}) \u2014 ${assignmentConsequence(f)}`)].join("\n") : "",
19762
+ `${tools ? "list_runners and list_models give" : "<available_models> shows"} what you may assign now.${flagged.length ? " Consider moving the tasks above to an enabled runner and an allowed model, or tell the user why you left them." : ""}`,
19763
+ "</runners_changed>"
19764
+ ].filter(Boolean).join("\n");
19765
+ }
19766
+
19391
19767
  // src/services/PlannerConversation.ts
19392
19768
  var CATALOG_MODEL_CAP = 100;
19393
19769
  var PLANNER_TOOLS_REMINDER = "Runners, models and modes may have changed since you last read them: call list_runners and list_models just before submit_plan.";
@@ -19452,6 +19828,17 @@ var PlannerTurnFailedError = class extends Error {
19452
19828
  this.name = "PlannerTurnFailedError";
19453
19829
  }
19454
19830
  };
19831
+ var PlannerUsageLimitError = class extends PlannerTurnFailedError {
19832
+ resendAt;
19833
+ /** Why nothing is sent again on its own, when nothing is. */
19834
+ held;
19835
+ constructor(limit) {
19836
+ super(limit.text);
19837
+ this.name = "PlannerUsageLimitError";
19838
+ this.resendAt = limit.resendAt;
19839
+ if (limit.held) this.held = limit.held;
19840
+ }
19841
+ };
19455
19842
  var PlannerTurnStoppedError = class extends Error {
19456
19843
  constructor(options) {
19457
19844
  super("The planner turn was stopped.", options);
@@ -19477,6 +19864,8 @@ var PlannerConversation = class {
19477
19864
  savedInBackground = 0;
19478
19865
  /** What the planner was last told of autorun: by its opening prompt, or a turn's block since. */
19479
19866
  toldAutorun = false;
19867
+ /** The runners and allowlist the planner was last told of: its opening prompt, or a turn's block since. Null until a conversation opens. */
19868
+ toldScope = null;
19480
19869
  live = null;
19481
19870
  openTurn = null;
19482
19871
  get transcript() {
@@ -19592,8 +19981,8 @@ var PlannerConversation = class {
19592
19981
  * from the moment it is handed in until it lands, is refused, or the turn
19593
19982
  * ends: the backend's reply claims it, but it has not landed by then.
19594
19983
  */
19595
- showHeld(turn, held) {
19596
- const result = held?.kind === "task_ops" ? this.host.validateOps(held.ops) : void 0;
19984
+ showHeld(turn, held2) {
19985
+ const result = held2?.kind === "task_ops" ? this.host.validateOps(held2.ops) : void 0;
19597
19986
  if (!result?.ok) {
19598
19987
  this.clearHeld(turn);
19599
19988
  return;
@@ -19612,9 +20001,9 @@ var PlannerConversation = class {
19612
20001
  * a whole plan, which an edit cannot be layered on.
19613
20002
  */
19614
20003
  pendingOps() {
19615
- const held = this.openTurn?.submitted;
19616
- if (!held) return [];
19617
- return held.kind === "task_ops" ? held.ops : null;
20004
+ const held2 = this.openTurn?.submitted;
20005
+ if (!held2) return [];
20006
+ return held2.kind === "task_ops" ? held2.ops : null;
19618
20007
  }
19619
20008
  /**
19620
20009
  * A read the planner makes through a tool. It spends the same per-turn budget
@@ -19654,7 +20043,8 @@ var PlannerConversation = class {
19654
20043
  history: plan.conversationHistory,
19655
20044
  researchLog: plan.researchLog,
19656
20045
  persisted: this.persisted,
19657
- savedInBackground: this.savedInBackground
20046
+ savedInBackground: this.savedInBackground,
20047
+ toldScope: this.toldScope
19658
20048
  };
19659
20049
  }
19660
20050
  /**
@@ -19668,7 +20058,9 @@ var PlannerConversation = class {
19668
20058
  * the undo is saved in turn, or a reload would bring the writes back.
19669
20059
  */
19670
20060
  restore(snapshot) {
19671
- if (this.host.plan() !== snapshot.plan || this.persisted !== snapshot.persisted) return false;
20061
+ if (this.host.plan() !== snapshot.plan) return false;
20062
+ this.toldScope = snapshot.toldScope;
20063
+ if (this.persisted !== snapshot.persisted) return false;
19672
20064
  const undo = () => {
19673
20065
  const actions = (snapshot.plan.conversationHistory ?? []).filter((entry) => entry.actor === "Ordewell setting" && !(snapshot.history ?? []).includes(entry));
19674
20066
  snapshot.plan.conversationHistory = actions.length > 0 ? [...snapshot.history ?? [], ...actions] : snapshot.history;
@@ -19703,8 +20095,9 @@ var PlannerConversation = class {
19703
20095
  * at it.
19704
20096
  */
19705
20097
  rewindTargets() {
20098
+ const opener = this.transcript.findIndex((m) => m.kind !== "error");
19706
20099
  return this.transcript.flatMap((m, index) => {
19707
- if (!isUserMessage(m) || index === 0) return [];
20100
+ if (!isUserMessage(m) || index === opener) return [];
19708
20101
  const line = m.content.split("\n")[0];
19709
20102
  const preview = line.length > REWIND_PREVIEW_WIDTH ? `${line.slice(0, REWIND_PREVIEW_WIDTH - 1)}\u2026` : line;
19710
20103
  return [{ index, preview, content: m.content, timestamp: m.timestamp }];
@@ -19831,8 +20224,10 @@ var PlannerConversation = class {
19831
20224
  return this.userTurn("supersede", goal, signal, async (userTurn) => {
19832
20225
  const opening = await prepare();
19833
20226
  this.assertCurrent(userTurn);
20227
+ userTurn.checkpoint = this.snapshot();
19834
20228
  this.recordUser(goal, (/* @__PURE__ */ new Date()).toISOString());
19835
20229
  this.toldAutorun = opening.autorun ?? false;
20230
+ this.toldScope = this.host.assignmentScope();
19836
20231
  const turn = await this.host.aiService().startConversation({
19837
20232
  ...opening,
19838
20233
  // The system prompt's goal is the text as typed; skill bodies ride the
@@ -19842,7 +20237,9 @@ var PlannerConversation = class {
19842
20237
  onProgress: userTurn.stream.sink(),
19843
20238
  signal: userTurn.signal
19844
20239
  });
19845
- return this.settle(await this.drainTaskQueries(turn, userTurn), userTurn);
20240
+ const settleable = await this.drainTaskQueries(turn, userTurn);
20241
+ this.assertNotLimited(settleable, userTurn);
20242
+ return this.settle(settleable, userTurn);
19846
20243
  });
19847
20244
  }
19848
20245
  /**
@@ -19875,13 +20272,16 @@ var PlannerConversation = class {
19875
20272
  // anything the turn appends as it runs goes after this point.
19876
20273
  at: priorHistory.length
19877
20274
  };
19878
- const contextBlock = [this.catalogBlock(), this.planContextBlock(), this.autorunBlock(), this.runUpdatesBlock()].filter(Boolean).join("\n\n");
20275
+ const toldBefore = this.toldScope;
20276
+ const contextBlock = [this.catalogBlock(), this.planContextBlock(), this.autorunBlock(), this.runnersChangedBlock(), this.runUpdatesBlock()].filter(Boolean).join("\n\n");
19879
20277
  const said2 = fromUser ? plannerMessage(message.text, message.skills) : "";
19880
20278
  const outgoing = [contextBlock, ...wakeUps, userTurn.wakeUp?.brief, said2].filter(Boolean).join("\n\n");
19881
20279
  const ai = this.host.aiService();
19882
20280
  try {
19883
20281
  const turn = this.canContinueLive() ? await ai.continueConversation(outgoing, userTurn.stream.sink(), signal) : await this.resume(outgoing, priorHistory, signal, userTurn.stream.sink());
19884
20282
  const settleable = await this.drainTaskQueries(turn, userTurn);
20283
+ if (settleable.kind === "message" && (settleable.failed || userTurn.signal.aborted)) this.toldScope = toldBefore;
20284
+ this.assertNotLimited(settleable, userTurn);
19885
20285
  if (userTurn.wakeUp) this.assertWakeUpAnswered(settleable, userTurn);
19886
20286
  this.assertCurrent(userTurn, settleable);
19887
20287
  return await this.settle(settleable, userTurn);
@@ -19900,6 +20300,34 @@ var PlannerConversation = class {
19900
20300
  if (userTurn.signal.aborted) throw new PlannerTurnStoppedError();
19901
20301
  if (turn.kind === "message" && turn.failed) throw new PlannerTurnFailedError(turn.text);
19902
20302
  }
20303
+ /**
20304
+ * A turn that ended on the planner's usage limit lands nothing. What it
20305
+ * wrote is taken back, its failure is kept in its place, and the limit's
20306
+ * wait is armed in the same save. Only a turn Ordewell started gets here,
20307
+ * so a limit is never acted on for a planner nobody asked anything of.
20308
+ */
20309
+ assertNotLimited(turn, userTurn) {
20310
+ if (turn.kind !== "message" || !turn.failed || userTurn.signal.aborted) return;
20311
+ const said2 = { message: turn.text, usageLimit: turn.usageLimit };
20312
+ if (classifyRunnerStop(said2) !== "usage-limit") return;
20313
+ this.assertCurrent(userTurn);
20314
+ if (userTurn.checkpoint) this.restore(userTurn.checkpoint);
20315
+ const limit = this.host.usageLimit(said2);
20316
+ this.host.mutate(() => {
20317
+ this.recordFailure(limit.text);
20318
+ return true;
20319
+ }, () => this.host.broadcastPlan());
20320
+ throw new PlannerUsageLimitError(limit);
20321
+ }
20322
+ /**
20323
+ * A failure kept in the transcript, drawn as one on reload too, and never
20324
+ * replayed to the planner. Stamped after the newest entry: surfaces take
20325
+ * only entries newer than the last they synced.
20326
+ */
20327
+ recordFailure(text3) {
20328
+ const newest = this.transcript.reduce((latest, e) => Math.max(latest, Date.parse(e.timestamp) || 0), 0);
20329
+ this.append("assistant", text3, { kind: "error", timestamp: new Date(Math.max(Date.now(), newest + 1)).toISOString() });
20330
+ }
19903
20331
  assertIdle(operation) {
19904
20332
  if (this.isTurnInFlight) throw new ConversationBusyError(operation);
19905
20333
  }
@@ -20041,6 +20469,7 @@ var PlannerConversation = class {
20041
20469
  const goal = opener || this.host.goal() || message;
20042
20470
  this.reset();
20043
20471
  this.toldAutorun = opening.autorun ?? false;
20472
+ this.toldScope = this.host.assignmentScope();
20044
20473
  return this.host.aiService().startConversation({
20045
20474
  ...opening,
20046
20475
  runners,
@@ -20279,6 +20708,24 @@ The plan is unchanged. Rephrase the request, or adjust the tasks manually.`,
20279
20708
  this.toldAutorun = now;
20280
20709
  return now ? AUTORUN_ON : AUTORUN_OFF;
20281
20710
  }
20711
+ /**
20712
+ * The user's runner or allowlist change, when the planner was not told yet —
20713
+ * the same once-per-change rule as autorun, for API and harness planners
20714
+ * alike. Tasks the change leaves outside the scope are named, not touched.
20715
+ *
20716
+ * A reloaded session is not told: {@link resume} takes the scope as it is
20717
+ * then, as it does the autorun rule, since the catalog the first turn carries
20718
+ * and `list_runners` already show the runners as they are now.
20719
+ */
20720
+ runnersChangedBlock() {
20721
+ const now = this.host.assignmentScope();
20722
+ const told = this.toldScope;
20723
+ this.toldScope = now;
20724
+ const change = told && scopeChange(told, now);
20725
+ if (!change) return null;
20726
+ const flagged = flaggedTasks(flattenTasksWithParents(this.host.tasks()), now);
20727
+ return runnersChangedBlock(change, flagged, this.host.aiService().plannerToolsAttached?.() ?? false);
20728
+ }
20282
20729
  runUpdatesBlock() {
20283
20730
  const updates = this.host.takeRunUpdates();
20284
20731
  return updates.length > 0 ? ["Run update:", ...updates.map((update) => `- ${update}`)].join("\n") : null;
@@ -20359,7 +20806,6 @@ var WAKE_DEBOUNCE_MS = 5e3;
20359
20806
  var WAKE_UP_TURN_TIMEOUT_MS = 10 * 6e4;
20360
20807
  var HUNG_TURN_GRACE_MS = 3e4;
20361
20808
  var DEFAULT_WAKE_UP_READ_BUDGET = 12;
20362
- var RESET_MARGIN_MS = 6e4;
20363
20809
  var DETAIL_CHARS = 240;
20364
20810
  var WakeUpTimeoutError = class extends Error {
20365
20811
  constructor(timeoutMs) {
@@ -20385,8 +20831,6 @@ var Supervisor = class {
20385
20831
  /** The run's end was reported: the scheduler can announce it more than once. */
20386
20832
  finished = false;
20387
20833
  runEndPending = false;
20388
- /** While the planner's usage limit holds wake-ups back: when it lets them go. */
20389
- limited = null;
20390
20834
  /** When each wake-up since the last pass was delivered, for the wake-up rate guard. */
20391
20835
  wakeUps = [];
20392
20836
  debounceMs;
@@ -20407,9 +20851,9 @@ var Supervisor = class {
20407
20851
  get awaitsRunEnd() {
20408
20852
  return this.runEndPending;
20409
20853
  }
20410
- /** Whether wake-ups alone may open a turn now: the supervisor watches, and no usage limit holds it. */
20854
+ /** Whether wake-ups alone may open a turn now: the supervisor watches. A usage limit holds every turn, and is the planner turns' to wait out. */
20411
20855
  mayWake() {
20412
- return this.watching && this.limited === null;
20856
+ return this.watching;
20413
20857
  }
20414
20858
  /**
20415
20859
  * What a wake-up turn is held to under the run's level. The brief is the
@@ -20476,8 +20920,6 @@ var Supervisor = class {
20476
20920
  /** The plan these events were about is gone: nothing gathered or waited on carries over. */
20477
20921
  reset() {
20478
20922
  this.disarm();
20479
- if (this.limited) clearTimeout(this.limited.timer);
20480
- this.limited = null;
20481
20923
  this.failures = 0;
20482
20924
  this.finished = false;
20483
20925
  this.runEndPending = false;
@@ -20528,9 +20970,13 @@ var Supervisor = class {
20528
20970
  }
20529
20971
  failed(err) {
20530
20972
  if (err instanceof PlannerTurnStoppedError || err instanceof PlannerTurnDiscardedError) return;
20973
+ if (err instanceof PlannerUsageLimitError) {
20974
+ if (err.resendAt === null) this.giveUp(`the planner hit its usage limit and ${err.held ?? "did not say when it resets"}`);
20975
+ return;
20976
+ }
20531
20977
  const reason = err instanceof Error ? err.message : String(err);
20532
- if (!(err instanceof WakeUpTimeoutError) && classifyRunnerStop(reason) === "usage-limit") {
20533
- this.waitForReset(reason);
20978
+ if (!(err instanceof WakeUpTimeoutError) && !(err instanceof PlannerTurnFailedError) && classifyRunnerStop(reason) === "usage-limit") {
20979
+ this.giveUp("the planner hit its usage limit and did not say when it resets");
20534
20980
  return;
20535
20981
  }
20536
20982
  this.failures++;
@@ -20541,22 +20987,6 @@ var Supervisor = class {
20541
20987
  }
20542
20988
  this.giveUp(`its wake-up turn failed twice (${brief(reason)})`);
20543
20989
  }
20544
- waitForReset(said2) {
20545
- const runner = this.host.plannerRunner();
20546
- const reset = runner ? readUsageLimitReset(runner, said2, new Date(this.now())) : null;
20547
- if (!reset) {
20548
- this.giveUp("the planner hit its usage limit and did not say when it resets");
20549
- return;
20550
- }
20551
- if (this.limited) clearTimeout(this.limited.timer);
20552
- const until = reset.getTime() + RESET_MARGIN_MS;
20553
- const timer = setTimeout(() => {
20554
- this.limited = null;
20555
- this.host.pump();
20556
- }, Math.max(0, until - this.now()));
20557
- this.limited = { until, timer };
20558
- 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.`);
20559
- }
20560
20990
  /** The run goes on without a supervisor, and nothing that happened is lost (see {@link standDown}). */
20561
20991
  giveUp(reason) {
20562
20992
  this.host.goUnsupervised(reason);
@@ -20570,8 +21000,8 @@ var Supervisor = class {
20570
21000
  standDown() {
20571
21001
  this.disarm();
20572
21002
  this.runEndPending = false;
20573
- const held = this.policy.poll(Number.POSITIVE_INFINITY);
20574
- if (held) this.deliver(held);
21003
+ const held2 = this.policy.poll(Number.POSITIVE_INFINITY);
21004
+ if (held2) this.deliver(held2);
20575
21005
  }
20576
21006
  arm() {
20577
21007
  const due = this.policy.dueAt();
@@ -20762,78 +21192,224 @@ function runSettingsOf(settings) {
20762
21192
  }
20763
21193
  return out;
20764
21194
  }
21195
+ var SAVE_TRIES = 5;
21196
+ var LOCK_STALE_MS = 2e3;
21197
+ var LOCK_TIMEOUT_MS = 1e4;
21198
+ var LOCK_POLL_MS = 5;
21199
+ var LOCK_POLL_MAX_MS = 100;
21200
+ function sleepSync(ms) {
21201
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
21202
+ }
21203
+ function withFileLock(file, swap) {
21204
+ const lock = `${file}.lock`;
21205
+ const token = `${process.pid}:${(0, import_crypto10.randomUUID)()}`;
21206
+ const deadline = Date.now() + LOCK_TIMEOUT_MS;
21207
+ let last = null;
21208
+ let poll = LOCK_POLL_MS;
21209
+ for (; ; ) {
21210
+ try {
21211
+ const fd = fs10.openSync(lock, "wx");
21212
+ try {
21213
+ fs10.writeSync(fd, token);
21214
+ } finally {
21215
+ fs10.closeSync(fd);
21216
+ }
21217
+ break;
21218
+ } catch (err) {
21219
+ if (err.code !== "EEXIST") throw err;
21220
+ }
21221
+ if (Date.now() >= deadline) {
21222
+ throw new Error(`Cannot save the settings file: ${lock} is still held after ${Math.round(LOCK_TIMEOUT_MS / 1e3)} seconds by a process that is not finishing, or it cannot be removed. Check for a process stuck saving there, and delete the file if there is none.`);
21223
+ }
21224
+ let holder;
21225
+ let mtimeMs;
21226
+ try {
21227
+ holder = fs10.readFileSync(lock, "utf8");
21228
+ mtimeMs = fs10.statSync(lock).mtimeMs;
21229
+ } catch (err) {
21230
+ if (gone(err)) {
21231
+ last = null;
21232
+ continue;
21233
+ }
21234
+ throw err;
21235
+ }
21236
+ const stale = Date.now() - mtimeMs > LOCK_STALE_MS;
21237
+ if (last && last.token === holder && last.mtimeMs === mtimeMs && stale) {
21238
+ unlinkLock(lock);
21239
+ last = null;
21240
+ continue;
21241
+ }
21242
+ last = stale ? { token: holder, mtimeMs } : null;
21243
+ sleepSync(poll);
21244
+ poll = Math.min(poll * 2, LOCK_POLL_MAX_MS);
21245
+ }
21246
+ try {
21247
+ return swap();
21248
+ } finally {
21249
+ releaseLock(lock, token);
21250
+ }
21251
+ }
21252
+ function gone(err) {
21253
+ return err.code === "ENOENT";
21254
+ }
21255
+ function releaseLock(lock, token) {
21256
+ let holder;
21257
+ try {
21258
+ holder = fs10.readFileSync(lock, "utf8");
21259
+ } catch (err) {
21260
+ if (gone(err)) return;
21261
+ throw err;
21262
+ }
21263
+ if (holder === token) unlinkLock(lock);
21264
+ }
21265
+ function unlinkLock(lock) {
21266
+ try {
21267
+ fs10.unlinkSync(lock);
21268
+ } catch (err) {
21269
+ if (!gone(err)) throw err;
21270
+ }
21271
+ }
20765
21272
  var SettingsService = class {
20766
21273
  filePath;
20767
21274
  cache = null;
20768
- cachedMtimeMs = null;
21275
+ cachedStamp = null;
20769
21276
  constructor(filePath = getSettingsPath()) {
20770
21277
  this.filePath = filePath;
20771
21278
  }
20772
21279
  // Several processes share the settings file (VS Code extension host, web
20773
- // server, CLI), so the cache is keyed on the file's mtime: a write from any
21280
+ // server, CLI), so the cache is keyed on the file's identity: a write from any
20774
21281
  // other process invalidates it. A stale cache here previously meant an
20775
21282
  // allowlist set in one surface never reached another's planner.
20776
21283
  getAll() {
20777
- const mtime = this.fileMtimeMs();
20778
- if (this.cache && mtime === this.cachedMtimeMs) return this.cache;
21284
+ const stamp = this.fileStamp();
21285
+ if (this.cache && stamp === this.cachedStamp) return this.cache;
20779
21286
  this.cache = this.load();
20780
- this.cachedMtimeMs = mtime;
21287
+ this.cachedStamp = stamp;
20781
21288
  return this.cache;
20782
21289
  }
20783
- fileMtimeMs() {
21290
+ /**
21291
+ * Every write replaces the file, so its inode changes with it; the mtime
21292
+ * alone cannot tell two writes apart on a filesystem whose timestamps are
21293
+ * coarse, and a write that shares the last one's tick stayed unseen.
21294
+ */
21295
+ fileStamp(file = this.filePath) {
20784
21296
  try {
20785
- return fs10.statSync(this.filePath).mtimeMs;
21297
+ const stat = fs10.statSync(file);
21298
+ return `${stat.ino}:${stat.mtimeMs}:${stat.size}`;
20786
21299
  } catch {
20787
21300
  return null;
20788
21301
  }
20789
21302
  }
21303
+ /**
21304
+ * What a change is applied to: the file as it is now, never a copy held since
21305
+ * an earlier read. Another process may have saved in between, and writing
21306
+ * the whole file back would silently undo it.
21307
+ */
21308
+ current() {
21309
+ this.cache = null;
21310
+ return this.getAll();
21311
+ }
21312
+ /**
21313
+ * Calls `listener` when another process (or this one) saves the file. The
21314
+ * directory is watched, not the file: a write that swaps the file in leaves
21315
+ * a watch on the old one pointing at nothing.
21316
+ */
21317
+ watch(listener) {
21318
+ const dir = path26.dirname(this.filePath);
21319
+ const name = path26.basename(this.filePath);
21320
+ let seen = this.fileStamp();
21321
+ let timer;
21322
+ let watcher;
21323
+ try {
21324
+ fs10.mkdirSync(dir, { recursive: true });
21325
+ watcher = fs10.watch(dir, (_event, changed) => {
21326
+ if (changed && changed !== name) return;
21327
+ clearTimeout(timer);
21328
+ timer = setTimeout(() => {
21329
+ const stamp = this.fileStamp();
21330
+ if (stamp === seen) return;
21331
+ seen = stamp;
21332
+ listener();
21333
+ }, 50);
21334
+ });
21335
+ } catch {
21336
+ return { dispose() {
21337
+ } };
21338
+ }
21339
+ return { dispose: () => {
21340
+ clearTimeout(timer);
21341
+ watcher.close();
21342
+ } };
21343
+ }
20790
21344
  getModelAllowlist(runner) {
20791
21345
  return this.getAll().modelAllowlist?.[runner];
20792
21346
  }
20793
21347
  setModelAllowlist(runner, ids) {
20794
- this.getAll();
20795
- if (ids === void 0) {
20796
- if (this.cache.modelAllowlist) {
20797
- delete this.cache.modelAllowlist[runner];
20798
- if (Object.keys(this.cache.modelAllowlist).length === 0) {
20799
- this.cache.modelAllowlist = void 0;
21348
+ this.save(() => {
21349
+ if (ids === void 0) {
21350
+ if (this.cache.modelAllowlist) {
21351
+ delete this.cache.modelAllowlist[runner];
21352
+ if (Object.keys(this.cache.modelAllowlist).length === 0) {
21353
+ this.cache.modelAllowlist = void 0;
21354
+ }
20800
21355
  }
21356
+ } else {
21357
+ if (!this.cache.modelAllowlist) {
21358
+ this.cache.modelAllowlist = {};
21359
+ }
21360
+ this.cache.modelAllowlist[runner] = ids;
20801
21361
  }
20802
- } else {
20803
- if (!this.cache.modelAllowlist) {
20804
- this.cache.modelAllowlist = {};
20805
- }
20806
- this.cache.modelAllowlist[runner] = ids;
20807
- }
20808
- this.persist();
21362
+ });
20809
21363
  }
20810
21364
  getPlannerModel(provider) {
20811
21365
  return this.getAll().plannerModels?.[provider];
20812
21366
  }
20813
21367
  setPlannerModel(provider, entry) {
20814
- this.getAll();
20815
- if (entry === void 0) {
20816
- if (this.cache.plannerModels) {
20817
- delete this.cache.plannerModels[provider];
20818
- if (Object.keys(this.cache.plannerModels).length === 0) {
20819
- this.cache.plannerModels = void 0;
21368
+ this.save(() => {
21369
+ if (entry === void 0) {
21370
+ if (this.cache.plannerModels) {
21371
+ delete this.cache.plannerModels[provider];
21372
+ if (Object.keys(this.cache.plannerModels).length === 0) {
21373
+ this.cache.plannerModels = void 0;
21374
+ }
20820
21375
  }
21376
+ } else {
21377
+ if (!this.cache.plannerModels) {
21378
+ this.cache.plannerModels = {};
21379
+ }
21380
+ this.cache.plannerModels[provider] = entry.effort ? { model: entry.model, effort: entry.effort } : { model: entry.model };
20821
21381
  }
20822
- } else {
20823
- if (!this.cache.plannerModels) {
20824
- this.cache.plannerModels = {};
20825
- }
20826
- this.cache.plannerModels[provider] = entry.effort ? { model: entry.model, effort: entry.effort } : { model: entry.model };
20827
- }
20828
- this.persist();
21382
+ });
20829
21383
  }
20830
21384
  getEnabledRunners() {
20831
21385
  return this.getAll().enabledRunners;
20832
21386
  }
20833
21387
  setEnabledRunners(ids) {
20834
- this.getAll();
20835
- this.cache.enabledRunners = [...ids];
20836
- this.persist();
21388
+ this.save(() => {
21389
+ this.cache.enabledRunners = [...ids];
21390
+ });
21391
+ }
21392
+ /**
21393
+ * Change the list from what the file holds at this moment: the change is
21394
+ * handed the saved list (undefined when the user never chose) and its result
21395
+ * is saved in the same call. A list worked out from an earlier read would
21396
+ * save over what another surface set since. It may be handed the list again,
21397
+ * when another process saved before this one could (see {@link save}).
21398
+ */
21399
+ updateEnabledRunners(change) {
21400
+ return this.save(() => {
21401
+ const next = [...change(this.cache.enabledRunners)];
21402
+ this.cache.enabledRunners = next;
21403
+ return next;
21404
+ });
21405
+ }
21406
+ /** Save `ids` as the user's choice unless one is saved already, judged on the file as it is now. */
21407
+ setEnabledRunnersIfUnset(ids) {
21408
+ return this.save(() => {
21409
+ if (this.cache.enabledRunners !== void 0) return false;
21410
+ this.cache.enabledRunners = [...ids];
21411
+ return true;
21412
+ });
20837
21413
  }
20838
21414
  /**
20839
21415
  * Full or Guarded (ADR-0001), as saved here (ADR-0030). A value set in the
@@ -20851,9 +21427,9 @@ var SettingsService = class {
20851
21427
  return env !== void 0 && env.trim() !== "";
20852
21428
  }
20853
21429
  setAutonomousMode(enabled) {
20854
- this.getAll();
20855
- this.cache.autonomousMode = enabled;
20856
- this.persist();
21430
+ this.save(() => {
21431
+ this.cache.autonomousMode = enabled;
21432
+ });
20857
21433
  }
20858
21434
  getRunSettings() {
20859
21435
  return runSettingsOf(this.getAll());
@@ -20865,11 +21441,11 @@ var SettingsService = class {
20865
21441
  throw new RunSettingsError(RUN_SETTING_CHECKS[key].refusal);
20866
21442
  }
20867
21443
  }
20868
- this.getAll();
20869
- for (const key of RUN_SETTING_KEYS) {
20870
- if (changes[key] !== void 0) this.cache[key] = changes[key];
20871
- }
20872
- this.persist();
21444
+ this.save(() => {
21445
+ for (const key of RUN_SETTING_KEYS) {
21446
+ if (changes[key] !== void 0) this.cache[key] = changes[key];
21447
+ }
21448
+ });
20873
21449
  return this.getRunSettings();
20874
21450
  }
20875
21451
  load() {
@@ -20898,13 +21474,43 @@ var SettingsService = class {
20898
21474
  }
20899
21475
  return {};
20900
21476
  }
20901
- persist() {
21477
+ /**
21478
+ * Makes `change` to the file as it is now, and saves it. Another process can
21479
+ * save between that read and this write; the file is looked at again just
21480
+ * before ours replaces it, and when it moved the change is made afresh from
21481
+ * what that process saved, so neither save undoes the other. A file that
21482
+ * keeps moving is written over after {@link SAVE_TRIES} tries rather than
21483
+ * never. The look and the swap are one step under the file's lock
21484
+ * ({@link withFileLock}). A change that changes nothing writes nothing.
21485
+ */
21486
+ save(change) {
21487
+ for (let tries = 1; ; tries++) {
21488
+ const before = JSON.stringify(this.current());
21489
+ const read2 = this.cachedStamp;
21490
+ const result = change();
21491
+ if (JSON.stringify(this.cache) === before || this.persist(tries < SAVE_TRIES ? read2 : void 0)) return result;
21492
+ }
21493
+ }
21494
+ /** False, saving nothing, when the file is no longer the one stamped `expected`. */
21495
+ persist(expected) {
20902
21496
  const dir = path26.dirname(this.filePath);
20903
21497
  if (!fs10.existsSync(dir)) {
20904
21498
  fs10.mkdirSync(dir, { recursive: true });
20905
21499
  }
20906
- fs10.writeFileSync(this.filePath, JSON.stringify(this.cache, null, 2));
20907
- this.cachedMtimeMs = this.fileMtimeMs();
21500
+ const staging = `${this.filePath}.${process.pid}.tmp`;
21501
+ fs10.writeFileSync(staging, JSON.stringify(this.cache, null, 2));
21502
+ const stamp = this.fileStamp(staging);
21503
+ const swapped = withFileLock(this.filePath, () => {
21504
+ if (expected !== void 0 && this.fileStamp() !== expected) return false;
21505
+ fs10.renameSync(staging, this.filePath);
21506
+ return true;
21507
+ });
21508
+ if (!swapped) {
21509
+ fs10.rmSync(staging, { force: true });
21510
+ return false;
21511
+ }
21512
+ this.cachedStamp = stamp;
21513
+ return true;
20908
21514
  }
20909
21515
  };
20910
21516
 
@@ -21393,7 +21999,7 @@ function capConflictFiles(files, max = 5) {
21393
21999
  }
21394
22000
 
21395
22001
  // src/services/mergeResultNotice.ts
21396
- function blockNotice({ repo, reason, files }) {
22002
+ function blockNotice({ repo, reason, files, expected, actual, rebasing }) {
21397
22003
  switch (reason) {
21398
22004
  case "merge-in-progress":
21399
22005
  return `${repo} has a merge in progress`;
@@ -21403,10 +22009,22 @@ function blockNotice({ repo, reason, files }) {
21403
22009
  return `${repo} has uncommitted changes to ${files.join(", ")}`;
21404
22010
  case "partial-landing":
21405
22011
  return `${repo} holds part of a task whose landing could not be rolled back`;
22012
+ case "wrong-branch":
22013
+ return `${repo === SELF_REPO ? "your checkout" : repo} is ${actual ? `on ${actual}` : rebasing ? "in the middle of a rebase" : "on a detached HEAD"}; this run started on ${expected}`;
21406
22014
  case "git-error":
21407
22015
  return `git could not check ${repo}`;
21408
22016
  }
21409
22017
  }
22018
+ function branchFix(blocked) {
22019
+ if (blocked.length === 0 || blocked.some((b) => b.reason !== "wrong-branch")) return "";
22020
+ const rebasing = blocked.filter((b) => b.rebasing);
22021
+ if (rebasing.length > 0) {
22022
+ const where = rebasing.map((b) => b.repo === SELF_REPO ? "your checkout" : b.repo).join(", ");
22023
+ return ` Finish or abort the rebase in ${where}${rebasing.length < blocked.length ? " and check out the branch each other repository started on" : ""}, then press Merge all again.`;
22024
+ }
22025
+ const started = [...new Set(blocked.map((b) => b.expected))];
22026
+ return started.length === 1 && blocked.length === 1 ? ` Check out ${started[0]} and press Merge all again.` : " Check out the branch each repository started on and press Merge all again.";
22027
+ }
21410
22028
  function landedNotice(landed2) {
21411
22029
  if (landed2.length === 0) return "Nothing was merged.";
21412
22030
  return landed2.length === 1 ? `${landed2[0]} was merged already and stays merged.` : `${landed2.join(", ")} were merged already and stay merged.`;
@@ -21423,7 +22041,7 @@ function describeMergeResult(result, branch, group, repaired = []) {
21423
22041
  return { level: "info", message: repaired.length > 0 ? `${base} ${repairedNotice(repaired)}` : base };
21424
22042
  }
21425
22043
  case "blocked":
21426
- return { level: "warn", message: `Merged nothing, so every tree is as it was: ${result.blocked.map(blockNotice).join("; ")}.` };
22044
+ return { level: "warn", message: `Merged nothing, so every tree is as it was: ${result.blocked.map(blockNotice).join("; ")}.${branchFix(result.blocked)}` };
21427
22045
  case "conflict":
21428
22046
  return {
21429
22047
  level: "warn",
@@ -21533,7 +22151,7 @@ ${await file.patch()}`);
21533
22151
  }
21534
22152
 
21535
22153
  // src/services/GitWorktreeIsolation.ts
21536
- var import_crypto10 = require("crypto");
22154
+ var import_crypto11 = require("crypto");
21537
22155
  var fs13 = __toESM(require("fs"));
21538
22156
  var path29 = __toESM(require("path"));
21539
22157
 
@@ -21661,7 +22279,7 @@ var GitWorktreeIsolation = class {
21661
22279
  this.resolvePath = deps.resolvePath ?? (() => augmentedPath());
21662
22280
  this.gitInvoker = { exec: deps.execFileImpl ?? defaultExecFile2, resolvePath: this.resolvePath };
21663
22281
  this.platform = deps.platform ?? process.platform;
21664
- this.mintRunId = deps.mintRunId ?? (() => (0, import_crypto10.randomBytes)(4).toString("hex"));
22282
+ this.mintRunId = deps.mintRunId ?? (() => (0, import_crypto11.randomBytes)(4).toString("hex"));
21665
22283
  }
21666
22284
  deps;
21667
22285
  resolvePath;
@@ -21787,7 +22405,7 @@ var GitWorktreeIsolation = class {
21787
22405
  path: repoPath,
21788
22406
  root,
21789
22407
  baseRef,
21790
- ...branch ? { baseBranch: branch } : {},
22408
+ ...branch ? { baseBranch: branch } : { startedDetached: true },
21791
22409
  integrationBranch: integrationBranchFor(run.id)
21792
22410
  };
21793
22411
  if (!(await this.tryGit(root, ["branch", repo.integrationBranch, baseRef])).ok) return null;
@@ -22140,7 +22758,9 @@ var GitWorktreeIsolation = class {
22140
22758
  async mergeAll(run) {
22141
22759
  const partial = await this.settleLanding(run);
22142
22760
  if (partial.length > 0) return { outcome: "blocked", blocked: partial.map((repo) => ({ repo, reason: "partial-landing", files: [] })) };
22761
+ const misplaced = await this.offBaseBranch(run.repos);
22143
22762
  const repos = await this.reposWithWork(run);
22763
+ if (misplaced.length > 0) return { outcome: "blocked", blocked: misplaced };
22144
22764
  if (run.repos.length === 1 || !await this.canPreflight()) return this.mergeInTurn(repos);
22145
22765
  const blocked = [];
22146
22766
  for (const repo of repos) {
@@ -22158,6 +22778,27 @@ var GitWorktreeIsolation = class {
22158
22778
  }
22159
22779
  return withWork;
22160
22780
  }
22781
+ /**
22782
+ * A merge goes into whatever is checked out, which is only what the user
22783
+ * meant if it is the branch the run started on. A run that started on a
22784
+ * detached HEAD named no branch, so anything is as good as it was.
22785
+ */
22786
+ async offBaseBranch(repos) {
22787
+ const off = [];
22788
+ for (const repo of repos) {
22789
+ if (!repo.baseBranch) continue;
22790
+ const current = (await this.tryGit(repo.root, ["symbolic-ref", "--short", "-q", "HEAD"])).stdout.trim();
22791
+ if (current === repo.baseBranch) continue;
22792
+ off.push({
22793
+ repo: repo.path,
22794
+ reason: "wrong-branch",
22795
+ files: [],
22796
+ expected: repo.baseBranch,
22797
+ ...current ? { actual: current } : await this.rebaseInProgress(repo.root) ? { rebasing: true } : {}
22798
+ });
22799
+ }
22800
+ return off;
22801
+ }
22161
22802
  /** `git merge-tree --write-tree`, which merges without touching a tree, arrived in git 2.38. */
22162
22803
  async canPreflight() {
22163
22804
  const version = await this.tryGit(void 0, ["--version"]);
@@ -22645,6 +23286,14 @@ var GitWorktreeIsolation = class {
22645
23286
  if ((await this.tryGit(cwd, ["rev-parse", "-q", "--verify", "MERGE_HEAD"])).ok) return true;
22646
23287
  return (await this.tryGit(cwd, ["ls-files", "-u"])).stdout.trim() !== "";
22647
23288
  }
23289
+ /** A rebase leaves HEAD detached, so it is told from a checkout the user detached on purpose. */
23290
+ async rebaseInProgress(cwd) {
23291
+ for (const dir of ["rebase-merge", "rebase-apply"]) {
23292
+ const found = await this.tryGit(cwd, ["rev-parse", "--git-path", dir]);
23293
+ if (found.ok && fs13.existsSync(path29.resolve(cwd, found.stdout.trim()))) return true;
23294
+ }
23295
+ return false;
23296
+ }
22648
23297
  async abortMerge(cwd) {
22649
23298
  if (!(await this.tryGit(cwd, ["merge", "--abort"])).ok) await this.tryGit(cwd, ["reset", "--hard"]);
22650
23299
  }
@@ -22665,6 +23314,29 @@ function createWorktreeIsolation(deps) {
22665
23314
  return new GitWorktreeIsolation(deps);
22666
23315
  }
22667
23316
 
23317
+ // src/services/mergeWording.ts
23318
+ function isRepoGroup(handoff) {
23319
+ return handoff.repos.some((r) => r.path !== ".");
23320
+ }
23321
+ function reposWithWork(handoff) {
23322
+ const withWork = handoff.repos.filter((r) => r.landed.length > 0);
23323
+ return (withWork.length > 0 ? withWork : handoff.repos).map((r) => r.path);
23324
+ }
23325
+ function mergeTarget(handoff) {
23326
+ if (!isRepoGroup(handoff)) return handoff.repos[0]?.baseBranch ?? "the branch you have checked out";
23327
+ const targets = handoff.repos.filter((r) => reposWithWork(handoff).includes(r.path));
23328
+ const branches = new Set(targets.map((r) => r.baseBranch));
23329
+ const [only] = branches;
23330
+ if (branches.size === 1 && only) return `${only} in each of ${targets.map((r) => r.path).join(", ")}`;
23331
+ return `each of ${targets.map((r) => `${r.path} (${r.baseBranch ?? "as checked out"})`).join(", ")}`;
23332
+ }
23333
+ function checkoutRule(handoff) {
23334
+ const named3 = handoff.repos.filter((r) => r.baseBranch);
23335
+ if (named3.length === 0) return "";
23336
+ if (!isRepoGroup(handoff)) return ` Nothing is merged unless ${named3[0].baseBranch} is checked out.`;
23337
+ return " Nothing is merged unless each repository is on the branch it started on.";
23338
+ }
23339
+
22668
23340
  // src/interfaces/ILogger.ts
22669
23341
  var ConsoleLogger = class {
22670
23342
  warn(scope, message, err) {
@@ -22717,7 +23389,10 @@ async function settleReply(opts) {
22717
23389
  opts.onProgress({ type: "interrupted" });
22718
23390
  return { done: turn };
22719
23391
  }
22720
- if (attempt.failure !== void 0) return { done: { ...message(attempt.failure), failed: true } };
23392
+ if (attempt.failure !== void 0) {
23393
+ const { usageLimit } = attempt;
23394
+ return { done: { ...message(attempt.failure), failed: true, ...usageLimit !== void 0 && { usageLimit } } };
23395
+ }
22721
23396
  if (!attempt.text.trim()) return { done: message(emptyReplyReport(deniedStep(attempt))) };
22722
23397
  const reply2 = opts.classify ? classifyPlannerReply(attempt.text, opts.classify) : { kind: "prose" };
22723
23398
  switch (reply2.kind) {
@@ -23164,8 +23839,8 @@ function summarizeToolCall(tool2, argsJson, toolLabel) {
23164
23839
  }
23165
23840
  const path35 = args.filePath || args.file_path || args.path;
23166
23841
  if (path35) {
23167
- const basename4 = String(path35).split("/").pop() || String(path35);
23168
- return `${display} ${basename4}`;
23842
+ const basename5 = String(path35).split("/").pop() || String(path35);
23843
+ return `${display} ${basename5}`;
23169
23844
  }
23170
23845
  if (args.pattern) {
23171
23846
  return `${display} ${args.pattern}`;
@@ -23715,7 +24390,7 @@ var CONVERSATION_TOOL_DEFINITIONS = toGeminiToolDeclarations(true);
23715
24390
  function tokenCount(value) {
23716
24391
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
23717
24392
  }
23718
- function usageRecordFrom(usage) {
24393
+ function usageRecordFrom(usage, model) {
23719
24394
  if (!usage) return void 0;
23720
24395
  const inputTokens = tokenCount(usage.promptTokenCount);
23721
24396
  const answerTokens = tokenCount(usage.candidatesTokenCount);
@@ -23723,6 +24398,7 @@ function usageRecordFrom(usage) {
23723
24398
  const reportedOutput = [answerTokens, thoughtTokens].filter((t) => t !== void 0);
23724
24399
  const record = {
23725
24400
  source: "google",
24401
+ model,
23726
24402
  ...inputTokens !== void 0 ? { inputTokens } : {},
23727
24403
  ...reportedOutput.length > 0 ? { outputTokens: reportedOutput.reduce((a, b) => a + b, 0) } : {},
23728
24404
  ...tokenCount(usage.cachedContentTokenCount) !== void 0 ? { cachedInputTokens: tokenCount(usage.cachedContentTokenCount) } : {}
@@ -23731,15 +24407,31 @@ function usageRecordFrom(usage) {
23731
24407
  return anyReported ? record : void 0;
23732
24408
  }
23733
24409
  var GeminiResearchChat = class {
23734
- constructor(chat, hooks = {}) {
23735
- this.chat = chat;
24410
+ constructor(getModel, open, hooks = {}) {
24411
+ this.getModel = getModel;
24412
+ this.open = open;
23736
24413
  this.hooks = hooks;
24414
+ this.model = getModel();
24415
+ this.chat = open(this.model);
23737
24416
  }
23738
- chat;
24417
+ getModel;
24418
+ open;
23739
24419
  hooks;
24420
+ chat;
24421
+ /** The model `chat` calls, named on its usage records. */
24422
+ model;
24423
+ /** The SDK binds a chat to one model, so a switch reopens it on the new one over the history so far. */
24424
+ async current() {
24425
+ const model = this.getModel();
24426
+ if (model !== this.model) {
24427
+ this.chat = this.open(model, await this.chat.getHistory());
24428
+ this.model = model;
24429
+ }
24430
+ return this.chat;
24431
+ }
23740
24432
  async sendMessage(text3, signal) {
23741
24433
  if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
23742
- const result = await this.chat.sendMessageStream(text3);
24434
+ const result = await (await this.current()).sendMessageStream(text3);
23743
24435
  return this.consume(result.stream);
23744
24436
  }
23745
24437
  async sendToolResults(results, signal) {
@@ -23750,7 +24442,7 @@ var GeminiResearchChat = class {
23750
24442
  response: { output: r.output, truncated: r.truncated, totalChars: r.totalChars }
23751
24443
  }
23752
24444
  }));
23753
- const result = await this.chat.sendMessageStream(funcResponses);
24445
+ const result = await (await this.current()).sendMessageStream(funcResponses);
23754
24446
  return this.consume(result.stream);
23755
24447
  }
23756
24448
  /**
@@ -23790,7 +24482,7 @@ var GeminiResearchChat = class {
23790
24482
  }
23791
24483
  }
23792
24484
  }
23793
- const usageRecord3 = usageRecordFrom(usage);
24485
+ const usageRecord3 = usageRecordFrom(usage, this.model);
23794
24486
  if (usageRecord3) this.hooks.onUsage?.(usageRecord3);
23795
24487
  return {
23796
24488
  text: text3,
@@ -23806,6 +24498,8 @@ var GeminiService = class extends BaseAiService {
23806
24498
  genAI = null;
23807
24499
  genAIKey = "";
23808
24500
  model = null;
24501
+ /** The id `model` was made for; the SDK's own `model.model` is its `models/…` resource path. */
24502
+ modelId = "";
23809
24503
  constructor(config) {
23810
24504
  super(config);
23811
24505
  }
@@ -23819,13 +24513,18 @@ var GeminiService = class extends BaseAiService {
23819
24513
  }
23820
24514
  return true;
23821
24515
  }
23822
- getPlanningModel() {
24516
+ /** The planner model picked for this session, as the API names it. */
24517
+ plannerModel() {
24518
+ return normalizeGeminiModel(this.config.orchestratorModel || this.config.planningModel);
24519
+ }
24520
+ getPlanningModel(model = this.plannerModel()) {
23823
24521
  this.init();
23824
- if (!this.model || this.model.model !== this.config.planningModel) {
24522
+ if (!this.model || this.modelId !== model) {
23825
24523
  this.model = this.genAI.getGenerativeModel({
23826
- model: this.config.planningModel,
24524
+ model,
23827
24525
  generationConfig: { temperature: 0.3, topP: 0.95, maxOutputTokens: 16384 }
23828
24526
  });
24527
+ this.modelId = model;
23829
24528
  }
23830
24529
  return this.model;
23831
24530
  }
@@ -23840,7 +24539,6 @@ var GeminiService = class extends BaseAiService {
23840
24539
  }
23841
24540
  async startConversation(req) {
23842
24541
  this.ensureInit();
23843
- const genModel = this.getPlanningModel();
23844
24542
  const contextStr = await BaseAiService.collectResearchContext(req.fs, req.runners);
23845
24543
  const systemPrompt = buildConversationSystemPrompt(
23846
24544
  req.goal,
@@ -23869,13 +24567,14 @@ ${m.content}` });
23869
24567
  if ((req.priorHistory?.length ?? 0) > 0 && seededHistory[seededHistory.length - 1].role === "user") {
23870
24568
  seededHistory.push({ role: "model", parts: [{ text: "Understood." }] });
23871
24569
  }
23872
- const chat = genModel.startChat({
23873
- history: seededHistory,
23874
- tools: [{ functionDeclarations: req.plannerTools ? CONVERSATION_TOOL_DEFINITIONS : TOOL_DEFINITIONS }],
24570
+ const tools = [{ functionDeclarations: req.plannerTools ? CONVERSATION_TOOL_DEFINITIONS : TOOL_DEFINITIONS }];
24571
+ const open = (model, history = seededHistory) => this.getPlanningModel(model).startChat({
24572
+ history,
24573
+ tools,
23875
24574
  generationConfig: { temperature: 0.3, topP: 0.95, maxOutputTokens: 16384 }
23876
24575
  });
23877
24576
  let currentProgress = req.onProgress;
23878
- const researchChat = new GeminiResearchChat(chat, {
24577
+ const researchChat = new GeminiResearchChat(() => this.plannerModel(), open, {
23879
24578
  onReasoning: (text3, segmentId) => currentProgress({ type: "thinking", text: text3, segmentId }),
23880
24579
  onContent: (text3, segmentId) => currentProgress({ type: "text_delta", text: text3, segmentId }),
23881
24580
  onUsage: (record) => currentProgress({ type: "usage", record })
@@ -23948,18 +24647,17 @@ ${repairHint}` }] : parts;
23948
24647
  async researchAndPlan(userDescription, runners, modelsByRunner, fs18, onProgress, fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
23949
24648
  this.ensureInit();
23950
24649
  const { autonomousDefault } = modes;
23951
- const genModel = this.getPlanningModel();
23952
24650
  const contextStr = await BaseAiService.collectResearchContext(fs18, runners);
23953
24651
  const systemPrompt = buildResearchPrompt(userDescription, contextStr, modelsByRunner, runners, runnerModes, modes);
23954
24652
  const firstMessage = `${buildResearchToolsPrompt()}
23955
24653
 
23956
24654
  Explore the workspace to understand the codebase, then generate the plan.`;
23957
- const chat = genModel.startChat({
23958
- history: [{ role: "user", parts: [{ text: systemPrompt }] }],
24655
+ const open = (model, history = [{ role: "user", parts: [{ text: systemPrompt }] }]) => this.getPlanningModel(model).startChat({
24656
+ history,
23959
24657
  tools: [{ functionDeclarations: TOOL_DEFINITIONS }],
23960
24658
  generationConfig: { temperature: 0.3, topP: 0.95, maxOutputTokens: 16384 }
23961
24659
  });
23962
- const researchChat = new GeminiResearchChat(chat, {
24660
+ const researchChat = new GeminiResearchChat(() => this.plannerModel(), open, {
23963
24661
  onUsage: (record) => onProgress({ type: "usage", record })
23964
24662
  });
23965
24663
  const result = await this.runResearchLoop(researchChat, firstMessage, fs18, onProgress, runners, void 0, fetcher, userDescription, runnerModes, autonomousDefault, signal);
@@ -23995,7 +24693,7 @@ var import_openai = __toESM(require("openai"));
23995
24693
  var import_uuid7 = require("uuid");
23996
24694
  var KEYLESS_PLACEHOLDER = "not-needed";
23997
24695
  var OpenAiResearchChat = class {
23998
- constructor(messages, getClient, getModel, tools, onReasoning, onContent, getSource = () => "openai", onUsage, contextWindow) {
24696
+ constructor(messages, getClient, getModel, tools, onReasoning, onContent, getSource = () => "openai", onUsage) {
23999
24697
  this.messages = messages;
24000
24698
  this.getClient = getClient;
24001
24699
  this.getModel = getModel;
@@ -24004,7 +24702,6 @@ var OpenAiResearchChat = class {
24004
24702
  this.onContent = onContent;
24005
24703
  this.getSource = getSource;
24006
24704
  this.onUsage = onUsage;
24007
- this.contextWindow = contextWindow;
24008
24705
  }
24009
24706
  messages;
24010
24707
  getClient;
@@ -24014,7 +24711,6 @@ var OpenAiResearchChat = class {
24014
24711
  onContent;
24015
24712
  getSource;
24016
24713
  onUsage;
24017
- contextWindow;
24018
24714
  async sendMessage(text3, signal) {
24019
24715
  this.answerAbandonedCalls();
24020
24716
  this.messages.push({ role: "user", content: text3 });
@@ -24129,7 +24825,6 @@ var OpenAiResearchChat = class {
24129
24825
  record.reportedCost = { amount: reported.cost, currency: "USD" };
24130
24826
  hasMeasure = true;
24131
24827
  }
24132
- if (this.contextWindow && this.contextWindow > 0) record.contextWindow = this.contextWindow;
24133
24828
  return hasMeasure ? record : void 0;
24134
24829
  }
24135
24830
  };
@@ -24256,8 +24951,7 @@ ${buildResearchToolsPrompt()}` }
24256
24951
  (delta, segmentId) => currentProgress({ type: "thinking", text: delta, segmentId }),
24257
24952
  (delta, segmentId) => currentProgress({ type: "text_delta", text: delta, segmentId }),
24258
24953
  () => this.config.aiProvider,
24259
- (record) => currentProgress({ type: "usage", record }),
24260
- req.contextWindow
24954
+ (record) => currentProgress({ type: "usage", record })
24261
24955
  );
24262
24956
  const ctx = {
24263
24957
  chat,
@@ -24819,6 +25513,10 @@ var VerdictEngine = class {
24819
25513
  this.generations.clear();
24820
25514
  }
24821
25515
  };
25516
+ var EXIT_CHECK = "exit_code";
25517
+ function endedByExit(verdict) {
25518
+ return verdict.checks.some((check2) => check2.name === EXIT_CHECK);
25519
+ }
24822
25520
  function exitVerdict(exitCode) {
24823
25521
  const normalized2 = exitCode == null ? 0 : exitCode;
24824
25522
  const exitOk = normalized2 === 0;
@@ -24827,7 +25525,7 @@ function exitVerdict(exitCode) {
24827
25525
  reason: exitOk ? "Failed verification: the runner exited cleanly but never called task_complete." : `Failed verification: the runner exited with code ${normalized2} without calling task_complete.`,
24828
25526
  checks: [
24829
25527
  { name: "task_complete", passed: false, skipped: false, detail: "the runner exited before calling task_complete" },
24830
- { name: "exit_code", passed: exitOk, skipped: false, detail: exitOk ? "the runner exited cleanly (code 0)" : `the runner exited with code ${normalized2}` }
25528
+ { name: EXIT_CHECK, passed: exitOk, skipped: false, detail: exitOk ? "the runner exited cleanly (code 0)" : `the runner exited with code ${normalized2}` }
24831
25529
  ],
24832
25530
  decidedAt: (/* @__PURE__ */ new Date()).toISOString()
24833
25531
  };
@@ -25026,6 +25724,7 @@ var PlanStore = class {
25026
25724
  this.rebuild();
25027
25725
  for (const task of this._allTasks) {
25028
25726
  if (task.status === "failed") task.status = "pending";
25727
+ if (task.runnerSessionId && !task.runnerSessionOwner) task.runnerSessionOwner = sessionOwner(task) ?? task.assignedRunner;
25029
25728
  }
25030
25729
  this.validateAssignedRunners();
25031
25730
  }
@@ -25155,9 +25854,17 @@ var PlanStore = class {
25155
25854
  const task = this._taskMap.get(id);
25156
25855
  if (task) task.outputSummary = summary;
25157
25856
  }
25158
- setTaskRunnerSessionId(id, runnerSessionId) {
25857
+ /** The owner is saved with the session it belongs to, and dropped with it. */
25858
+ setTaskRunnerSessionId(id, runnerSessionId, owner) {
25859
+ const task = this._taskMap.get(id);
25860
+ if (!task) return;
25861
+ task.runnerSessionId = runnerSessionId;
25862
+ if (runnerSessionId && owner) task.runnerSessionOwner = owner;
25863
+ else delete task.runnerSessionOwner;
25864
+ }
25865
+ setCarriedFrom(id, runner) {
25159
25866
  const task = this._taskMap.get(id);
25160
- if (task) task.runnerSessionId = runnerSessionId;
25867
+ if (task) task.carriedFrom = runner;
25161
25868
  }
25162
25869
  clearCarriedFrom(id) {
25163
25870
  const task = this._taskMap.get(id);
@@ -25171,9 +25878,9 @@ var PlanStore = class {
25171
25878
  const task = this._taskMap.get(id);
25172
25879
  if (task?.attemptUsage) task.attemptUsage = task.attemptUsage.map((usage) => usage.attempt === attempt ? addAttemptUsage(usage, record, now) : usage);
25173
25880
  }
25174
- endAttemptUsage(id, attempt, now) {
25881
+ endAttemptUsage(id, attempt, now, runnerSessionCost, sessionId) {
25175
25882
  const task = this._taskMap.get(id);
25176
- if (task?.attemptUsage) task.attemptUsage = task.attemptUsage.map((usage) => usage.attempt === attempt ? endAttemptUsage(usage, now) : usage);
25883
+ if (task?.attemptUsage) task.attemptUsage = task.attemptUsage.map((usage) => usage.attempt === attempt ? endAttemptUsage(usage, now, runnerSessionCost, sessionId) : usage);
25177
25884
  }
25178
25885
  setTaskAttemptSkills(id, skills) {
25179
25886
  const task = this._taskMap.get(id);
@@ -26031,6 +26738,8 @@ function conflictResolverTask(task, run, plan) {
26031
26738
  }
26032
26739
 
26033
26740
  // src/services/UsageLimitWaits.ts
26741
+ var RESUME_MARGIN_MS = 6e4;
26742
+ var RELAPSE_MS = 5 * 6e4;
26034
26743
  var UsageLimitWaits = class {
26035
26744
  constructor(onDue) {
26036
26745
  this.onDue = onDue;
@@ -26153,6 +26862,9 @@ function selectReadyTasks(input) {
26153
26862
  gated
26154
26863
  };
26155
26864
  }
26865
+ function isBlocked(task, store) {
26866
+ return blockedIn(task, blockingFailures(store.allTasks));
26867
+ }
26156
26868
  var blockedIn = (task, blocked) => task.status === "blocked" || blocked.has(task.id);
26157
26869
  function dependencyMet(depId, store, runs) {
26158
26870
  return waitingBehind([depId], store, runs).length === 0;
@@ -26282,6 +26994,12 @@ var TaskOrchestrator = class _TaskOrchestrator {
26282
26994
  * task whose spawn always throws.
26283
26995
  */
26284
26996
  onHold = /* @__PURE__ */ new Set();
26997
+ /**
26998
+ * Force starts queued behind dependencies the scheduler will not start them
26999
+ * after: outside a run it drives, or past a failed dependency, which it
27000
+ * never forces. Each starts once those still under way have finished.
27001
+ */
27002
+ queuedStarts = /* @__PURE__ */ new Set();
26285
27003
  /**
26286
27004
  * Tasks Mark complete is landing after it ended their attempt: in progress
26287
27005
  * with no attempt for as long as that takes, and not orphans for it.
@@ -26303,6 +27021,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
26303
27021
  observers = [];
26304
27022
  skillsAt;
26305
27023
  previousAttemptFromLog;
27024
+ lastLoggedAttempt;
26306
27025
  runSettings;
26307
27026
  isApproved;
26308
27027
  constructor(deps) {
@@ -26318,6 +27037,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
26318
27037
  this.workspaceEnv = deps.workspaceEnv;
26319
27038
  this.skillsAt = deps.skillsAt;
26320
27039
  this.previousAttemptFromLog = deps.previousAttemptFromLog;
27040
+ this.lastLoggedAttempt = deps.lastLoggedAttempt;
26321
27041
  this.runSettings = deps.runSettings;
26322
27042
  this.isApproved = deps.isApproved;
26323
27043
  this.store.onMutate = () => this.emit("onTaskChanged");
@@ -26382,6 +27102,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
26382
27102
  workspaceEnv: options.workspaceEnv ?? ((cwd) => resolveWorkspaceEnv(cwd)),
26383
27103
  skillsAt: options.skillsAt ?? ((roots) => new SkillsService(roots)),
26384
27104
  previousAttemptFromLog: options.previousAttemptFromLog ?? (() => null),
27105
+ lastLoggedAttempt: options.lastLoggedAttempt ?? (() => 0),
26385
27106
  runSettings: options.runSettings ?? (() => void 0),
26386
27107
  isApproved: options.isApproved ?? (() => true)
26387
27108
  });
@@ -26638,10 +27359,10 @@ var TaskOrchestrator = class _TaskOrchestrator {
26638
27359
  */
26639
27360
  holdStarts() {
26640
27361
  this.startHolds++;
26641
- let held = true;
27362
+ let held2 = true;
26642
27363
  return () => {
26643
- if (!held) return;
26644
- held = false;
27364
+ if (!held2) return;
27365
+ held2 = false;
26645
27366
  this.startHolds--;
26646
27367
  };
26647
27368
  }
@@ -26652,24 +27373,25 @@ var TaskOrchestrator = class _TaskOrchestrator {
26652
27373
  */
26653
27374
  admitStart() {
26654
27375
  const generation2 = this.stopGeneration;
26655
- let held = true;
27376
+ let held2 = true;
26656
27377
  this.admissions++;
26657
27378
  return {
26658
27379
  current: () => generation2 === this.stopGeneration,
26659
27380
  release: () => {
26660
- if (!held) return;
26661
- held = false;
27381
+ if (!held2) return;
27382
+ held2 = false;
26662
27383
  this.admissions--;
26663
27384
  }
26664
27385
  };
26665
27386
  }
26666
27387
  /**
26667
27388
  * Something can still start that nobody has to ask for again: a start
26668
- * admitted and not yet claimed, or a task that resumes on its own at its
26669
- * usage limit's reset. A Stop is owed to it, whether or not anything runs.
27389
+ * admitted and not yet claimed, a force start queued behind its
27390
+ * dependencies, or a task that resumes on its own at its usage limit's
27391
+ * reset. A Stop is owed to it, whether or not anything runs.
26670
27392
  */
26671
27393
  get startsPending() {
26672
- return this.admissions > 0 || this.store.allTasks.some(waitsForReset);
27394
+ return this.admissions > 0 || this.queuedStarts.size > 0 || this.store.allTasks.some(waitsForReset);
26673
27395
  }
26674
27396
  /** Run `work`, which awaits before it claims, under an admission it holds throughout; it is handed the generation it was admitted under. */
26675
27397
  async whileAdmitted(work) {
@@ -26768,6 +27490,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
26768
27490
  this.retryCounts.clear();
26769
27491
  this.spawnCounts.clear();
26770
27492
  this.onHold.clear();
27493
+ this.queuedStarts.clear();
26771
27494
  this.recoverOrphans();
26772
27495
  this.rearmLimitWaits();
26773
27496
  }
@@ -26855,6 +27578,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
26855
27578
  }
26856
27579
  this.runs.interrupt();
26857
27580
  this.onHold.clear();
27581
+ if (reason === "user") this.queuedStarts.clear();
26858
27582
  for (const t of this.store.allTasks.filter(waitsForReset)) this.cancelScheduledResume(t.id);
26859
27583
  this.sayStopped(stopped);
26860
27584
  this.recoverOrphans();
@@ -26954,6 +27678,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
26954
27678
  const attempt = this.attempts.get(taskId);
26955
27679
  if (!task || !attempt) return;
26956
27680
  attempt.decided = true;
27681
+ const stoppedMidTurn = endedByExit(verdict) ? saidInFlight(attempt) : null;
26957
27682
  console.error(`[TaskOrchestrator] Task ${taskRefOf(this.store.planTasks, task.id) ?? `#${task.order}`} "${task.title}" verdict=${verdict.outcome}`);
26958
27683
  console.error(`[TaskOrchestrator] Runner: ${task.assignedRunner}, Model: ${task.assignedModel?.modelId ?? "default"}`);
26959
27684
  console.error(`[TaskOrchestrator] Prompt preview: ${(task.prompt ?? "").slice(0, 200)}`);
@@ -26975,8 +27700,8 @@ ${summary || "(empty: no output captured)"}`);
26975
27700
  this.tell("warn", `Ops task "${task.title}" finished, but changed tracked files in your checkout: ${capConflictFiles(changedFiles)}. An ops task must not change repository files, so nothing was committed: look at the changes, then mark it complete or retry it.`);
26976
27701
  } else if (landing) {
26977
27702
  await this.applyLanding(task, landing, () => this.notifications.info(`Task "${task.title}" completed.`));
26978
- } else if (classifyRunnerStop(attempt.session?.getOutput() ?? "") === "usage-limit") {
26979
- this.pauseOnLimit(task, attempt, attempt.session?.getOutput() ?? "");
27703
+ } else if (stoppedMidTurn !== null && classifyRunnerStop(stoppedMidTurn) === "usage-limit") {
27704
+ this.pauseOnLimit(task, attempt, stoppedMidTurn);
26980
27705
  if (attempt.worktree) await this.runs.release(taskId, { keep: true });
26981
27706
  } else {
26982
27707
  this.store.markFailed(taskId);
@@ -26991,16 +27716,17 @@ ${summary || "(empty: no output captured)"}`);
26991
27716
  const status = this.store.get(taskId)?.status;
26992
27717
  if (status === "completed" || status === "failed" || status === "awaiting_user") this.emit("onTaskSettled", { taskId });
26993
27718
  this.emit("onTaskChanged");
26994
- if (!this.running) {
26995
- if (this.attempts.size === 0) {
26996
- if (this.store.isFinished()) this.planStatus = "completed";
26997
- this.emit("onTick");
26998
- if (this.settled) await this.closeRun();
26999
- this.emit("onExecutionComplete");
27000
- }
27719
+ if (this.running) {
27720
+ await this.tick();
27001
27721
  return;
27002
27722
  }
27003
- await this.tick();
27723
+ await this.startQueued();
27724
+ if (this.attempts.size === 0) {
27725
+ if (this.store.isFinished()) this.planStatus = "completed";
27726
+ this.emit("onTick");
27727
+ if (this.settled) await this.closeRun();
27728
+ this.emit("onExecutionComplete");
27729
+ }
27004
27730
  }
27005
27731
  /**
27006
27732
  * A repair's verdict never replaces the verdict the task's own work earned,
@@ -27268,11 +27994,17 @@ ${summary || "(empty: no output captured)"}`);
27268
27994
  const resumes = this.awaitsResume;
27269
27995
  this.retryCounts.set(taskId, (this.retryCounts.get(taskId) ?? 0) + 1);
27270
27996
  const ended = this.endAttempt(taskId, "retry");
27997
+ const movedFrom = this.movedFrom(task);
27271
27998
  this.store.retry(taskId);
27999
+ if (movedFrom) this.store.setCarriedFrom(taskId, movedFrom);
27272
28000
  this.store.unblockDependents(taskId);
27273
28001
  this.onHold.delete(taskId);
27274
28002
  this.emit("onTaskChanged");
27275
- await this.runs.release(taskId, { keep: false }, ended?.integration);
28003
+ if (movedFrom) {
28004
+ await ended?.integration;
28005
+ } else {
28006
+ await this.runs.release(taskId, { keep: false }, ended?.integration);
28007
+ }
27276
28008
  if (admittedAt !== this.stopGeneration) return;
27277
28009
  if (resumes) await this.start(origin);
27278
28010
  else await this.tick();
@@ -27323,6 +28055,16 @@ ${summary || "(empty: no output captured)"}`);
27323
28055
  this.emit("onTaskChanged");
27324
28056
  return true;
27325
28057
  }
28058
+ /**
28059
+ * The runner whose attempt left the task's work, when the task has been
28060
+ * moved off it since: a session cannot cross runners, so whatever starts
28061
+ * the task next is a carry-over of that work, the rule a paused task's move
28062
+ * follows (ADR-0018, U1).
28063
+ */
28064
+ movedFrom(task) {
28065
+ const owner = sessionOwner(task);
28066
+ return owner && owner !== this.store.resolveTaskRunner(task) ? owner : void 0;
28067
+ }
27326
28068
  /**
27327
28069
  * Run before writing assignments so cancellation sees the old runner's
27328
28070
  * shared wait. A session cannot cross runners, so the next attempt is a
@@ -27380,7 +28122,7 @@ ${summary || "(empty: no output captured)"}`);
27380
28122
  const summary = this.output.finalText(taskId);
27381
28123
  this.endAttempt(taskId, "usage-limit");
27382
28124
  this.pauseOnLimit(task, attempt, attempt.turnError ?? "");
27383
- this.store.setTaskOutputSummary(taskId, summarizeOutput(attempt.turnError ?? `${attempt.runner} hit its usage limit`, summary));
28125
+ this.store.setTaskOutputSummary(taskId, summarizeOutput(attempt.turnError?.message ?? `${attempt.runner} hit its usage limit`, summary));
27384
28126
  if (attempt.worktree) await this.runs.release(taskId, { keep: true });
27385
28127
  await this.afterVerdict(taskId);
27386
28128
  }
@@ -27479,10 +28221,14 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
27479
28221
  return false;
27480
28222
  }
27481
28223
  /**
27482
- * Manually start a single AI task right now, bypassing dependency/readiness
27483
- * gating (the "force start" affordance on a task card) but never the claim:
27484
- * it still needs the user's approval, a free slot, and no Stop since it was
27485
- * asked for, and throws {@link TaskControlError} saying which it lacks.
28224
+ * Manually start a single AI task right now (the "force start" affordance on
28225
+ * a task card), past a failed dependency (ADR-0034) or a merge gate
28226
+ * (ADR-0020), but never past the claim: it still needs the user's approval,
28227
+ * a free slot, and no Stop since it was asked for, and throws
28228
+ * {@link TaskControlError} saying which it lacks. A dependency still under
28229
+ * way or yet to run is not forced past: the task is released to the
28230
+ * scheduler instead, which starts it once they finish, and the line saying
28231
+ * so is the answer.
27486
28232
  * Reuses the scheduler's own claim so a force-started task gets the same
27487
28233
  * augmented prompt, session tracking, and exit handling — callers must not
27488
28234
  * re-spawn the runner themselves. No-op if the task is unknown, not an AI
@@ -27493,8 +28239,12 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
27493
28239
  if (!task || task.type !== "ai") return;
27494
28240
  if (this.attempts.has(taskId)) return;
27495
28241
  this.refuseOpsDuringMerge(task);
28242
+ const queued2 = this.queuedBehindDependencies(task);
28243
+ if (queued2) return queued2;
27496
28244
  await this.whileAdmitted(async (admittedAt) => {
27497
- if (!await this.openForAdmitted(taskId, origin, () => this.forceStartTask(taskId, origin), admittedAt)) return;
28245
+ if (!await this.openForAdmitted(taskId, origin, async () => {
28246
+ await this.forceStartTask(taskId, origin);
28247
+ }, admittedAt)) return;
27498
28248
  if (this.attempts.has(taskId)) return;
27499
28249
  this.refuseOpsDuringMerge(task);
27500
28250
  this.refuseClaim(task, admittedAt);
@@ -27503,6 +28253,68 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
27503
28253
  await this.claimed(task, admittedAt);
27504
28254
  });
27505
28255
  }
28256
+ /**
28257
+ * A force start of a task whose dependencies are still under way or yet to
28258
+ * run: started now, it would build on work that is not there, so it waits
28259
+ * for them in the run, off any hold, and says so. Dependencies are never
28260
+ * bypassed silently. A blocked task is forced past its failure as asked.
28261
+ */
28262
+ queuedBehindDependencies(task) {
28263
+ const waiting = this.underWayBefore(task);
28264
+ if (waiting.length === 0) return null;
28265
+ this.onHold.delete(task.id);
28266
+ if (!this.running || isBlocked(task, this.store)) this.queuedStarts.add(task.id);
28267
+ const names = waiting.map((id) => `task "${this.store.get(id)?.title ?? id}"`);
28268
+ const listed3 = names.length === 1 ? names[0] : `${names.slice(0, -1).join(", ")} and ${names.at(-1)}`;
28269
+ const idle = this.running || waiting.some((id) => this.attempts.has(id)) ? "" : " Nothing is running now: run the plan to start them.";
28270
+ const queued2 = `Task "${task.title}" is approved and starts once ${listed3} ${names.length === 1 ? "finishes" : "finish"}.${idle}`;
28271
+ this.tell("info", queued2);
28272
+ this.emit("onTaskChanged");
28273
+ return queued2;
28274
+ }
28275
+ /**
28276
+ * A task's unmet dependencies, less the failed or blocked ones a force start
28277
+ * goes past. A blocked one is gone past for its failure only: the work under
28278
+ * way or yet to run that it waits on is still waited for, through it.
28279
+ */
28280
+ underWayBefore(task) {
28281
+ const blocked = blockingFailures(this.store.allTasks);
28282
+ const waiting = /* @__PURE__ */ new Set();
28283
+ const seen = /* @__PURE__ */ new Set();
28284
+ const walk = (of) => {
28285
+ for (const id of unmetDependencies(of, this.store, this.runs)) {
28286
+ if (seen.has(id)) continue;
28287
+ seen.add(id);
28288
+ const dep = this.store.get(id);
28289
+ if (dep?.status === "failed") continue;
28290
+ if (dep && (dep.status === "blocked" || blocked.has(id))) walk(dep);
28291
+ else waiting.add(id);
28292
+ }
28293
+ };
28294
+ walk(task);
28295
+ return [...waiting];
28296
+ }
28297
+ /**
28298
+ * Starts the queued force starts whose dependencies have now finished. One
28299
+ * the scheduler now drives the run for, and nothing blocks, is left to it.
28300
+ */
28301
+ async startQueued() {
28302
+ for (const id of [...this.queuedStarts]) {
28303
+ const task = this.store.get(id);
28304
+ if (!task || this.attempts.has(id) || task.status === "completed" || task.status === "skipped") {
28305
+ this.queuedStarts.delete(id);
28306
+ continue;
28307
+ }
28308
+ if (this.underWayBefore(task).length > 0) continue;
28309
+ this.queuedStarts.delete(id);
28310
+ if (this.running && !isBlocked(task, this.store)) continue;
28311
+ try {
28312
+ await this.forceStartTask(id);
28313
+ } catch (err) {
28314
+ this.tell("warn", `Task "${task.title}" did not start once its dependencies finished: ${err instanceof Error ? err.message : String(err)}`);
28315
+ }
28316
+ }
28317
+ }
27506
28318
  refuseOpsDuringMerge(task) {
27507
28319
  if (this.merging && mergeExcludes(this.nextAttemptKind(task.id))) {
27508
28320
  throw new TaskControlError(`A Merge all is under way: ops task "${task.title}" can start once it has finished.`);
@@ -27567,6 +28379,7 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
27567
28379
  async tick() {
27568
28380
  const admittedAt = this.stopGeneration;
27569
28381
  this.recoverOrphans();
28382
+ await this.startQueued();
27570
28383
  if (!this.running) {
27571
28384
  if (this.attempts.size === 0 && this.runs.isOpen && this.settled) await this.closeRun();
27572
28385
  return;
@@ -27723,7 +28536,7 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
27723
28536
  if (refused3) return { refused: refused3 };
27724
28537
  const attempt = {
27725
28538
  taskId: task.id,
27726
- attempt: Math.max(this.spawnCounts.get(task.id) ?? 0, ...task.attemptUsage?.map((u) => u.attempt) ?? []) + 1,
28539
+ attempt: Math.max(this.spawnCounts.get(task.id) ?? 0, this.lastLoggedAttempt(task.id), ...task.attemptUsage?.map((u) => u.attempt) ?? []) + 1,
27727
28540
  phase: "starting",
27728
28541
  session: null,
27729
28542
  runner: this.store.resolveTaskRunner(task),
@@ -27737,6 +28550,7 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
27737
28550
  skills: [],
27738
28551
  autoResumed: opts.autoResumed === true,
27739
28552
  turnError: null,
28553
+ turnOutputFrom: 0,
27740
28554
  reminded: false
27741
28555
  };
27742
28556
  this.spawnCounts.set(task.id, attempt.attempt);
@@ -27810,6 +28624,8 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
27810
28624
  title: task.title,
27811
28625
  env,
27812
28626
  resumeSessionId: kind.kind === "continuation" ? kind.resumeSessionId : void 0,
28627
+ resumeCost: kind.kind === "continuation" ? countedSessionCost(task.attemptUsage, kind.resumeSessionId) : void 0,
28628
+ resumeWindow: kind.kind === "continuation" ? resumedSessionWindow(task.attemptUsage, attempt.runner, attempt.attempt, kind.resumeSessionId) : void 0,
27813
28629
  attempt: attempt.attempt,
27814
28630
  ...attempt.skills.length > 0 ? { skills: attempt.skills } : {},
27815
28631
  onNotice: (message) => this.tell("warn", `Task "${task.title}": ${message}`)
@@ -27834,8 +28650,21 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
27834
28650
  this.output.attach(task.id, session);
27835
28651
  this.verifier.watch(task, session);
27836
28652
  session.onEvent((event) => {
27837
- if (event.type === "turn_start") attempt.turnError = null;
27838
- else if (event.type === "error") attempt.turnError = event.message;
28653
+ if (event.type === "turn_start") {
28654
+ attempt.turnError = null;
28655
+ attempt.turnOutputFrom = session.getOutput().length;
28656
+ } else if (event.type === "error") attempt.turnError = event;
28657
+ else if (event.type === "model_switched" || event.type === "model_drifted") {
28658
+ this.emit("onModelSwitched", {
28659
+ taskId: task.id,
28660
+ sessionId: session.id,
28661
+ from: event.from,
28662
+ model: event.to,
28663
+ ...event.type === "model_switched" && event.effort ? { effort: event.effort } : {},
28664
+ asked: event.type === "model_switched",
28665
+ attemptEnded: this.attempts.get(task.id) !== attempt
28666
+ });
28667
+ }
27839
28668
  });
27840
28669
  session.onTurnEnd((reason) => this.onTurnEnd(task.id, attempt, session, reason));
27841
28670
  return true;
@@ -27845,6 +28674,16 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
27845
28674
  return false;
27846
28675
  }
27847
28676
  this.endAttempt(task.id, "spawn-failed");
28677
+ const refusal = err instanceof Error ? err.message : String(err);
28678
+ if (attempt.kind.kind !== "repair" && classifyRunnerStop(refusal) === "usage-limit") {
28679
+ this.pauseOnLimit(task, attempt, refusal);
28680
+ this.store.setTaskOutputSummary(task.id, summarizeOutput(refusal, ""));
28681
+ await this.runs.release(task.id, { keep: true });
28682
+ this.emit("onTaskSettled", { taskId: task.id });
28683
+ this.emit("onTaskChanged");
28684
+ await this.tick();
28685
+ return false;
28686
+ }
27848
28687
  if (attempt.kind.kind === "continuation") {
27849
28688
  const refused3 = err instanceof RunnerStartupExit;
27850
28689
  const reason = refused3 ? unresumedMessage(task, `${attempt.runner} could not find its saved session`) : unresumedMessage(task, `could not start: ${err instanceof Error ? err.message : String(err)}`);
@@ -27964,12 +28803,13 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
27964
28803
  endAttempt(taskId, reason) {
27965
28804
  const attempt = this.attempts.get(taskId);
27966
28805
  this.attempts.delete(taskId);
28806
+ const session = attempt?.session ?? null;
28807
+ const nativeSessionId = session?.nativeSessionId() ?? null;
27967
28808
  if (attempt) {
27968
- this.store.endAttemptUsage(taskId, attempt.attempt, (/* @__PURE__ */ new Date()).toISOString());
28809
+ this.store.endAttemptUsage(taskId, attempt.attempt, (/* @__PURE__ */ new Date()).toISOString(), session?.sessionCost?.() ?? void 0, nativeSessionId ?? void 0);
27969
28810
  this.output.detach(taskId);
27970
28811
  }
27971
- const session = attempt?.session ?? null;
27972
- if (session) this.saveNativeSession(taskId, session.nativeSessionId());
28812
+ if (attempt && session) this.saveNativeSession(taskId, nativeSessionId, attempt.runner);
27973
28813
  if (stopsRunner(reason)) {
27974
28814
  const task = this.store.get(taskId);
27975
28815
  if (task) this.verifier.clear(task);
@@ -27978,8 +28818,8 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
27978
28818
  return attempt;
27979
28819
  }
27980
28820
  /** Kept on the task once the attempt ends, so a continue can resume it after a reload (ADR-0018, K1). */
27981
- saveNativeSession(taskId, nativeSessionId) {
27982
- if (nativeSessionId) this.store.setTaskRunnerSessionId(taskId, nativeSessionId);
28821
+ saveNativeSession(taskId, nativeSessionId, runner) {
28822
+ if (nativeSessionId) this.store.setTaskRunnerSessionId(taskId, nativeSessionId, runner);
27983
28823
  }
27984
28824
  endAllAttempts(reason) {
27985
28825
  const ended = [...this.attempts.values()];
@@ -27989,13 +28829,17 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
27989
28829
  return ended;
27990
28830
  }
27991
28831
  };
27992
- var RESUME_MARGIN_MS = 6e4;
27993
- var RELAPSE_MS = 5 * 6e4;
27994
28832
  var LIMIT_RESET_MESSAGE = "Your usage limit has reset. Carry on with the task from where you stopped.";
27995
28833
  var OPS_RETRY_TAIL_LINES = 60;
27996
28834
  function unresumedMessage(task, why) {
27997
28835
  return `Could not continue task "${task.title}": ${why}. Retry starts it afresh.`;
27998
28836
  }
28837
+ function saidInFlight(attempt) {
28838
+ const session = attempt.session;
28839
+ if (!session || session.turnState() !== "working") return null;
28840
+ if (attempt.turnError?.usageLimit !== void 0) return attempt.turnError;
28841
+ return session.getOutput().slice(attempt.turnOutputFrom);
28842
+ }
27999
28843
 
28000
28844
  // src/services/Planner.ts
28001
28845
  function plannerModesWith(req) {
@@ -28098,15 +28942,15 @@ var ModelResolver = class {
28098
28942
  return toOrchestratorOptions({}, ORCHESTRATOR_SHORTCUTS);
28099
28943
  }
28100
28944
  async modelsForRunners(runners) {
28101
- const out = {};
28102
- for (const runner of runners) {
28945
+ const unique = [...new Set(runners)];
28946
+ const discovered = await Promise.all(unique.map(async (runner) => {
28103
28947
  try {
28104
- out[runner] = await this.discovery.discover(runner);
28948
+ return [runner, await this.discovery.discover(runner)];
28105
28949
  } catch {
28106
- out[runner] = [];
28950
+ return [runner, []];
28107
28951
  }
28108
- }
28109
- return out;
28952
+ }));
28953
+ return Object.fromEntries(discovered);
28110
28954
  }
28111
28955
  async pickerOptions() {
28112
28956
  if (this.cachedPickerOptions) return this.cachedPickerOptions;
@@ -28153,21 +28997,18 @@ var ModelResolver = class {
28153
28997
  return this.cachedPickerOptions ?? [];
28154
28998
  }
28155
28999
  /**
28156
- * The planner model's context window, from whatever catalog is already
28157
- * cached (#49): a vendor model comes from the picker catalog, a harness
28158
- * planner's model from the runner's own discovered models. Reads only —
28159
- * never triggers a fetch or discovery of its own, so an unknown window stays
28160
- * unknown and context fill is omitted rather than guessed.
29000
+ * A model's context window from whatever catalog is already cached (#49),
29001
+ * for a usage record whose reporter stated none: a vendor's model from the
29002
+ * picker catalog (its own `/models` endpoint), a runner's model from that
29003
+ * runner's own discovered listing. `source` is the record's reporter, and
29004
+ * `model` the id it named. Reads only — never triggers a fetch or discovery
29005
+ * of its own, so an unknown window stays unknown and context fill is
29006
+ * omitted rather than guessed.
28161
29007
  */
28162
- contextWindowFor(modelId) {
28163
- if (!modelId) return void 0;
28164
- const picker = this.cachedPickerOptions?.find((o) => o.id === modelId)?.contextWindow;
28165
- if (picker && picker > 0) return picker;
28166
- for (const runner of this.config.enabledRunners ?? []) {
28167
- const found = this.discovery.getCached(runner)?.find((m) => m.modelId === modelId)?.contextWindow;
28168
- if (found && found > 0) return found;
28169
- }
28170
- return void 0;
29008
+ contextWindowFor(source, model) {
29009
+ if (!model) return void 0;
29010
+ const window = isApiProvider(source) ? this.cachedPickerOptions?.find((o) => o.apiProvider === source && o.id === prefixModelId(source, stripModelPrefix(model, source)))?.contextWindow : this.discovery.getCached(source)?.find((m) => m.modelId === model)?.contextWindow;
29011
+ return window && window > 0 ? window : void 0;
28171
29012
  }
28172
29013
  invalidate() {
28173
29014
  this.discovery.clear();
@@ -28193,24 +29034,28 @@ var defaultExec3 = async (command, options) => {
28193
29034
  return { stdout: String(stdout) };
28194
29035
  };
28195
29036
  var CHECK_TIMEOUT_MS = 8e3;
29037
+ var NEGATIVE_TTL_MS2 = 6e4;
28196
29038
  var RunnerInstallation = class {
28197
29039
  cache = /* @__PURE__ */ new Map();
28198
29040
  inflight = /* @__PURE__ */ new Map();
28199
29041
  registry;
28200
29042
  execImpl;
28201
- constructor(registry, execImpl = defaultExec3) {
29043
+ now;
29044
+ constructor(registry, execImpl = defaultExec3, now = Date.now) {
28202
29045
  this.registry = registry;
28203
29046
  this.execImpl = execImpl;
29047
+ this.now = now;
28204
29048
  }
28205
29049
  /** True if the runner's CLI responds to `--version`. Cached per process. */
28206
29050
  async isInstalled(runner) {
28207
- if (this.cache.has(runner)) return this.cache.get(runner);
29051
+ const cached2 = this.cache.get(runner);
29052
+ if (cached2 && (cached2.installed || this.now() - cached2.at < NEGATIVE_TTL_MS2)) return cached2.installed;
28208
29053
  const existing = this.inflight.get(runner);
28209
29054
  if (existing) return existing;
28210
29055
  const manifest = this.registry.getManifest(runner);
28211
29056
  const command = manifest?.runner.command;
28212
29057
  if (!command) {
28213
- this.cache.set(runner, false);
29058
+ this.cache.set(runner, { installed: false, at: this.now() });
28214
29059
  return false;
28215
29060
  }
28216
29061
  const probe = (async () => {
@@ -28221,13 +29066,22 @@ var RunnerInstallation = class {
28221
29066
  return false;
28222
29067
  }
28223
29068
  })().then((installed) => {
28224
- this.cache.set(runner, installed);
29069
+ this.cache.set(runner, { installed, at: this.now() });
28225
29070
  this.inflight.delete(runner);
28226
29071
  return installed;
28227
29072
  });
28228
29073
  this.inflight.set(runner, probe);
28229
29074
  return probe;
28230
29075
  }
29076
+ /**
29077
+ * Forget cached "not installed" verdicts so the next probe re-runs them. A
29078
+ * runner-list refresh calls this, so a CLI installed while the host runs is
29079
+ * offered without a restart. An installed verdict stays: it cannot have
29080
+ * changed without the binary going away, and re-probing one costs a spawn.
29081
+ */
29082
+ refresh() {
29083
+ for (const [runner, probe] of this.cache) if (!probe.installed) this.cache.delete(runner);
29084
+ }
28231
29085
  /**
28232
29086
  * Whether this runner can serve as a harness planner (ADR-0009, T9), and why
28233
29087
  * not when it cannot. Surfaces call this to grey out an unusable agent in the
@@ -28658,6 +29512,8 @@ var PlannerSession = class {
28658
29512
  * outlives the session — nothing from one goal may reach the next.
28659
29513
  */
28660
29514
  nativeSessionId = null;
29515
+ /** What was counted of that session's running cost, so a resume counts on from it rather than dropping its first turn. */
29516
+ sessionCost = null;
28661
29517
  /** Set once the first spawn attached. */
28662
29518
  start = null;
28663
29519
  /**
@@ -28848,6 +29704,7 @@ var CliAgentAiService = class {
28848
29704
  // hit its rate limit, or lost its login must say so in the chat rather
28849
29705
  // than leave an empty planner bubble.
28850
29706
  failure: turn.error,
29707
+ usageLimit: turn.usageLimit,
28851
29708
  fullText: [...carried, turn.text].filter((part) => part.trim()).join("\n\n")
28852
29709
  };
28853
29710
  };
@@ -28914,6 +29771,7 @@ var CliAgentAiService = class {
28914
29771
  };
28915
29772
  const subagentUsage = /* @__PURE__ */ new Map();
28916
29773
  let error;
29774
+ let usageLimit;
28917
29775
  let stepIndex = 0;
28918
29776
  let backgroundAgents = 0;
28919
29777
  const truncate = (value) => value.length > LOG_MAX_CHARS ? `${value.slice(0, LOG_MAX_CHARS)}
@@ -29022,6 +29880,7 @@ var CliAgentAiService = class {
29022
29880
  }
29023
29881
  case "error":
29024
29882
  error = event.message;
29883
+ usageLimit = event.usageLimit;
29025
29884
  return;
29026
29885
  case "turn_end":
29027
29886
  return;
@@ -29040,12 +29899,13 @@ var CliAgentAiService = class {
29040
29899
  settle(id, "The agent ended the turn without reporting this call's result.", false, "not_executed");
29041
29900
  }
29042
29901
  session.nativeSessionId = adapter.nativeSessionId() ?? session.nativeSessionId;
29902
+ session.sessionCost = adapter.sessionCost?.() ?? session.sessionCost;
29043
29903
  if (error || signal?.aborted) session.stop();
29044
29904
  if (error) {
29045
29905
  const failure = failureAfterReply(text3.slice(finalRunStart), Math.max(blockStart, finalRunStart) - finalRunStart, error);
29046
29906
  onProgress({ type: "text_retracted", segmentId: finalSegment });
29047
29907
  text3 = text3.slice(0, finalRunStart);
29048
- return { text: text3, researchLog, error: failure, aborted: signal?.aborted, backgroundAgents };
29908
+ return { text: text3, researchLog, error: failure, usageLimit, aborted: signal?.aborted, backgroundAgents };
29049
29909
  }
29050
29910
  return { text: text3, researchLog, error, aborted: signal?.aborted, backgroundAgents };
29051
29911
  }
@@ -29101,7 +29961,7 @@ var CliAgentAiService = class {
29101
29961
  session.stop();
29102
29962
  const start = session.start;
29103
29963
  if (!start) throw new Error("No active planner conversation.");
29104
- return this.startAdapter({ ...start.startOptions, resumeSessionId: session.nativeSessionId ?? void 0 }, start.tools, session);
29964
+ return this.startAdapter({ ...start.startOptions, resumeSessionId: session.nativeSessionId ?? void 0, resumeCost: session.sessionCost ?? void 0 }, start.tools, session);
29105
29965
  }
29106
29966
  /**
29107
29967
  * What the planner gets of the repository's config, its runner being
@@ -29113,9 +29973,9 @@ var CliAgentAiService = class {
29113
29973
  const reader = connectorFor(this.runner)?.repoConfig;
29114
29974
  if (!reader) return { systemPrompt };
29115
29975
  const found = reader.projectServers(cwd);
29116
- const { started, held } = await decideProjectServers(found.servers, { repoRoot: repoRoot(cwd), approval, approvals: this.projectServerApprovals });
29976
+ const { started, held: held2 } = await decideProjectServers(found.servers, { repoRoot: repoRoot(cwd), approval, approvals: this.projectServerApprovals });
29117
29977
  return {
29118
- systemPrompt: [systemPrompt, instructionsBlock(reader.instructions(cwd)), heldServersNote(held.map((server4) => ({ server: server4, reason: NOT_APPROVED })), found.unreadable)].filter(Boolean).join("\n\n"),
29978
+ systemPrompt: [systemPrompt, instructionsBlock(reader.instructions(cwd)), heldServersNote(held2.map((server4) => ({ server: server4, reason: NOT_APPROVED })), found.unreadable)].filter(Boolean).join("\n\n"),
29119
29979
  ...started.length > 0 ? { projectServers: started } : {}
29120
29980
  };
29121
29981
  }
@@ -29280,6 +30140,10 @@ var SessionCatalog = class {
29280
30140
  allowlist() {
29281
30141
  return this.deps.settings().modelAllowlist ?? {};
29282
30142
  }
30143
+ /** The enabled runners and allowlist together, for whoever compares an assignment with them. */
30144
+ scope() {
30145
+ return { enabledRunners: this.enabledRunners(), allowlist: this.allowlist(), models: this.models() };
30146
+ }
29283
30147
  /** The models the user allows on one runner; unset means no restriction. */
29284
30148
  allowlistFor(runner) {
29285
30149
  return this.allowlist()[runner];
@@ -29294,7 +30158,7 @@ var SessionCatalog = class {
29294
30158
  */
29295
30159
  models() {
29296
30160
  const out = { ...this.discovered };
29297
- for (const runner of /* @__PURE__ */ new Set([...Object.keys(out), ...this.deps.planRunners()])) {
30161
+ for (const runner of /* @__PURE__ */ new Set([...Object.keys(out), ...this.deps.planRunners(), ...this.enabledRunners()])) {
29298
30162
  const cached2 = this.deps.modelResolver.getCachedRunnerModels(runner);
29299
30163
  if (cached2.length > 0) out[runner] = cached2;
29300
30164
  }
@@ -29331,9 +30195,16 @@ var SessionCatalog = class {
29331
30195
  * asked for.
29332
30196
  */
29333
30197
  async live() {
29334
- const runners = this.enabledRunners();
29335
- await this.discover(runners);
29336
- return this.queryCatalog(runners);
30198
+ await this.discover(this.enabledRunners());
30199
+ return this.current();
30200
+ }
30201
+ /**
30202
+ * What {@link live} answers, from what is cached and without discovering:
30203
+ * the enabled runners, however the plan was started. The per-turn block
30204
+ * reads this, so it can never offer a different set than `list_runners`.
30205
+ */
30206
+ current() {
30207
+ return this.queryCatalog(this.enabledRunners());
29337
30208
  }
29338
30209
  queryCatalog(runners) {
29339
30210
  const registered = runners.filter((runner) => this.deps.registry.getManifest(runner) !== void 0);
@@ -29359,7 +30230,8 @@ var SessionCatalog = class {
29359
30230
  modelsByRunner: this.models(),
29360
30231
  runnerModes: this.modes(runners),
29361
30232
  perRunnerAllowlist: this.allowlist(),
29362
- autonomousDefault: this.deps.config.autonomousMode
30233
+ autonomousDefault: this.deps.config.autonomousMode,
30234
+ ...this.deps.liveRunner ? { liveRunner: this.deps.liveRunner } : {}
29363
30235
  };
29364
30236
  }
29365
30237
  /** What a runner offers, as `runnerAssignment` needs it. Spawns the runner's CLI to list models. */
@@ -29424,6 +30296,108 @@ function forkPlanState(plan, tasks, dialogue, now) {
29424
30296
  };
29425
30297
  }
29426
30298
 
30299
+ // src/services/PlannerResend.ts
30300
+ var HELD = "Send a message once it has reset: what waits goes with it.";
30301
+ var WAIT = "planner";
30302
+ var PlannerResend = class {
30303
+ constructor(host) {
30304
+ this.host = host;
30305
+ }
30306
+ host;
30307
+ waits = new UsageLimitWaits(() => this.due());
30308
+ /** When the last resend at a reset went out, until a turn lands: a limit in that window is a relapse. */
30309
+ resentAt = null;
30310
+ /** Whether turns wait on the limit: until the reset, or for the user's next message. */
30311
+ holds() {
30312
+ return this.host.plan()?.plannerResend !== void 0;
30313
+ }
30314
+ /** A planner turn Ordewell started ended on the planner's usage limit, which it reported as `said`. */
30315
+ limited(said2) {
30316
+ const plan = this.host.plan();
30317
+ const runner = this.host.plannerRunner() ?? void 0;
30318
+ const now = /* @__PURE__ */ new Date();
30319
+ const relapsed = this.resentAt !== null && now.getTime() - this.resentAt < RELAPSE_MS;
30320
+ this.resentAt = null;
30321
+ this.waits.clear();
30322
+ const name = runner ?? "Planner";
30323
+ const reset = runner && !relapsed ? readUsageLimitReset(runner, said2, now) : null;
30324
+ if (!reset) {
30325
+ if (plan) plan.plannerResend = runner ? { runner } : {};
30326
+ const held2 = relapsed ? "hit it again right after the resend" : "did not say when it resets";
30327
+ const text3 = relapsed ? `${name} usage limit again right after the resend, so nothing is resent on its own. ${HELD}` : `${name} usage limit; it did not say when it resets. ${HELD}`;
30328
+ return { text: text3, resendAt: null, held: held2 };
30329
+ }
30330
+ const at = new Date(reset.getTime() + RESUME_MARGIN_MS);
30331
+ if (plan) plan.plannerResend = { runner, at: at.toISOString() };
30332
+ this.waits.arm(WAIT, at);
30333
+ return { text: `${name} usage limit, resets ${resumeTime(reset, now)}. Resending at ${resumeTime(at, now)}; anything you send meanwhile goes with it.`, resendAt: at };
30334
+ }
30335
+ /**
30336
+ * A new input arrived — the user's message, or a wake-up the supervisor
30337
+ * asked for. What was held for one goes with it now; so does what waits on
30338
+ * a runner the planner has left, since nothing it waits for applies to the
30339
+ * planner that will answer. True when it lifted a wait.
30340
+ */
30341
+ arrived() {
30342
+ const saved = this.host.plan()?.plannerResend;
30343
+ if (!saved || saved.at !== void 0 && !this.switched(saved)) return false;
30344
+ this.lift();
30345
+ return true;
30346
+ }
30347
+ /** Keep what waits from going on its own; it goes with the user's next message. False when nothing waits on a reset. */
30348
+ cancel() {
30349
+ const plan = this.host.plan();
30350
+ const saved = plan?.plannerResend;
30351
+ if (!plan || saved?.at === void 0) return false;
30352
+ this.waits.clear();
30353
+ plan.plannerResend = held(saved);
30354
+ return true;
30355
+ }
30356
+ /** A turn landed: the resend before it got past the limit. */
30357
+ landed() {
30358
+ this.resentAt = null;
30359
+ }
30360
+ /** A plan adopted off disk: re-arm the wait it saved. A time already past resends at once. */
30361
+ restore() {
30362
+ this.reset();
30363
+ const saved = this.host.plan()?.plannerResend;
30364
+ if (saved?.at === void 0) return;
30365
+ const at = Date.parse(saved.at);
30366
+ if (Number.isNaN(at)) delete saved.at;
30367
+ else this.waits.arm(WAIT, new Date(at));
30368
+ }
30369
+ /** The plan is gone: nothing it waited on carries over. */
30370
+ reset() {
30371
+ this.waits.clear();
30372
+ this.resentAt = null;
30373
+ }
30374
+ due() {
30375
+ const plan = this.host.plan();
30376
+ const saved = plan?.plannerResend;
30377
+ if (!plan || saved?.at === void 0) return;
30378
+ if (this.switched(saved)) {
30379
+ plan.plannerResend = held(saved);
30380
+ this.host.changed();
30381
+ this.host.notice(`The planner no longer runs on ${saved.runner}, whose usage limit it waited on, so nothing was resent. ${HELD}`);
30382
+ return;
30383
+ }
30384
+ delete plan.plannerResend;
30385
+ this.resentAt = Date.now();
30386
+ this.host.due();
30387
+ }
30388
+ lift() {
30389
+ this.waits.clear();
30390
+ const plan = this.host.plan();
30391
+ if (plan) delete plan.plannerResend;
30392
+ }
30393
+ switched(saved) {
30394
+ return saved.runner !== void 0 && saved.runner !== (this.host.plannerRunner() ?? void 0);
30395
+ }
30396
+ };
30397
+ function held({ runner }) {
30398
+ return runner ? { runner } : {};
30399
+ }
30400
+
29427
30401
  // src/services/PlanTurns.ts
29428
30402
  var START_HOLD_TIMEOUT_MS = 5 * 6e4;
29429
30403
  function turnPrompt(batch) {
@@ -29440,6 +30414,16 @@ var PlanTurns = class {
29440
30414
  constructor(host, holdTimeoutMs = START_HOLD_TIMEOUT_MS) {
29441
30415
  this.host = host;
29442
30416
  this.holdTimeoutMs = holdTimeoutMs;
30417
+ this.resend = new PlannerResend({
30418
+ plan: () => host.plan(),
30419
+ plannerRunner: () => host.plannerRunner(),
30420
+ due: () => {
30421
+ host.announce();
30422
+ this.pump();
30423
+ },
30424
+ changed: () => host.announce(),
30425
+ notice: (message) => host.notice(message)
30426
+ });
29443
30427
  }
29444
30428
  host;
29445
30429
  holdTimeoutMs;
@@ -29452,6 +30436,33 @@ var PlanTurns = class {
29452
30436
  /** The user stopped the turn wake-ups waited behind: they wait for the next input to open one, not for the stop's release. */
29453
30437
  quiet = false;
29454
30438
  seq = 0;
30439
+ resend;
30440
+ /** A turn ended on the planner's usage limit: arm the wait for its inputs. Saved with the failure the turn records. */
30441
+ usageLimit(said2) {
30442
+ return this.resend.limited(said2);
30443
+ }
30444
+ /** Keep what waits on the planner's usage limit from being sent at the reset; see {@link PlannerResend.cancel}. */
30445
+ cancelResend() {
30446
+ if (!this.resend.cancel()) return false;
30447
+ this.host.announce();
30448
+ return true;
30449
+ }
30450
+ /**
30451
+ * The conversation's opening ended on the planner's usage limit: its goal
30452
+ * waits with the limit, ahead of what was sent behind it, whose senders are
30453
+ * answered now as any sent during the wait are.
30454
+ */
30455
+ waitOnLimit(input) {
30456
+ const plan = this.host.plan();
30457
+ if (!plan) return;
30458
+ plan.pendingTurns = [this.pending(input, false), ...plan.pendingTurns ?? []];
30459
+ this.host.persist();
30460
+ for (const t of plan.pendingTurns) if (t.source === "user") this.settle(t.id, (w) => w.resolve(plan));
30461
+ }
30462
+ /** A plan adopted off disk: the wait it saved is armed again. */
30463
+ restoreResend() {
30464
+ this.resend.restore();
30465
+ }
29455
30466
  /** Whether a turn this module opened is in flight. */
29456
30467
  get busy() {
29457
30468
  return this.running !== null;
@@ -29493,25 +30504,33 @@ var PlanTurns = class {
29493
30504
  admit(input, forced) {
29494
30505
  const plan = this.host.plan();
29495
30506
  if (!plan) return Promise.reject(new Error("No planning conversation to continue"));
29496
- const entry = {
29497
- id: `turn-${Date.now()}-${++this.seq}`,
29498
- source: input.source,
29499
- text: input.text,
29500
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
29501
- ...input.skills && input.skills.length > 0 ? { skills: [...input.skills] } : {},
29502
- ...forced ? { forced: true } : {}
29503
- };
30507
+ const entry = this.pending(input, forced);
29504
30508
  this.quiet = false;
30509
+ if (this.resend.arrived()) this.host.announce();
29505
30510
  const pending = plan.pendingTurns ?? [];
29506
30511
  const at = forced ? pending.findIndex((t) => !t.forced && !this.running?.ids.has(t.id)) : -1;
29507
30512
  plan.pendingTurns = at === -1 ? [...pending, entry] : [...pending.slice(0, at), entry, ...pending.slice(at)];
29508
30513
  this.host.persist();
30514
+ if (input.source === "user" && this.resend.holds()) {
30515
+ this.pump();
30516
+ return Promise.resolve(plan);
30517
+ }
29509
30518
  const settled = new Promise((resolve9, reject) => {
29510
30519
  this.waiters.set(entry.id, { resolve: resolve9, reject, signal: input.signal });
29511
30520
  });
29512
30521
  this.pump();
29513
30522
  return settled;
29514
30523
  }
30524
+ pending(input, forced) {
30525
+ return {
30526
+ id: `turn-${Date.now()}-${++this.seq}`,
30527
+ source: input.source,
30528
+ text: input.text,
30529
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
30530
+ ...input.skills && input.skills.length > 0 ? { skills: [...input.skills] } : {},
30531
+ ...forced ? { forced: true } : {}
30532
+ };
30533
+ }
29515
30534
  /**
29516
30535
  * Start the next turn if one is due: nothing of ours is in flight, the
29517
30536
  * conversation is free, and inputs wait. The host calls it when any planner
@@ -29522,7 +30541,7 @@ var PlanTurns = class {
29522
30541
  * the plan, what still waits, and the planner in use at that moment.
29523
30542
  */
29524
30543
  pump() {
29525
- if (this.running || this.detecting) return;
30544
+ if (this.running || this.detecting || this.resend.holds()) return;
29526
30545
  const plan = this.host.plan();
29527
30546
  if (!plan || this.host.conversationBusy()) return;
29528
30547
  const batch = this.liveBatch(plan);
@@ -29602,6 +30621,7 @@ var PlanTurns = class {
29602
30621
  * conversation's to abandon; whatever it settles lands nothing here.
29603
30622
  */
29604
30623
  abandon() {
30624
+ this.resend.reset();
29605
30625
  this.running?.release();
29606
30626
  this.running = null;
29607
30627
  this.quiet = false;
@@ -29645,8 +30665,10 @@ var PlanTurns = class {
29645
30665
  clearTimeout(timeout);
29646
30666
  if (this.running !== running2) return;
29647
30667
  const forceStopped = !outcome.ok && running2.forceStopped === true;
30668
+ const limited = !outcome.ok && outcome.err instanceof PlannerUsageLimitError;
30669
+ if (outcome.ok) this.resend.landed();
29648
30670
  try {
29649
- const dropped = outcome.ok || isInterruption(outcome.err) && !forceStopped ? ids : new Set(batch.filter((t) => t.source === "user").map((t) => t.id));
30671
+ const dropped = limited ? /* @__PURE__ */ new Set() : outcome.ok || isInterruption(outcome.err) && !forceStopped ? ids : new Set(batch.filter((t) => t.source === "user").map((t) => t.id));
29650
30672
  const took = this.take(plan, dropped);
29651
30673
  if (forceStopped) plan.pendingTurns = [...(plan.pendingTurns ?? []).filter((t) => t.forced), ...(plan.pendingTurns ?? []).filter((t) => !t.forced)];
29652
30674
  if (took || forceStopped) this.host.persist();
@@ -29660,7 +30682,11 @@ var PlanTurns = class {
29660
30682
  const ended = outcome;
29661
30683
  const waiting = new Set((plan.pendingTurns ?? []).map((t) => t.id));
29662
30684
  for (const t of batch) {
29663
- if (forceStopped && waiting.has(t.id)) continue;
30685
+ if (limited && t.source === "user") {
30686
+ this.settle(t.id, (w) => w.resolve(plan));
30687
+ continue;
30688
+ }
30689
+ if ((forceStopped || limited) && waiting.has(t.id)) continue;
29664
30690
  const delivered = this.settle(t.id, (w) => ended.ok ? w.resolve(ended.plan) : w.reject(ended.err));
29665
30691
  if (!delivered && !ended.ok && !isInterruption(ended.err) && t.source === "user") this.host.notice(replayFailed(t, ended.err));
29666
30692
  }
@@ -29737,7 +30763,6 @@ var RunSupervision = class {
29737
30763
  blockedBy: (failedId) => [...blockingFailures([...this.host.tasks()])].filter(([, failures]) => failures.includes(failedId)).map(([taskId]) => taskId),
29738
30764
  readBudget: () => this.settings()?.wakeUpReadBudget ?? host.userSettings().wakeUpReadBudget,
29739
30765
  retryLimit: () => this.retryLimit(),
29740
- plannerRunner: () => host.plannerRunner(),
29741
30766
  restartPlanner: () => host.restartPlanner(),
29742
30767
  abandonTurn: () => host.abandonTurn(),
29743
30768
  goUnsupervised: (reason) => this.goUnsupervised(reason),
@@ -29946,14 +30971,14 @@ function split({ verdict, idleSince, isolation, checkpoint, queued: queued2, con
29946
30971
  return { plan, status: { verdict, idleSince, isolation, checkpoint, queued: queued2, continuable, awaitingApproval, mergeGate: mergeGate2, forcedPastGate, waitsForApproval: waitsForApproval2 } };
29947
30972
  }
29948
30973
  function refresh(previous, tasks) {
29949
- const held = /* @__PURE__ */ new Map();
30974
+ const held2 = /* @__PURE__ */ new Map();
29950
30975
  const index = (list) => list.forEach((t) => {
29951
- held.set(t.id, t);
30976
+ held2.set(t.id, t);
29952
30977
  index(t.subtasks);
29953
30978
  });
29954
30979
  index(previous);
29955
30980
  const fold = ({ subtasks, ...task }) => {
29956
- const was = held.get(task.id);
30981
+ const was = held2.get(task.id);
29957
30982
  const { checkpoint, ...status } = was ? split(was).status : {};
29958
30983
  const waits = task.status === "awaiting_user" && task.awaitingReason === "checkpoint";
29959
30984
  return { ...task, ...status, ...waits && checkpoint ? { checkpoint } : {}, subtasks: subtasks.map(fold) };
@@ -30327,9 +31352,9 @@ var fs14 = __toESM(require("fs"));
30327
31352
  var path31 = __toESM(require("path"));
30328
31353
 
30329
31354
  // src/utils/sessionId.ts
30330
- var import_crypto11 = require("crypto");
31355
+ var import_crypto12 = require("crypto");
30331
31356
  function mintSessionId() {
30332
- return `session-${(0, import_crypto11.randomBytes)(16).toString("hex")}`;
31357
+ return `session-${(0, import_crypto12.randomBytes)(16).toString("hex")}`;
30333
31358
  }
30334
31359
 
30335
31360
  // src/utils/sessionStore.ts
@@ -30366,8 +31391,8 @@ function dropLegacyFile(sessionsDir, meta) {
30366
31391
  const legacy = path31.join(sessionsDir, legacySessionFilename(meta));
30367
31392
  if (legacy === path31.join(sessionsDir, sessionFilename(meta)) || !fs14.existsSync(legacy)) return;
30368
31393
  try {
30369
- const held = JSON.parse(fs14.readFileSync(legacy, "utf-8")).meta?.id;
30370
- if (held === meta.id) fs14.unlinkSync(legacy);
31394
+ const held2 = JSON.parse(fs14.readFileSync(legacy, "utf-8")).meta?.id;
31395
+ if (held2 === meta.id) fs14.unlinkSync(legacy);
30371
31396
  } catch {
30372
31397
  }
30373
31398
  }
@@ -30500,24 +31525,25 @@ function listTaskLogAttempts(location, taskId) {
30500
31525
  }
30501
31526
  return names.map((name) => /^([1-9]\d*)\.jsonl$/.exec(name)?.[1]).filter((n) => n !== void 0).map(Number).sort((a, b) => a - b);
30502
31527
  }
30503
- function openTaskLog(location, taskId) {
31528
+ function openTaskLog(location, taskId, attempt) {
30504
31529
  ensureStateDirIgnored(location.baseDir);
30505
31530
  const dir = taskDir(location, taskId);
30506
31531
  fs15.mkdirSync(dir, { recursive: true });
30507
- let attempt = (listTaskLogAttempts(location, taskId).pop() ?? 0) + 1;
31532
+ const next = (listTaskLogAttempts(location, taskId).pop() ?? 0) + 1;
31533
+ let number = attempt !== void 0 && Number.isInteger(attempt) && attempt >= 1 ? attempt : next;
30508
31534
  for (; ; ) {
30509
- const file = path32.join(dir, `${attempt}${EXTENSION}`);
31535
+ const file = path32.join(dir, `${number}${EXTENSION}`);
30510
31536
  try {
30511
31537
  fs15.writeFileSync(file, "", { flag: "wx" });
30512
31538
  } catch (err) {
30513
31539
  if (err.code === "EEXIST") {
30514
- attempt += 1;
31540
+ number = Math.max(number + 1, next);
30515
31541
  continue;
30516
31542
  }
30517
31543
  throw err;
30518
31544
  }
30519
31545
  return {
30520
- attempt,
31546
+ attempt: number,
30521
31547
  append(events) {
30522
31548
  if (events.length === 0) return;
30523
31549
  fs15.appendFileSync(file, events.map((e) => `${JSON.stringify(e)}
@@ -30563,11 +31589,13 @@ var TaskLogRecorder = class {
30563
31589
  open;
30564
31590
  flushMs;
30565
31591
  logger;
31592
+ /** Each task's latest attempt, by the session it records: its log takes a {@link note} until the next attempt starts. */
31593
+ latest = /* @__PURE__ */ new Map();
30566
31594
  wrap(runner) {
30567
31595
  return {
30568
31596
  spawn: async (opts) => {
30569
31597
  const session = await runner.spawn(opts);
30570
- this.record(opts.taskId, session, opts.skills);
31598
+ this.record(opts.taskId, session, opts.skills, opts.attempt);
30571
31599
  return session;
30572
31600
  },
30573
31601
  stop: (sessionId) => runner.stop(sessionId),
@@ -30577,11 +31605,20 @@ var TaskLogRecorder = class {
30577
31605
  }
30578
31606
  };
30579
31607
  }
31608
+ /**
31609
+ * An entry the session did not emit, written into the log of the attempt
31610
+ * that runs `sessionId` and sent at once: what Ordewell made of one of its
31611
+ * events once that settled. Dropped once the task's next attempt started.
31612
+ */
31613
+ note(sessionId, taskId, entry) {
31614
+ const attempt = this.latest.get(taskId);
31615
+ if (attempt?.sessionId === sessionId) attempt.note(entry);
31616
+ }
30580
31617
  /** Start a new attempt's log for `session`, opening with the skills it was given, and keep it until the session exits. */
30581
- record(taskId, session, skills = []) {
31618
+ record(taskId, session, skills = [], attemptNumber) {
30582
31619
  let file;
30583
31620
  try {
30584
- file = this.open(this.deps.location(), taskId);
31621
+ file = this.open(this.deps.location(), taskId, attemptNumber);
30585
31622
  } catch (err) {
30586
31623
  this.logger.warn("taskLog", `could not create the log of task ${taskId}; it streams but is not saved`, err);
30587
31624
  file = null;
@@ -30608,6 +31645,10 @@ var TaskLogRecorder = class {
30608
31645
  }
30609
31646
  this.deps.broadcast({ type: "task_log", taskId, attempt, events: taskLogForSurface(events) });
30610
31647
  };
31648
+ this.latest.set(taskId, { sessionId: session.id, note: (entry) => {
31649
+ pending.push(entry);
31650
+ flush();
31651
+ } });
30611
31652
  const openCalls = /* @__PURE__ */ new Set();
30612
31653
  session.onEvent((event) => {
30613
31654
  const entry = toTaskLogEvent(event);
@@ -30628,21 +31669,82 @@ var TaskLogRecorder = class {
30628
31669
  }
30629
31670
  };
30630
31671
 
31672
+ // src/services/ContextWindowFill.ts
31673
+ var ContextWindowFill = class {
31674
+ constructor(catalog, last = {}) {
31675
+ this.catalog = catalog;
31676
+ if (last.contextWindow !== void 0) this.known.set(modelKey(last.source, last.model), last.contextWindow);
31677
+ }
31678
+ catalog;
31679
+ /** The window known for each model of the stream, by reporter and model. */
31680
+ known = /* @__PURE__ */ new Map();
31681
+ fill(record) {
31682
+ if (record.subagentId || record.aggregate) return record;
31683
+ const key = modelKey(record.source, record.model);
31684
+ const reported = record.contextWindow !== void 0 && record.contextWindow > 0 ? record.contextWindow : void 0;
31685
+ const window = reported ?? this.known.get(key) ?? (record.model ? positive(this.catalog(record.source, record.model)) : void 0);
31686
+ if (window !== void 0) this.known.set(key, window);
31687
+ return window === void 0 || window === record.contextWindow ? record : { ...record, contextWindow: window };
31688
+ }
31689
+ };
31690
+ function modelKey(source, model) {
31691
+ return JSON.stringify([source ?? null, model ?? null]);
31692
+ }
31693
+ function withContextWindows(runner, catalog) {
31694
+ return {
31695
+ spawn: async (opts) => windowedSession(await runner.spawn(opts), new ContextWindowFill(catalog, opts.resumeWindow)),
31696
+ stop: (sessionId) => runner.stop(sessionId),
31697
+ stopAll: () => runner.stopAll(),
31698
+ get activeCount() {
31699
+ return runner.activeCount;
31700
+ }
31701
+ };
31702
+ }
31703
+ function windowedSession(session, windows) {
31704
+ const listeners = [];
31705
+ session.onEvent((event) => {
31706
+ const filled = event.type === "usage" ? { ...event, record: windows.fill(event.record) } : event;
31707
+ for (const listener of listeners) listener(filled);
31708
+ });
31709
+ const onEvent = (listener) => {
31710
+ listeners.push(listener);
31711
+ };
31712
+ return new Proxy(session, {
31713
+ get: (target, prop) => {
31714
+ if (prop === "onEvent") return onEvent;
31715
+ const value = Reflect.get(target, prop, target);
31716
+ return typeof value === "function" ? value.bind(target) : value;
31717
+ }
31718
+ });
31719
+ }
31720
+ function positive(n) {
31721
+ return n !== void 0 && n > 0 ? n : void 0;
31722
+ }
31723
+
30631
31724
  // src/services/PlannerUsage.ts
30632
31725
  var PlannerUsageLedger = class {
31726
+ /** `catalogWindow` serves the window of a planner model whose backend reports none. */
31727
+ constructor(catalogWindow = () => void 0) {
31728
+ this.catalogWindow = catalogWindow;
31729
+ this.windows = new ContextWindowFill(catalogWindow);
31730
+ }
31731
+ catalogWindow;
30633
31732
  usage = { totals: {} };
31733
+ windows;
30634
31734
  /** Fold one model call's record into the totals; returns the running ledger. */
30635
31735
  record(record) {
30636
- this.usage = addPlannerUsage(this.usage, record);
31736
+ this.usage = addPlannerUsage(this.usage, this.windows.fill(record));
30637
31737
  return this.usage;
30638
31738
  }
30639
31739
  /** Adopt a persisted ledger (a reopened session) or start from zero. */
30640
31740
  restore(usage) {
30641
31741
  this.usage = usage ?? { totals: {} };
31742
+ this.windows = new ContextWindowFill(this.catalogWindow, this.usage);
30642
31743
  }
30643
31744
  /** Start a fresh session's ledger from zero. */
30644
31745
  clear() {
30645
31746
  this.usage = { totals: {} };
31747
+ this.windows = new ContextWindowFill(this.catalogWindow);
30646
31748
  }
30647
31749
  /** Whether anything has been recorded — a plan with no usage says nothing. */
30648
31750
  get hasUsage() {
@@ -30737,19 +31839,54 @@ var PlanEditor = class {
30737
31839
  * between AI and MAN, are the patches that can leave a task incoherent
30738
31840
  * (unschedulable, or carrying fields that mean nothing for its new type),
30739
31841
  * so both go through the same {@link validateTaskEdit} guard the planner's
30740
- * task-ops applier uses (as the 'direct' actor, which skips the lock rule)
30741
- * rather than a second copy of the rules — and throws, so the surface can
30742
- * say why. Gated on the task existing so an edit to an unknown id still
31842
+ * task-ops applier uses (as the 'direct' actor, which skips the lock rule
31843
+ * but not a live runner's hold on its model) rather than a second copy of
31844
+ * the rules — and throws, so the surface can say why. Gated on the task existing so an edit to an unknown id still
30743
31845
  * falls through to the no-op `store.update` below instead of throwing.
30744
31846
  * Attached skills meet the rule a planner's do: a planner skill is refused,
30745
- * a name not found yet only warns.
31847
+ * a name not found yet only warns. A runner change is made by
31848
+ * {@link setTaskRunner}, which re-derives what the runner scopes, ahead of
31849
+ * the rest of the patch.
30746
31850
  *
30747
31851
  * Edits to one task commit in call order: the skill check waits on git, so
30748
31852
  * two quick chip edits (`[a]` then `[a, b]`) could otherwise land the first
30749
31853
  * one last.
30750
31854
  */
30751
31855
  updateTask(taskId, changes) {
30752
- const edit = (this.taskEdits.get(taskId) ?? Promise.resolve()).then(() => this.applyTaskUpdate(taskId, changes));
31856
+ return this.inTaskOrder(taskId, () => this.applyTaskUpdate(taskId, changes));
31857
+ }
31858
+ /**
31859
+ * The plan follows a model switch the user made with the runner's own
31860
+ * command (#106), as a direct edit of theirs: saved and announced as
31861
+ * `task_updated`, so every surface shows it, and what a later retry or
31862
+ * continue spawns on. It is the one model edit a live runner allows, since
31863
+ * it says what that runner already runs, and the one that skips the
31864
+ * allowlist: the user picked it in the runner. The runner's own label is
31865
+ * used only for a model discovery does not list. Effort is the runner's
31866
+ * when it reported one, else the plan's, either clamped to what the new
31867
+ * model offers; none for a model discovery does not list. `live` is asked
31868
+ * as the edit lands, after the edits queued ahead of it: by then the plan
31869
+ * may be another one, whose task of the same id the switch says nothing of.
31870
+ */
31871
+ followRunnerModel(taskId, model, effort, live) {
31872
+ return this.inTaskOrder(taskId, async () => {
31873
+ const task = this.store.get(taskId);
31874
+ if (!task || task.type === "user" || !live()) return null;
31875
+ const listed3 = this.catalog.models()[task.assignedRunner]?.find((m) => m.modelId === model.model);
31876
+ const thinkingEffort = clampThinkingEffort(effort ?? task.assignedModel?.thinkingEffort, listed3?.variants ?? []);
31877
+ if (task.assignedModel?.modelId === model.model && task.assignedModel.thinkingEffort === thinkingEffort) return this.plan();
31878
+ const assignedModel = {
31879
+ modelId: model.model,
31880
+ modelLabel: listed3?.modelLabel ?? model.label ?? model.model,
31881
+ ...thinkingEffort ? { thinkingEffort } : {},
31882
+ ...listed3 ? { availableVariants: listed3.variants.map((v) => v.id) } : {}
31883
+ };
31884
+ return this.commitTaskUpdate(taskId, { assignedModel });
31885
+ });
31886
+ }
31887
+ /** Edits to one task commit in call order; see {@link updateTask}. */
31888
+ inTaskOrder(taskId, apply) {
31889
+ const edit = (this.taskEdits.get(taskId) ?? Promise.resolve()).then(apply);
30753
31890
  const settled = edit.catch(() => {
30754
31891
  });
30755
31892
  this.taskEdits.set(taskId, settled);
@@ -30765,22 +31902,31 @@ var PlanEditor = class {
30765
31902
  const target = this.store.get(taskId);
30766
31903
  const skills = "skills" in changes && target ? await checkTaskSkillsEdit(target, typedSkills, this.taskSkills()) : void 0;
30767
31904
  if (skills?.errors.length) throw new PlanEditError(skills.errors.map((e) => e.message).join(" "));
30768
- if ((changes.dependencies || changes.type || changes.assignedModel || changes.taskMode || "ops" in changes) && target) {
31905
+ if ((changes.dependencies || changes.type || "assignedModel" in changes || "assignedRunner" in changes || changes.taskMode || "ops" in changes) && target) {
30769
31906
  const check2 = validateTaskEdit("direct", this.store.planTasks, taskId, changes, this.catalog.edit());
30770
31907
  if (!check2.ok) throw new PlanEditError(check2.error ?? "Those changes are not valid");
30771
31908
  if (check2.clear?.length || check2.set) {
30772
31909
  changes = { ...changes, ...Object.fromEntries((check2.clear ?? []).map((f) => [f, void 0])), ...check2.set };
30773
31910
  }
30774
31911
  }
30775
- const plan = await this.commit(
31912
+ const { assignedRunner, ...rest } = changes;
31913
+ if (target && assignedRunner && assignedRunner !== target.assignedRunner) {
31914
+ const moved = await this.setTaskRunner(taskId, assignedRunner);
31915
+ if (!moved || Object.keys(rest).length === 0) return moved;
31916
+ changes = rest;
31917
+ }
31918
+ const plan = await this.commitTaskUpdate(taskId, changes);
31919
+ if (plan) for (const warning of skills?.warnings ?? []) this.notice("warn", warning);
31920
+ return plan;
31921
+ }
31922
+ commitTaskUpdate(taskId, changes) {
31923
+ return this.commit(
30776
31924
  () => {
30777
31925
  changes = this.scheduler.releaseMovedResume(taskId, changes);
30778
31926
  return Boolean(this.store.update(taskId, changes));
30779
31927
  },
30780
31928
  () => this.broadcast({ type: "task_updated", taskId, changes })
30781
31929
  );
30782
- if (plan) for (const warning of skills?.warnings ?? []) this.notice("warn", warning);
30783
- return plan;
30784
31930
  }
30785
31931
  /**
30786
31932
  * Replace one task's dependency list — the named entry point the surfaces'
@@ -30797,7 +31943,8 @@ var PlanEditor = class {
30797
31943
  * because a runner change is never a single-field edit: the task's model,
30798
31944
  * thinking effort and mode are all scoped to its runner, so they are
30799
31945
  * re-derived from the new runner's catalog (see {@link retargetTaskRunner}).
30800
- * That needs discovery, which is why it is not a branch inside `updateTask`.
31946
+ * That needs discovery, which is why `updateTask` hands a runner change here
31947
+ * rather than writing it as a field.
30801
31948
  *
30802
31949
  * The runner is also admitted into `plan.runners` — see {@link admitRunner}.
30803
31950
  */
@@ -30807,11 +31954,13 @@ var PlanEditor = class {
30807
31954
  const task = this.store.get(taskId);
30808
31955
  if (!task) return null;
30809
31956
  if (task.assignedRunner === runner || task.type === "user") return plan;
31957
+ this.refuseLiveRetarget(task);
30810
31958
  const catalog = await this.catalog.runner(runner);
30811
31959
  const from = this.catalog.edit([task.assignedRunner]).runnerModes[task.assignedRunner];
30812
31960
  const changes = retargetTaskRunner(task, runner, this.allowedCatalog(catalog, runner), from);
30813
31961
  if (Object.keys(changes).length === 0) return this.plan();
30814
31962
  this.refuseUnavailableMode(runner, changes.taskMode, catalog.modes);
31963
+ this.refuseLiveRetarget(task);
30815
31964
  return this.commit(() => {
30816
31965
  if (!this.store.update(taskId, this.scheduler.releaseMovedResume(taskId, changes))) return false;
30817
31966
  this.admitRunner(runner, catalog.models);
@@ -30845,6 +31994,9 @@ var PlanEditor = class {
30845
31994
  return true;
30846
31995
  });
30847
31996
  }
31997
+ refuseLiveRetarget(task) {
31998
+ if (this.catalog.edit().liveRunner?.(task.id)) throw new PlanEditError(liveRunnerRefusal(task));
31999
+ }
30848
32000
  /**
30849
32001
  * A mode the host cannot run is refused, with its reason, not swapped for
30850
32002
  * another: the plan is the source of truth for what runs (ADR-0001), and a
@@ -31174,6 +32326,10 @@ var ExecutionCommands = class {
31174
32326
  const all = this.all(tasks);
31175
32327
  const live = all.filter(({ task }) => this.host.running(task.id));
31176
32328
  if (this.host.hasLiveWork()) {
32329
+ if (actor.kind === "user") {
32330
+ const waiting = all.filter(({ task }) => this.host.waitsForApproval(task.id)).length;
32331
+ return { act: async () => ({ message: await this.host.joinRun(waiting) }) };
32332
+ }
31177
32333
  return { refusal: `The plan is already running (${named2(live)} ${live.length === 1 ? "is" : "are"} running). Start one more ready task with run_task, or wait.` };
31178
32334
  }
31179
32335
  const toRun = all.filter(({ task }) => task.type === "ai" && task.status !== "completed" && task.status !== "skipped");
@@ -31599,8 +32755,11 @@ function mergeStopped(result) {
31599
32755
  switch (result.outcome) {
31600
32756
  case "merged":
31601
32757
  return "it merged";
31602
- case "blocked":
31603
- return "the user's checkout has changes that overlap what it would merge";
32758
+ case "blocked": {
32759
+ const off = result.blocked.filter((b) => b.reason === "wrong-branch");
32760
+ if (off.length === 0) return "the user's checkout has changes that overlap what it would merge";
32761
+ return off.map((b) => `${b.repo === "." ? "the checkout" : b.repo} is ${b.actual ? `on ${b.actual}` : b.rebasing ? "in the middle of a rebase" : "on a detached HEAD"}, not ${b.expected}, which the run started on`).join("; ");
32762
+ }
31604
32763
  case "conflict":
31605
32764
  return `it conflicts in ${result.repo}${result.files?.length ? `: ${result.files.slice(0, 5).join(", ")}` : ""}`;
31606
32765
  case "failed":
@@ -31725,11 +32884,17 @@ function liveAiService(config, workspaceRoot, mcpServer) {
31725
32884
  function isPlannerApproval(request) {
31726
32885
  return !isRunnerApproval(request);
31727
32886
  }
32887
+ function writtenTasks(before, after) {
32888
+ const said2 = ({ subtasks: _subtasks, status: _status, ...task }) => JSON.stringify(task);
32889
+ const prior = new Map(flattenTasks(before).map((t) => [t.id, said2(t)]));
32890
+ return flattenTasks(after).filter((t) => prior.get(t.id) !== said2(t)).map((t) => t.id);
32891
+ }
31728
32892
  function createSession(deps) {
31729
32893
  const pinnedAiService = deps.aiService;
31730
32894
  const mcpServer = deps.mcpServer ?? sharedMcpServer();
31731
32895
  const aiService = pinnedAiService ? () => pinnedAiService : liveAiService(deps.config, deps.workspaceRoot, mcpServer);
31732
32896
  const store = new PlanStore();
32897
+ const catalogWindow = (source, model) => deps.modelResolver.contextWindowFor?.(source, model);
31733
32898
  const taskLogs = new TaskLogRecorder({
31734
32899
  broadcast: deps.broadcast,
31735
32900
  // Read through the Session, whose id and workspace are the ones it saves
@@ -31763,7 +32928,7 @@ function createSession(deps) {
31763
32928
  const orchestrator = TaskOrchestrator.compose({
31764
32929
  config: deps.config,
31765
32930
  notifications: deps.notifications,
31766
- runner: taskLogs.wrap(runnerApprovals.wrap(deps.runner)),
32931
+ runner: taskLogs.wrap(runnerApprovals.wrap(withContextWindows(deps.runner, catalogWindow))),
31767
32932
  store,
31768
32933
  output: deps.taskOutput,
31769
32934
  isolation: deps.isolation,
@@ -31771,10 +32936,11 @@ function createSession(deps) {
31771
32936
  workspaceRoot: deps.workspaceRoot,
31772
32937
  skillsAt: (roots) => (deps.skillsService ?? new SkillsService(roots)).forRoot(roots),
31773
32938
  previousAttemptFromLog: (taskId) => lastAttemptDigest(session.taskLogLocation, taskId),
32939
+ lastLoggedAttempt: (taskId) => session.taskLogAttempts(taskId).at(-1) ?? 0,
31774
32940
  runSettings: () => session.planState?.runSettings,
31775
32941
  isApproved: (taskId) => session.isApproved(taskId)
31776
32942
  });
31777
- const usage = new PlannerUsageLedger();
32943
+ const usage = new PlannerUsageLedger(catalogWindow);
31778
32944
  const events = new SessionEventRelay({
31779
32945
  broadcast: deps.broadcast,
31780
32946
  onNotice: deps.onNotice,
@@ -31823,7 +32989,8 @@ function createSession(deps) {
31823
32989
  modelResolver: deps.modelResolver,
31824
32990
  settings: deps.settings,
31825
32991
  // Read through the Session, which holds the plan; the catalog is built first.
31826
- planRunners: () => session.planState?.runners ?? []
32992
+ planRunners: () => session.planState?.runners ?? [],
32993
+ liveRunner: (taskId) => orchestrator.getAttempt(taskId) !== void 0
31827
32994
  });
31828
32995
  const session = new Session({
31829
32996
  config: deps.config,
@@ -31845,6 +33012,7 @@ function createSession(deps) {
31845
33012
  usage,
31846
33013
  approvals,
31847
33014
  approvalPolicy,
33015
+ taskLogs,
31848
33016
  // Every planner's web reads, carried here or natively, are decided by the
31849
33017
  // one policy, which asks through the same channel as paths and commands.
31850
33018
  // The response cache and search provider are process-wide, so parallel
@@ -31895,6 +33063,7 @@ var Session = class {
31895
33063
  fsAdapter;
31896
33064
  approvals;
31897
33065
  approvalPolicy;
33066
+ taskLogs;
31898
33067
  fetcher;
31899
33068
  settingsFn;
31900
33069
  save;
@@ -31930,7 +33099,7 @@ var Session = class {
31930
33099
  turnId: () => this.conversation.currentTurnId,
31931
33100
  recordSkillLoad: (skill) => this.conversation.recordPlannerSkill(skill),
31932
33101
  liveCatalog: () => this.liveCatalog(),
31933
- coerce: (tasks) => coerceAssignments(tasks, this.catalog.allowlist(), this.catalog.models()),
33102
+ coerce: (tasks) => coerceAssignments(tasks, this.catalog.allowlist(), this.catalog.models(), this.store.planTasks),
31934
33103
  submitPlan: (tasks) => this.submitPlanFromTool(tasks),
31935
33104
  editPlan: (ops) => this.editPlanFromTool(ops),
31936
33105
  tasks: () => this.store.planTasks,
@@ -31975,6 +33144,7 @@ var Session = class {
31975
33144
  this.usage = parts.usage;
31976
33145
  this.approvals = parts.approvals;
31977
33146
  this.approvalPolicy = parts.approvalPolicy;
33147
+ this.taskLogs = parts.taskLogs;
31978
33148
  this.fetcher = parts.fetcher;
31979
33149
  this.skillsService = parts.skillsService;
31980
33150
  this.save = parts.saveSession;
@@ -31984,7 +33154,8 @@ var Session = class {
31984
33154
  aiService: () => this.aiService(),
31985
33155
  onProgress: (p) => this.events.progress(p),
31986
33156
  opening: (runners) => this.conversationOpening(runners),
31987
- catalog: () => this.catalog.queryCatalog(this.plan?.runners ?? []),
33157
+ catalog: () => this.catalog.current(),
33158
+ assignmentScope: () => this.catalog.scope(),
31988
33159
  autorun: () => this.userRunSettings().autorun,
31989
33160
  takeRunUpdates: () => this.untoldRunUpdates.splice(0),
31990
33161
  tasks: () => this.store.planTasks,
@@ -32004,13 +33175,18 @@ var Session = class {
32004
33175
  turnAborted: () => this.approvals.clear(isPlannerApproval),
32005
33176
  // After the turn has announced its end, so the next one never starts
32006
33177
  // ahead of it on a surface.
32007
- turnReleased: () => queueMicrotask(() => this.turns.pump())
33178
+ turnReleased: () => queueMicrotask(() => this.turns.pump()),
33179
+ usageLimit: (said2) => this.turns.usageLimit(said2)
32008
33180
  });
32009
33181
  this.turns = new PlanTurns({
32010
33182
  plan: () => this.closed ? null : this.plan,
32011
33183
  conversationBusy: () => this.conversation.isTurnInFlight,
32012
33184
  mayWake: () => this.supervision.mayWake(),
32013
33185
  persist: () => this.persist({ background: true }),
33186
+ announce: () => {
33187
+ this.persist({ background: true });
33188
+ this.events.planGenerated(this.plan, this.goal);
33189
+ },
32014
33190
  interrupt: () => {
32015
33191
  this.conversation.stopTurn("force_send");
32016
33192
  },
@@ -32027,7 +33203,8 @@ var Session = class {
32027
33203
  },
32028
33204
  notice: (message) => this.notice("error", message),
32029
33205
  plannerDetecting: () => installedPlannerAnswered() ? null : installedPlannerDetected(),
32030
- assertPlannerReady: () => this.assertPlannerReady()
33206
+ assertPlannerReady: () => this.assertPlannerReady(),
33207
+ plannerRunner: () => runnerForProvider(this.config.aiProvider)
32031
33208
  });
32032
33209
  this.supervision = new RunSupervision({
32033
33210
  plan: () => this.plan,
@@ -32045,7 +33222,6 @@ var Session = class {
32045
33222
  },
32046
33223
  deliver: (digest) => this.turns.enqueue({ source: "supervisor", text: digest }),
32047
33224
  pump: () => this.turns.pump(),
32048
- plannerRunner: () => runnerForProvider(this.config.aiProvider),
32049
33225
  restartPlanner: () => this.aiService().restart?.(),
32050
33226
  abandonTurn: () => this.conversation.abandonHungTurn(),
32051
33227
  notice: (level, message) => this.notice(level, message)
@@ -32127,6 +33303,7 @@ var Session = class {
32127
33303
  tasks: () => this.plan ? this.store.planTasks : null,
32128
33304
  planApproved: () => this.plan?.approved === true,
32129
33305
  taskApproved: (taskId) => this.isApproved(taskId),
33306
+ waitsForApproval: (taskId) => this.waitsForApproval(taskId),
32130
33307
  approve: (taskIds) => this.approve(taskIds),
32131
33308
  autonomous: () => this.config.autonomousMode,
32132
33309
  autorun: () => this.userRunSettings().autorun,
@@ -32148,6 +33325,7 @@ var Session = class {
32148
33325
  blockedBy: (taskId) => this.orchestrator.getBlockedBy(taskId),
32149
33326
  waitingRequests: (taskId) => this.approvals.outstanding().filter(({ request }) => isRunnerApproval(request) && request.taskId === taskId).map(({ id, request }) => ({ id, subject: request.subject })),
32150
33327
  runPlan: (actor) => this.runWholePlan(runOrigin(actor)),
33328
+ joinRun: (approved) => this.joinRun(approved),
32151
33329
  startTask: (taskId, actor) => this.startOneTask(taskId, runOrigin(actor)),
32152
33330
  stopTask: (taskId) => this.cancelTask(taskId),
32153
33331
  stopRun: () => this.stopExecution(),
@@ -32176,8 +33354,8 @@ var Session = class {
32176
33354
  * one: nothing it would start is approved until the user executes it.
32177
33355
  */
32178
33356
  heldEdit(taskId) {
32179
- const held = this.conversation.pendingOps();
32180
- if (!held || held.length === 0 || !this.plan) return false;
33357
+ const held2 = this.conversation.pendingOps();
33358
+ if (!held2 || held2.length === 0 || !this.plan) return false;
32181
33359
  if (taskId === void 0) return true;
32182
33360
  const before = this.store.get(taskId);
32183
33361
  const after = this.asEdited(taskId);
@@ -32190,10 +33368,10 @@ var Session = class {
32190
33368
  /** {@link editedTasks}, with the id each of the held batch's handles names in them. */
32191
33369
  editedPlan() {
32192
33370
  const unedited = { tasks: this.store.planTasks, handles: /* @__PURE__ */ new Map() };
32193
- const held = this.conversation.pendingOps();
32194
- if (!held || held.length === 0 || !this.plan) return unedited;
33371
+ const held2 = this.conversation.pendingOps();
33372
+ if (!held2 || held2.length === 0 || !this.plan) return unedited;
32195
33373
  const runners = this.editRunners();
32196
- const result = applyTaskOps(this.store.planTasks, held, runners, this.catalog.edit(runners));
33374
+ const result = applyTaskOps(this.store.planTasks, held2, runners, this.catalog.edit(runners));
32197
33375
  return result.ok ? { tasks: result.tasks, handles: result.handles ?? /* @__PURE__ */ new Map() } : unedited;
32198
33376
  }
32199
33377
  /** The task as the open turn's held edit leaves it; undefined when that edit removes it. */
@@ -32217,9 +33395,9 @@ var Session = class {
32217
33395
  * rules, then, for the supervisor, the oscillation guard. `held` are the
32218
33396
  * ops the turn already holds, which `ops` join; errors are numbered by `ops`.
32219
33397
  */
32220
- checkPlannerOps(ops, held = []) {
33398
+ checkPlannerOps(ops, held2 = []) {
32221
33399
  const runners = this.editRunners();
32222
- const result = applyTaskOps(this.store.planTasks, ops, runners, this.catalog.edit(runners), held);
33400
+ const result = applyTaskOps(this.store.planTasks, ops, runners, this.catalog.edit(runners), held2);
32223
33401
  if (!result.ok || !this.supervisorEditing()) return result;
32224
33402
  const trip = this.supervision.checkEdit(this.store.planTasks, result.tasks);
32225
33403
  if (!trip) return result;
@@ -32389,6 +33567,9 @@ var Session = class {
32389
33567
  onUsageLimitWait: ({ taskId, runner, resumeAt, cancelled }) => {
32390
33568
  if (!cancelled) this.supervision.observe({ kind: "usage_limit", taskId, runner, resumeAt });
32391
33569
  },
33570
+ onModelSwitched: (switched) => {
33571
+ void this.followRunnerModel(switched);
33572
+ },
32392
33573
  onTaskIdle: ({ taskId, idleSince }) => {
32393
33574
  this.supervision.observe({ kind: "task_idle", taskId, detail: `no output since ${idleSince}` });
32394
33575
  },
@@ -32509,10 +33690,12 @@ var Session = class {
32509
33690
  * boundary rewrites `in_progress` to `pending` — nothing is running when a
32510
33691
  * session comes off a file — so a surface that re-reads the plan mid-run must
32511
33692
  * come here instead, or every task it is watching reads as never started.
33693
+ * An archived snapshot carries the task as it is now: a retry or continue
33694
+ * keeps the archive while the live task runs on and spends more.
32512
33695
  */
32513
33696
  get currentPlanState() {
32514
33697
  if (!this.plan) return null;
32515
- const executionLog = this.store.getExecutionLog();
33698
+ const executionLog = this.store.getExecutionLog().map((s) => ({ ...s, ...this.store.get(s.id) }));
32516
33699
  const logged = new Set(executionLog.map((s) => s.id));
32517
33700
  const pendingTasks = this.store.planTasks.filter((t) => !logged.has(t.id));
32518
33701
  const record = approvalRecordOf(this.plan);
@@ -32523,7 +33706,7 @@ var Session = class {
32523
33706
  phase: "executing",
32524
33707
  history: [],
32525
33708
  message: "",
32526
- executionLog: [...executionLog],
33709
+ executionLog,
32527
33710
  pendingTasks,
32528
33711
  goal: this.goal,
32529
33712
  runners: this.plan.runners,
@@ -32693,16 +33876,20 @@ var Session = class {
32693
33876
  opened = this.openTurn(() => this.conversation.start(this.resolveSkillInvocation(goal), () => this.conversationOpening(chosenRunners), options?.signal));
32694
33877
  } catch (err) {
32695
33878
  if (this.opening === pending) this.opening = null;
32696
- for (const held2 of pending.held ?? []) held2.reject(err);
33879
+ for (const held3 of pending.held ?? []) held3.reject(err);
32697
33880
  throw err;
32698
33881
  }
32699
- const held = pending.held ?? [];
33882
+ const held2 = pending.held ?? [];
32700
33883
  pending.held = null;
32701
- for (const h of held) this.turns.follow(h.input, h.forced).then(h.resolve, h.reject);
33884
+ for (const h of held2) this.turns.follow(h.input, h.forced).then(h.resolve, h.reject);
32702
33885
  try {
32703
33886
  const { landed: landed2, effects } = await opened;
32704
33887
  this.autorunSubmitted(effects);
32705
33888
  return landed2;
33889
+ } catch (err) {
33890
+ if (!(err instanceof PlannerUsageLimitError) || !this.plan) throw err;
33891
+ this.turns.waitOnLimit({ source: "user", ...this.resolveSkillInvocation(goal) });
33892
+ return this.plan;
32706
33893
  } finally {
32707
33894
  if (this.opening === pending) this.opening = null;
32708
33895
  }
@@ -32717,10 +33904,10 @@ var Session = class {
32717
33904
  behindOpening(input, forced = false) {
32718
33905
  const pending = this.opening;
32719
33906
  if (!pending) return null;
32720
- const held = pending.held;
32721
- if (!held) return this.turns.follow(input, forced);
33907
+ const held2 = pending.held;
33908
+ if (!held2) return this.turns.follow(input, forced);
32722
33909
  return new Promise((resolve9, reject) => {
32723
- held.push({ input, forced, resolve: resolve9, reject });
33910
+ held2.push({ input, forced, resolve: resolve9, reject });
32724
33911
  });
32725
33912
  }
32726
33913
  /**
@@ -32819,12 +34006,12 @@ var Session = class {
32819
34006
  await this.liveCatalog();
32820
34007
  if (this.conversation.currentTurnId !== turn) return refuse(PLANNER_TURN_ENDED);
32821
34008
  if (!this.plan || this.store.planTasks.length === 0) return refuse("There is no plan to edit yet: submit one with submit_plan.");
32822
- const held = this.conversation.pendingOps();
32823
- 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.");
32824
- const result = this.checkPlannerOps(ops, held);
34009
+ const held2 = this.conversation.pendingOps();
34010
+ if (!held2) return refuse("A whole plan was submitted in this reply and replaces the plan when it ends: make the edit in your next reply.");
34011
+ const result = this.checkPlannerOps(ops, held2);
32825
34012
  if (!result.ok) return { ok: false, errors: result.errors };
32826
- if (!this.conversation.submit({ kind: "task_ops", ops: [...held, ...ops] })) return refuse("No planning turn is open to take the edit.");
32827
- const { coerced } = coerceAssignments(result.tasks, this.catalog.allowlist(), this.catalog.models());
34013
+ if (!this.conversation.submit({ kind: "task_ops", ops: [...held2, ...ops] })) return refuse("No planning turn is open to take the edit.");
34014
+ const { coerced } = coerceAssignments(result.tasks, this.catalog.allowlist(), this.catalog.models(), this.store.planTasks);
32828
34015
  return { ok: true, summary: result.summary, coerced: coerced.map(({ taskId: _id, ...rest }) => rest) };
32829
34016
  }
32830
34017
  /** Whether the planner conversation is live (started and not yet committed to a plan). */
@@ -32861,9 +34048,6 @@ var Session = class {
32861
34048
  autonomousDefault: this.config.autonomousMode,
32862
34049
  autorun: this.userRunSettings().autorun,
32863
34050
  isolatedExecution: await this.runs.plannerLayout(),
32864
- // The planner's own model window, when a cached catalog knows it, so the
32865
- // usage line can show context fill (#49). Unknown stays absent.
32866
- contextWindow: this.modelResolver.contextWindowFor?.(this.config.orchestratorModel),
32867
34051
  fs: this.fsAdapter,
32868
34052
  fetcher: this.fetcher,
32869
34053
  approval: this.approvalPolicy,
@@ -32890,7 +34074,7 @@ var Session = class {
32890
34074
  for (const runner of runnersOf(tasks)) this.editor.admitRunner(runner, this.catalog.known(runner));
32891
34075
  const runners = this.plan.runners;
32892
34076
  const before = this.store.planTasks;
32893
- let coerced = coerceAssignments(tasks, this.catalog.allowlist(), this.catalog.models()).tasks;
34077
+ let coerced = coerceAssignments(tasks, this.catalog.allowlist(), this.catalog.models(), before).tasks;
32894
34078
  if (how === "commit") coerced = keepExecutionState(before, carryPlannedModes(before, coerced, this.catalog.edit(runners)));
32895
34079
  const bySupervisor = this.supervisorEditing();
32896
34080
  const { tasks: stamped, added } = stampOrigin(before, coerced, bySupervisor);
@@ -32903,6 +34087,7 @@ var Session = class {
32903
34087
  this.store.resetForRun({ keepScheduledResumes: true });
32904
34088
  }
32905
34089
  if (bySupervisor) this.approve(added);
34090
+ else if (this.conversation.currentWakeUp === void 0 && this.userRunSettings().autorun) this.approve(writtenTasks(before, coerced));
32906
34091
  return coerced.length;
32907
34092
  }
32908
34093
  preparePlannerTaskUpdates(tasks) {
@@ -32937,6 +34122,11 @@ var Session = class {
32937
34122
  if (changes.mergeAtGatesAutomatically) this.orchestrator.runSettingsChanged();
32938
34123
  }
32939
34124
  async executePlan() {
34125
+ if (this.orchestrator.hasLiveWork) {
34126
+ const outcome = await this.execute({ command: "run_plan" }, { kind: "user" });
34127
+ if (!outcome.ok) throw new TaskControlError(outcome.message);
34128
+ return;
34129
+ }
32940
34130
  this.idlePlan();
32941
34131
  this.approve(flattenTasks(this.store.planTasks).map((t) => t.id));
32942
34132
  await this.runWholePlan("user");
@@ -32958,6 +34148,25 @@ var Session = class {
32958
34148
  await this.orchestrator.approveReview(origin);
32959
34149
  });
32960
34150
  }
34151
+ /**
34152
+ * Execute while a run is under way (#95): the seam has approved every task,
34153
+ * as executing does, and the run takes them in. A run the scheduler was not
34154
+ * driving, one task started on its own, becomes the plan's run, as Execute
34155
+ * would make it.
34156
+ */
34157
+ async joinRun(approved) {
34158
+ const plan = this.plan;
34159
+ const scheduling = this.orchestrator.isScheduling;
34160
+ await this.withSave(async () => {
34161
+ plan.approved = true;
34162
+ if (scheduling) await this.orchestrator.tick();
34163
+ else if (this.orchestrator.isReviewApproved) await this.orchestrator.start("user");
34164
+ else await this.orchestrator.approveReview("user");
34165
+ });
34166
+ const line = approved === 0 ? scheduling ? "The plan is already running, and no task waits for your approval." : "The plan is running: each task starts once its dependencies finish." : approved === 1 ? "Approved 1 task; it starts once its dependencies finish." : `Approved ${approved} tasks; each starts once its dependencies finish.`;
34167
+ this.notice("info", line);
34168
+ return line;
34169
+ }
32961
34170
  async approveReview() {
32962
34171
  if (!this.plan) throw new NoPlanError("review");
32963
34172
  const plan = this.plan;
@@ -32966,9 +34175,12 @@ var Session = class {
32966
34175
  await this.withSave(() => this.orchestrator.approveReview("user"));
32967
34176
  return plan;
32968
34177
  }
34178
+ /** Answers the line saying the task waits for its dependencies, when it was queued behind them rather than started. */
32969
34179
  async forceStartTask(taskId) {
32970
34180
  this.approveTask(taskId);
32971
- await this.startOneTask(taskId, "user");
34181
+ if (!this.plan) return;
34182
+ this.requireTask(taskId);
34183
+ return this.withSave(() => this.orchestrator.forceStartTask(taskId, "user"));
32972
34184
  }
32973
34185
  async startOneTask(taskId, origin) {
32974
34186
  if (!this.plan) return;
@@ -33025,10 +34237,51 @@ var Session = class {
33025
34237
  async resumeNow(taskId) {
33026
34238
  await this.withSave(() => this.orchestrator.resumeNow(taskId, "user"));
33027
34239
  }
34240
+ /**
34241
+ * Keep what waits on the planner's usage limit from being sent at the reset:
34242
+ * it goes with the user's next message instead. False when nothing waits on
34243
+ * a reset.
34244
+ */
34245
+ cancelPlannerResend() {
34246
+ return this.turns.cancelResend();
34247
+ }
33028
34248
  /** Keep a task paused on a usage limit from resuming on its own; see {@link TaskOrchestrator.cancelScheduledResume}. */
33029
34249
  cancelScheduledResume(taskId) {
33030
34250
  return this.saved(() => this.orchestrator.cancelScheduledResume(taskId));
33031
34251
  }
34252
+ /**
34253
+ * The plan follows a model switch the user made in the runner (#106), and
34254
+ * the task's log says what the plan holds once that settled: a write-back
34255
+ * not made — no model command asked for the change, or the edit failed — is
34256
+ * stated, never shown as made. Once the switch's attempt has ended, the plan
34257
+ * may be another one, so nothing of it is said.
34258
+ */
34259
+ async followRunnerModel({ taskId, sessionId, from, model, effort, asked, attemptEnded }) {
34260
+ let kept = asked ? void 0 : "no model command in the chat asked for it";
34261
+ const live = () => this.orchestrator.activeSessionMap.get(taskId) === sessionId;
34262
+ const ended = () => this.taskLogs.note(sessionId, taskId, { type: "model_switched", from, to: model, kept: kept ?? "its attempt had already ended" });
34263
+ if (attemptEnded) return ended();
34264
+ if (asked) {
34265
+ try {
34266
+ if (!await this.editor.followRunnerModel(taskId, model, effort, live) && !live()) return ended();
34267
+ } catch (err) {
34268
+ kept = `the plan could not follow it: ${err instanceof Error ? err.message : String(err)}`;
34269
+ this.notice("warn", `The plan could not follow the model switch of task ${this.store.get(taskId)?.order ?? taskId}: ${err instanceof Error ? err.message : String(err)}`);
34270
+ }
34271
+ }
34272
+ const assigned = this.store.get(taskId)?.assignedModel;
34273
+ if (!kept && assigned?.modelId !== model.model) kept = "the plan did not take the edit";
34274
+ this.taskLogs.note(sessionId, taskId, {
34275
+ type: "model_switched",
34276
+ from,
34277
+ to: model,
34278
+ plan: {
34279
+ ...assigned ? { model: { model: assigned.modelId, label: assigned.modelLabel } } : {},
34280
+ ...assigned?.thinkingEffort ? { effort: assigned.thinkingEffort } : {}
34281
+ },
34282
+ ...kept ? { kept } : {}
34283
+ });
34284
+ }
33032
34285
  /** Said through both channels: a host shows one or the other, never both (the daemon's toasts are no-ops). */
33033
34286
  notice(level, message) {
33034
34287
  this.notifications[level](message);
@@ -33100,8 +34353,8 @@ var Session = class {
33100
34353
  if (!this.plan) return EMPTY_RUN;
33101
34354
  const planned = reduceRun(EMPTY_RUN, { type: "plan_snapshot", plan: surfacePlan({ ...this.plan, tasks: this.store.snapshot() }) });
33102
34355
  const view = reduceRun(planned, this.events.statusMessage());
33103
- const held = this.sentHeld ? reduceRun(view, { type: "plan_edit_held", held: this.sentHeld }) : view;
33104
- return this.sentHandoff ? reduceRun(held, { type: "isolation_handoff", ...this.sentHandoff }) : held;
34356
+ const held2 = this.sentHeld ? reduceRun(view, { type: "plan_edit_held", held: this.sentHeld }) : view;
34357
+ return this.sentHandoff ? reduceRun(held2, { type: "isolation_handoff", ...this.sentHandoff }) : held2;
33105
34358
  }
33106
34359
  /** The ancestors a task waits on at its merge gate (ADR-0020); empty when it waits for no Merge all. */
33107
34360
  mergeGate(taskId) {
@@ -33260,6 +34513,7 @@ var Session = class {
33260
34513
  if (adopting) this.dropPlanScope();
33261
34514
  this.plan = plan;
33262
34515
  if (adopting) this.supervision.restore(plan.supervision);
34516
+ if (adopting) this.turns.restoreResend();
33263
34517
  this.goal = goal;
33264
34518
  this.workspace = workspace;
33265
34519
  plan.approved ??= plan.status !== "draft" && plan.status !== "rejected";
@@ -33502,6 +34756,7 @@ function syncTranscript(view, { plan, turnId }) {
33502
34756
  if (isNew && entry.kind === "wake_up") next = takePendingInputs(next, void 0, [entry.content]).view;
33503
34757
  if (isNew && entry.kind === "plan_generated") next = markPlan(next, entry.content, i === committed ? turnId : void 0);
33504
34758
  if (isNew && entry.kind === "system") next = append(next, (id) => settledMessage(id, "system", entry.content));
34759
+ if (isNew && entry.kind === "error") next = append(next, (id) => settledMessage(id, "error", entry.content));
33505
34760
  if (isNew && entry.kind === "compaction") next = markCompaction(next, entry.content);
33506
34761
  if (latest === void 0 || entry.timestamp > latest) latest = entry.timestamp;
33507
34762
  }
@@ -33582,6 +34837,8 @@ function roleOf(entry) {
33582
34837
  return "wake_up";
33583
34838
  case "supervisor":
33584
34839
  return "supervisor";
34840
+ case "error":
34841
+ return "error";
33585
34842
  default:
33586
34843
  return entry.role === "user" ? "user" : "planner";
33587
34844
  }
@@ -33846,13 +35103,18 @@ function blockedBy(task, tasks) {
33846
35103
  if (!tasks?.length || !NOT_STARTED.has(task.status)) return [];
33847
35104
  return blockingFailures(asBlocking(tasks)).get(task.id) ?? [];
33848
35105
  }
35106
+ function flagOf(task, scope) {
35107
+ const issue = scope && task.assignedRunner !== void 0 ? assignmentIssue({ ...task, assignedRunner: task.assignedRunner }, scope) : null;
35108
+ return issue ? { assignmentFlag: assignmentIssueText(issue), assignmentConsequence: assignmentConsequence(task) } : { assignmentFlag: null, assignmentConsequence: null };
35109
+ }
33849
35110
  function taskRowView(task, context = {}) {
33850
35111
  const parent = context.parent ?? void 0;
33851
35112
  const isolation = task.isolation && task.isolation.state !== "none" ? task.isolation : null;
33852
- const latestUsage = task.attemptUsage?.at(-1);
35113
+ const spend = taskUsageSummary(task);
35114
+ const latestAttempt = task.attemptUsage?.at(-1);
33853
35115
  const blocked = blockedBy(task, context.tasks);
33854
35116
  return {
33855
- usage: latestUsage ? { ...usageLine(latestUsage), attempt: latestUsage.attempt, wallTimeMs: latestUsage.wallTimeMs, byModel: latestUsage.byModel } : task.type === "ai" && ["completed", "failed", "in_progress", "awaiting_user"].includes(task.status) ? { totals: {}, attempt: 0, byModel: [] } : null,
35117
+ usage: latestAttempt ? { totals: spend.totals, ...spend.contextFill ? { contextFill: spend.contextFill } : {}, attempt: latestAttempt.attempt } : task.type === "ai" && ["completed", "failed", "in_progress", "awaiting_user"].includes(task.status) ? { totals: {}, attempt: 0 } : null,
33856
35118
  orderLabel: taskOrderLabel(task, parent),
33857
35119
  kind: task.type === "user" ? "user" : task.ops ? "ops" : "ai",
33858
35120
  status: blocked.length > 0 ? "blocked" : taskStatusKind(task),
@@ -33861,6 +35123,7 @@ function taskRowView(task, context = {}) {
33861
35123
  resumes: resumeLabel(task, context.now),
33862
35124
  approvals: approvalLabel(task.awaitingApproval),
33863
35125
  waitsForApproval: task.waitsForApproval && NOT_STARTED.has(task.status) ? WAITS_FOR_APPROVAL_LABEL : null,
35126
+ ...flagOf(task, context.runnerScope),
33864
35127
  autonomous: task.type === "ai" && context.modes?.find((m) => m.id === task.taskMode)?.autonomous === true,
33865
35128
  repos: namedRepos(isolation?.repos),
33866
35129
  conflict: isolation?.state === "conflict" ? { repo: namedRepo(isolation.conflictRepo), files: filesOf(isolation.conflictFiles) } : null,
@@ -34066,6 +35329,7 @@ var StructuredSession = class extends AbstractRunnerSession {
34066
35329
  messageCount = 0;
34067
35330
  interrupting = null;
34068
35331
  lastSessionId = null;
35332
+ lastSessionCost = null;
34069
35333
  structuredEmitter = new import_events2.EventEmitter();
34070
35334
  permissionCount = 0;
34071
35335
  /**
@@ -34270,6 +35534,9 @@ var StructuredSession = class extends AbstractRunnerSession {
34270
35534
  nativeSessionId() {
34271
35535
  return this.adapter?.nativeSessionId() ?? this.lastSessionId;
34272
35536
  }
35537
+ sessionCost() {
35538
+ return this.adapter?.sessionCost?.() ?? this.lastSessionCost;
35539
+ }
34273
35540
  answerPermission(id, decision) {
34274
35541
  const open = this.permissions.get(id);
34275
35542
  if (!open) return false;
@@ -34317,6 +35584,7 @@ var StructuredSession = class extends AbstractRunnerSession {
34317
35584
  */
34318
35585
  async restartInterrupted(turn) {
34319
35586
  const resumeSessionId = this.nativeSessionId() ?? this.startOptions.resumeSessionId;
35587
+ const resumeCost = this.sessionCost() ?? this.startOptions.resumeCost;
34320
35588
  this.generation += 1;
34321
35589
  turn.abort.abort();
34322
35590
  if (this.adapter) {
@@ -34326,7 +35594,7 @@ var StructuredSession = class extends AbstractRunnerSession {
34326
35594
  for (const message of this.queue) this.requeue(message);
34327
35595
  try {
34328
35596
  this.revokeTools();
34329
- await this.startAttached({ ...this.startOptions, resumeSessionId });
35597
+ await this.startAttached({ ...this.startOptions, resumeSessionId, resumeCost });
34330
35598
  } catch (err) {
34331
35599
  this.text.line(`Could not restart ${this.launch.runner} after the interrupt: ${err instanceof Error ? err.message : String(err)}`);
34332
35600
  this.withdrawPermissions();
@@ -34353,6 +35621,7 @@ var StructuredSession = class extends AbstractRunnerSession {
34353
35621
  return;
34354
35622
  }
34355
35623
  this.lastSessionId = adapter.nativeSessionId() ?? this.lastSessionId;
35624
+ this.lastSessionCost = adapter.sessionCost?.() ?? this.lastSessionCost;
34356
35625
  this.withdrawPermissions();
34357
35626
  this.baseHandleExit(code);
34358
35627
  });
@@ -34390,6 +35659,7 @@ var StructuredSession = class extends AbstractRunnerSession {
34390
35659
  this.handleEvent({ type: "error", message: err instanceof Error ? err.message : String(err) });
34391
35660
  }).then(() => {
34392
35661
  this.lastSessionId = adapter.nativeSessionId() ?? this.lastSessionId;
35662
+ this.lastSessionCost = adapter.sessionCost?.() ?? this.lastSessionCost;
34393
35663
  if (generation2 === this.generation) this.endTurn(turn, turn.reason);
34394
35664
  });
34395
35665
  this.offerNext();
@@ -34516,6 +35786,11 @@ var StructuredSession = class extends AbstractRunnerSession {
34516
35786
  case "error":
34517
35787
  this.text.failure(event.message);
34518
35788
  break;
35789
+ // The plan follows the switch (#106), so a restart after an interrupt
35790
+ // runs what the plan now says rather than what this attempt started on.
35791
+ case "model_switched":
35792
+ this.startOptions = { ...this.startOptions, model: event.to.model };
35793
+ break;
34519
35794
  default:
34520
35795
  break;
34521
35796
  }
@@ -34596,7 +35871,8 @@ var StructuredRunner = class extends AbstractRunner {
34596
35871
  model: opts.modelId,
34597
35872
  mode: opts.mode ?? "default",
34598
35873
  flags: resolveTaskRunnerFlags(manifest, { mode: opts.mode ?? "default", model: opts.modelId, thinkingEffort: opts.thinkingEffort }),
34599
- resumeSessionId: opts.resumeSessionId
35874
+ resumeSessionId: opts.resumeSessionId,
35875
+ resumeCost: opts.resumeCost
34600
35876
  },
34601
35877
  interruptGraceMs: this.interruptGraceMs,
34602
35878
  tools: { server: this.mcp, scope: { sessionId: opts.planSessionId ?? "", taskId: opts.taskId, attempt: opts.attempt ?? 1 } },
@@ -34899,6 +36175,7 @@ var PlannerModelMemory = class {
34899
36175
  PlannerNotReadyError,
34900
36176
  PlannerTurnDiscardedError,
34901
36177
  PlannerTurnStoppedError,
36178
+ PlannerUsageLimitError,
34902
36179
  REFUSED_COMMANDS,
34903
36180
  RUN_SETTINGS_DEFAULTS,
34904
36181
  RunSettingsError,
@@ -34955,6 +36232,8 @@ var PlannerModelMemory = class {
34955
36232
  approvalScopes,
34956
36233
  assertWorkspaceExists,
34957
36234
  assertWorkspaceIsProject,
36235
+ assignmentIssue,
36236
+ assignmentIssueText,
34958
36237
  augmentedPath,
34959
36238
  automergeSummary,
34960
36239
  autonomyLevelLabel,
@@ -34971,6 +36250,7 @@ var PlannerModelMemory = class {
34971
36250
  canSetDependencies,
34972
36251
  canSplitTask,
34973
36252
  capConflictFiles,
36253
+ checkoutRule,
34974
36254
  classifyCommand,
34975
36255
  classifyOutcome,
34976
36256
  clearAugmentedPathCache,
@@ -34984,6 +36264,7 @@ var PlannerModelMemory = class {
34984
36264
  commandAllowedBy,
34985
36265
  configuredProviders,
34986
36266
  continuability,
36267
+ countedSessionCost,
34987
36268
  createAiService,
34988
36269
  createEmptyPlan,
34989
36270
  createSession,
@@ -35047,6 +36328,7 @@ var PlannerModelMemory = class {
35047
36328
  isMeasured,
35048
36329
  isOpenAiProvider,
35049
36330
  isPlannerBackend,
36331
+ isRepoGroup,
35050
36332
  isRunnerApproval,
35051
36333
  isSupervisorLevel,
35052
36334
  isTaskRunning,
@@ -35069,6 +36351,7 @@ var PlannerModelMemory = class {
35069
36351
  markdownHtml,
35070
36352
  markdownTokens,
35071
36353
  mcpClientConfig,
36354
+ mergeTarget,
35072
36355
  migrateOldConfigDir,
35073
36356
  migratePlanIsolation,
35074
36357
  migratePlanState,
@@ -35118,6 +36401,7 @@ var PlannerModelMemory = class {
35118
36401
  removedPluginNotice,
35119
36402
  renderTaskQueryAnswer,
35120
36403
  replayTaskLog,
36404
+ reposWithWork,
35121
36405
  researchShellWarning,
35122
36406
  researchToolsPath,
35123
36407
  resetInstalledPlanner,
@@ -35131,6 +36415,7 @@ var PlannerModelMemory = class {
35131
36415
  resolveWithin,
35132
36416
  resumeLabel,
35133
36417
  resumeTime,
36418
+ resumedSessionWindow,
35134
36419
  runSettingsOf,
35135
36420
  runnerForProvider,
35136
36421
  runnerModesFrom,
@@ -35171,6 +36456,7 @@ var PlannerModelMemory = class {
35171
36456
  taskRowView,
35172
36457
  taskStartedNotice,
35173
36458
  taskStatusKind,
36459
+ taskUsageSummary,
35174
36460
  textHasTaskOps,
35175
36461
  textHasTaskQuery,
35176
36462
  titledRefs,