@ordewell/core 0.10.8 → 0.10.10

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 (40) hide show
  1. package/dist/{IFileSystem-C4_mpfze.d.ts → IFileSystem-1PqRCjhu.d.mts} +23 -3
  2. package/dist/{IFileSystem-DUgeWWQN.d.mts → IFileSystem-lTRL_G9Z.d.ts} +23 -3
  3. package/dist/{ModeResolver-4qNjxcDs.d.mts → ModeResolver-6TQXVsde.d.mts} +1 -1
  4. package/dist/{ModeResolver-DGK2rWHE.d.ts → ModeResolver-CoJAJD-f.d.ts} +1 -1
  5. package/dist/{Task-C0o4i3Nn.d.mts → Task-CfBM0wkV.d.mts} +100 -24
  6. package/dist/{Task-C0o4i3Nn.d.ts → Task-CfBM0wkV.d.ts} +100 -24
  7. package/dist/{chunk-H7QX32BH.mjs → chunk-24EL4Z2W.mjs} +2 -2
  8. package/dist/{chunk-DMG4ISUG.mjs → chunk-ESKK5I4F.mjs} +145 -21
  9. package/dist/chunk-ESKK5I4F.mjs.map +1 -0
  10. package/dist/{chunk-LTHKFBKM.mjs → chunk-KWS3VEER.mjs} +3 -3
  11. package/dist/{chunk-EEE2GBLG.mjs → chunk-PFH7WFIB.mjs} +1 -1
  12. package/dist/chunk-PFH7WFIB.mjs.map +1 -0
  13. package/dist/index.d.mts +145 -20
  14. package/dist/index.d.ts +145 -20
  15. package/dist/index.js +668 -112
  16. package/dist/index.js.map +1 -1
  17. package/dist/index.mjs +537 -96
  18. package/dist/index.mjs.map +1 -1
  19. package/dist/parsing.d.mts +2 -2
  20. package/dist/parsing.d.ts +2 -2
  21. package/dist/parsing.js.map +1 -1
  22. package/dist/parsing.mjs +3 -3
  23. package/dist/plan-utils.d.mts +4 -4
  24. package/dist/plan-utils.d.ts +4 -4
  25. package/dist/plan-utils.js +144 -18
  26. package/dist/plan-utils.js.map +1 -1
  27. package/dist/plan-utils.mjs +22 -4
  28. package/dist/{runSettingsText-BgOmLxdw.d.mts → runSettingsText-C1i9wBG1.d.mts} +61 -21
  29. package/dist/{runSettingsText-BgXvajj2.d.ts → runSettingsText-CWNrmF5U.d.ts} +61 -21
  30. package/dist/testing.d.mts +7 -2
  31. package/dist/testing.d.ts +7 -2
  32. package/dist/testing.js +11 -0
  33. package/dist/testing.js.map +1 -1
  34. package/dist/testing.mjs +12 -1
  35. package/dist/testing.mjs.map +1 -1
  36. package/package.json +1 -1
  37. package/dist/chunk-DMG4ISUG.mjs.map +0 -1
  38. package/dist/chunk-EEE2GBLG.mjs.map +0 -1
  39. /package/dist/{chunk-H7QX32BH.mjs.map → chunk-24EL4Z2W.mjs.map} +0 -0
  40. /package/dist/{chunk-LTHKFBKM.mjs.map → chunk-KWS3VEER.mjs.map} +0 -0
package/dist/index.js CHANGED
@@ -203,7 +203,10 @@ __export(index_exports, {
203
203
  collectProviderCredentials: () => collectProviderCredentials,
204
204
  commandAllowedBy: () => commandAllowedBy,
205
205
  configuredProviders: () => configuredProviders,
206
+ contextMeterText: () => contextMeterText,
207
+ contextMeterTitle: () => contextMeterTitle,
206
208
  continuability: () => continuability,
209
+ costText: () => costText,
207
210
  countedSessionCost: () => countedSessionCost,
208
211
  createAiService: () => createAiService,
209
212
  createEmptyPlan: () => createEmptyPlan,
@@ -246,7 +249,9 @@ __export(index_exports, {
246
249
  flattenTasksWithParents: () => flattenTasksWithParents,
247
250
  followTaskLog: () => followTaskLog,
248
251
  followTurn: () => followTurn,
252
+ formatCost: () => formatCost,
249
253
  formatSearchOutput: () => formatSearchOutput,
254
+ formatTokenCount: () => formatTokenCount,
250
255
  fromTranscript: () => fromTranscript,
251
256
  getLatestSession: () => getLatestSession,
252
257
  getProviderMeta: () => getProviderMeta,
@@ -298,6 +303,7 @@ __export(index_exports, {
298
303
  migratePlanState: () => migratePlanState,
299
304
  mintDaemonToken: () => mintDaemonToken,
300
305
  mintSessionId: () => mintSessionId,
306
+ modelCostTitle: () => modelCostTitle,
301
307
  namedRepos: () => namedRepos,
302
308
  newTaskFields: () => newTaskFields,
303
309
  normalizeAgentArgs: () => normalizeAgentArgs,
@@ -383,6 +389,7 @@ __export(index_exports, {
383
389
  stopTurn: () => stopTurn,
384
390
  stripAnsi: () => stripAnsi,
385
391
  stripModelPrefix: () => stripModelPrefix,
392
+ subagentCostTitle: () => subagentCostTitle,
386
393
  subtreeIds: () => subtreeIds,
387
394
  summarizeToolCall: () => summarizeToolCall,
388
395
  supervisorSummary: () => supervisorSummary,
@@ -401,6 +408,7 @@ __export(index_exports, {
401
408
  taskStartedNotice: () => taskStartedNotice,
402
409
  taskStatusKind: () => taskStatusKind,
403
410
  taskUsageSummary: () => taskUsageSummary,
411
+ tasksTotalText: () => tasksTotalText,
404
412
  textHasTaskOps: () => textHasTaskOps,
405
413
  textHasTaskQuery: () => textHasTaskQuery,
406
414
  titledRefs: () => titledRefs,
@@ -418,6 +426,7 @@ __export(index_exports, {
418
426
  unsendAll: () => unsendAll,
419
427
  unsendLatest: () => unsendLatest,
420
428
  usageLine: () => usageLine,
429
+ usageText: () => usageText,
421
430
  waitsForApproval: () => waitsForApproval,
422
431
  walkPlan: () => walkPlan,
423
432
  wellKnownBinDirs: () => wellKnownBinDirs,
@@ -880,25 +889,66 @@ function addUsage(totals, record) {
880
889
  }
881
890
  if (record.reportedCost) {
882
891
  const { amount, currency } = record.reportedCost;
883
- next.reportedCost = { ...totals.reportedCost, [currency]: (totals.reportedCost?.[currency] ?? 0) + amount };
892
+ const provenance = record.reportedCost.kind || record.sessionBilling ? {
893
+ kind: record.reportedCost.kind ?? "unknown",
894
+ billing: record.sessionBilling?.type ?? "unknown",
895
+ ...record.reportedCost.basis ? { basis: record.reportedCost.basis } : {}
896
+ } : void 0;
897
+ addCost(next, currency, amount, provenance);
884
898
  }
885
899
  return next;
886
900
  }
901
+ function addCost(totals, currency, amount, provenance) {
902
+ const hadCost = totals.reportedCost?.[currency] !== void 0;
903
+ const previous = totals.costProvenance?.[currency];
904
+ totals.reportedCost = { ...totals.reportedCost, [currency]: (totals.reportedCost?.[currency] ?? 0) + amount };
905
+ if (!previous && !provenance) return;
906
+ const unknown = { kind: "unknown", billing: "unknown" };
907
+ const a = previous ?? unknown;
908
+ const b = provenance ?? unknown;
909
+ const merged = !hadCost ? b : {
910
+ kind: a.kind === "estimate" || b.kind === "estimate" ? "estimate" : a.kind === "billed" && b.kind === "billed" ? "billed" : "unknown",
911
+ billing: a.billing === b.billing ? a.billing : "mixed",
912
+ ...a.basis && a.basis === b.basis ? { basis: a.basis } : {}
913
+ };
914
+ totals.costProvenance = { ...totals.costProvenance, [currency]: merged };
915
+ }
887
916
  function addPlannerUsage(usage, record) {
888
- const next = { ...usage, totals: addUsage(usage.totals, record), byModel: addModelUsage(usage.byModel ?? [], record) };
917
+ if (record.compacted) {
918
+ if (record.subagentId || record.aggregate) return usage;
919
+ const next2 = { ...usage };
920
+ if (record.source !== void 0) next2.source = record.source;
921
+ if (record.model !== void 0) {
922
+ if (record.source !== usage.source || record.model !== usage.model) delete next2.contextWindow;
923
+ next2.model = record.model;
924
+ }
925
+ if (record.contextTokens !== void 0) next2.lastPromptTokens = record.contextTokens;
926
+ else delete next2.lastPromptTokens;
927
+ if (record.contextWindow !== void 0) next2.contextWindow = record.contextWindow > 0 ? record.contextWindow : void 0;
928
+ return next2;
929
+ }
930
+ if (record.sessionBilling && !record.reportedCost && record.inputTokens === void 0 && record.outputTokens === void 0 && record.cachedInputTokens === void 0 && record.contextWindow === void 0) {
931
+ return record.subagentId ? usage : { ...usage, sessionBilling: record.sessionBilling, source: record.source };
932
+ }
933
+ record = { ...record, sessionBilling: record.sessionBilling ?? (usage.source === void 0 || record.source === usage.source ? usage.sessionBilling : void 0) };
934
+ const next = { ...usage, ...!record.subagentId && record.sessionBilling ? { sessionBilling: record.sessionBilling } : {}, totals: addUsage(usage.totals, record), byModel: addModelUsage(usage.byModel ?? [], record) };
889
935
  if (record.subagentId) {
890
936
  next.bySubagent = { ...usage.bySubagent, [record.subagentId]: addUsage(usage.bySubagent?.[record.subagentId] ?? {}, record) };
891
937
  return next;
892
938
  }
893
- if (!record.aggregate) {
894
- if (record.source !== usage.source || record.model !== usage.model) {
895
- delete next.lastPromptTokens;
896
- delete next.contextWindow;
897
- }
898
- next.source = record.source;
899
- next.model = record.model;
939
+ if (record.aggregate) {
940
+ if (record.source !== usage.source && !record.sessionBilling) delete next.sessionBilling;
941
+ return next;
942
+ }
943
+ if (record.source !== usage.source || record.model !== usage.model) {
944
+ delete next.lastPromptTokens;
945
+ delete next.contextWindow;
900
946
  }
901
- if (record.inputTokens !== void 0) next.lastPromptTokens = record.inputTokens;
947
+ if (record.source !== usage.source && !record.sessionBilling) delete next.sessionBilling;
948
+ next.source = record.source;
949
+ next.model = record.model;
950
+ const used = record.contextTokens ?? record.inputTokens;
951
+ if (used !== void 0) next.lastPromptTokens = used;
902
952
  if (record.contextWindow !== void 0) next.contextWindow = record.contextWindow > 0 ? record.contextWindow : void 0;
903
953
  return next;
904
954
  }
@@ -909,6 +959,7 @@ function usageLine(usage) {
909
959
  const contextFill = plannerContextFill(usage);
910
960
  return {
911
961
  totals: usage.totals,
962
+ ...usage.sessionBilling ? { sessionBilling: usage.sessionBilling } : {},
912
963
  ...usage.byModel ? { byModel: usage.byModel } : {},
913
964
  ...usage.model ? { model: usage.model } : {},
914
965
  ...usage.bySubagent ? { bySubagent: usage.bySubagent } : {},
@@ -916,8 +967,8 @@ function usageLine(usage) {
916
967
  };
917
968
  }
918
969
  function plannerContextFill(usage) {
919
- if (usage.lastPromptTokens === void 0 || usage.contextWindow === void 0 || usage.contextWindow <= 0) return void 0;
920
- return { usedTokens: usage.lastPromptTokens, windowTokens: usage.contextWindow };
970
+ if (usage.lastPromptTokens === void 0) return void 0;
971
+ return { usedTokens: usage.lastPromptTokens, ...usage.contextWindow !== void 0 && usage.contextWindow > 0 ? { windowTokens: usage.contextWindow } : {} };
921
972
  }
922
973
  function addModelUsage(models, record) {
923
974
  const index = models.findIndex((m) => m.source === record.source && m.model === record.model);
@@ -929,13 +980,14 @@ function startAttemptUsage(attempt, runner, startedAt) {
929
980
  }
930
981
  function addAttemptUsage(usage, record, now) {
931
982
  const folded = addPlannerUsage(usage, record);
932
- return { ...usage, ...folded, lastPromptTokens: folded.lastPromptTokens, contextWindow: folded.contextWindow, wallTimeMs: Math.max(0, Date.parse(now) - Date.parse(usage.startedAt)) };
983
+ return { ...usage, ...folded, lastPromptTokens: folded.lastPromptTokens, contextWindow: folded.contextWindow, wallTimeMs: Math.max(0, Date.parse(usage.endedAt ?? now) - Date.parse(usage.startedAt)) };
933
984
  }
934
985
  function endAttemptUsage(usage, now, runnerSessionCost, sessionId) {
986
+ const endedAt = usage.endedAt ?? now;
935
987
  return {
936
988
  ...usage,
937
- endedAt: now,
938
- wallTimeMs: Math.max(0, Date.parse(now) - Date.parse(usage.startedAt)),
989
+ endedAt,
990
+ wallTimeMs: Math.max(0, Date.parse(endedAt) - Date.parse(usage.startedAt)),
939
991
  ...sessionId ? { sessionId } : {},
940
992
  ...runnerSessionCost ? { runnerSessionCost } : {}
941
993
  };
@@ -948,8 +1000,8 @@ function resumedSessionWindow(attempts, runner, attempt, sessionId) {
948
1000
  return last ? { source: last.source, model: last.model, contextWindow: last.contextWindow } : void 0;
949
1001
  }
950
1002
  function addUsageTotals(totals, addition) {
951
- let next = addUsage(totals, { source: "", inputTokens: addition.inputTokens, outputTokens: addition.outputTokens, cachedInputTokens: addition.cachedInputTokens });
952
- for (const [currency, amount] of Object.entries(addition.reportedCost ?? {})) next = addUsage(next, { source: "", reportedCost: { currency, amount } });
1003
+ const next = addUsage(totals, { source: "", inputTokens: addition.inputTokens, outputTokens: addition.outputTokens, cachedInputTokens: addition.cachedInputTokens });
1004
+ for (const [currency, amount] of Object.entries(addition.reportedCost ?? {})) addCost(next, currency, amount, addition.costProvenance?.[currency]);
953
1005
  return next;
954
1006
  }
955
1007
  function planUsage(tasks) {
@@ -984,6 +1036,71 @@ function taskUsageSummary(task) {
984
1036
  ...contextFill ? { contextFill } : {}
985
1037
  };
986
1038
  }
1039
+ function formatTokenCount(n) {
1040
+ if (n < 1e3) return String(n);
1041
+ if (n < 1e4) return `${(n / 1e3).toFixed(1).replace(/\.0$/, "")}k`;
1042
+ if (n < 999500) return `${Math.round(n / 1e3)}k`;
1043
+ return `${(n / 1e6).toFixed(1).replace(/\.0$/, "")}M`;
1044
+ }
1045
+ function formatCost(currency, amount) {
1046
+ const value = amount >= 1 ? amount.toFixed(2) : (([whole, frac = ""]) => `${whole}.${frac.padEnd(2, "0")}`)(amount.toFixed(4).replace(/0+$/, "").split("."));
1047
+ return currency.toLowerCase() === "usd" ? `$${value}` : `${value} ${currency.toUpperCase()}`;
1048
+ }
1049
+ function contextMeterText(fill) {
1050
+ if (!fill) return null;
1051
+ const used = `ctx ${formatTokenCount(fill.usedTokens)}`;
1052
+ if (!fill.windowTokens) return used;
1053
+ const share = fill.usedTokens / fill.windowTokens * 100;
1054
+ return `${used} (${share < 1 ? "<1" : Math.round(share)}%)`;
1055
+ }
1056
+ function contextMeterTitle(fill) {
1057
+ const used = fill.usedTokens.toLocaleString("en-US");
1058
+ return fill.windowTokens ? `${used} of ${fill.windowTokens.toLocaleString("en-US")} tokens` : `${used} tokens`;
1059
+ }
1060
+ function costParts(totals) {
1061
+ const rank = (currency) => currency.toLowerCase() === "usd" ? "" : currency.toUpperCase();
1062
+ return Object.entries(totals.reportedCost ?? {}).sort(([a], [b]) => rank(a).localeCompare(rank(b))).map(([currency, amount]) => {
1063
+ const provenance = totals.costProvenance?.[currency];
1064
+ const equivalent = provenance?.billing === "subscription";
1065
+ return `${provenance?.kind === "estimate" || equivalent ? "\u2248" : ""}${formatCost(currency, amount)}${equivalent ? " API-equivalent" : ""}`;
1066
+ });
1067
+ }
1068
+ function costText(totals) {
1069
+ const parts = costParts(totals);
1070
+ return parts.length > 0 ? parts.join(" \xB7 ") : null;
1071
+ }
1072
+ function usageText(usage) {
1073
+ const parts = [contextMeterText(usage.contextFill), costText(usage.totals)].filter((p) => p !== null);
1074
+ return parts.length > 0 ? parts.join(" \xB7 ") : null;
1075
+ }
1076
+ function modelCostTitle(byModel) {
1077
+ const lines2 = byModel.flatMap((m) => {
1078
+ const cost = costParts(m.totals);
1079
+ const descriptions = [...new Set(Object.values(m.totals.costProvenance ?? {}).map((p) => {
1080
+ const source = m.source === "claude-code" ? "Claude Code" : m.source === "opencode" ? "OpenCode" : m.source === "openrouter" ? "OpenRouter" : m.source;
1081
+ const basis = p.basis === "list" ? " at list price" : p.basis === "managed" ? " at managed price" : "";
1082
+ const kind = p.kind === "estimate" ? `${source}'s estimate${basis}` : p.kind === "billed" ? `Amount billed by ${source}` : "Reported cost";
1083
+ return `${kind}${p.billing === "subscription" ? "; not billed on a subscription" : ""}`;
1084
+ }))];
1085
+ return cost.length > 0 ? [`${m.source} / ${m.model ?? "unattributed"}: ${cost.join(" \xB7 ")}${descriptions.length ? ` \u2014 ${descriptions.join("; ")}` : ""}`] : [];
1086
+ });
1087
+ return lines2.length > 0 ? lines2.join("\n") : void 0;
1088
+ }
1089
+ function subagentCostTitle(bySubagent) {
1090
+ return Object.entries(bySubagent).map(([id, totals]) => `${id}: ${costParts(totals).join(" \xB7 ") || "reported no cost"}`).join("\n");
1091
+ }
1092
+ function runnersOf(tasks) {
1093
+ return tasks.flatMap((task) => [...task.attemptUsage ?? [], ...runnersOf(task.subtasks ?? [])]);
1094
+ }
1095
+ function tasksTotalText(tasks) {
1096
+ const attempts = runnersOf(tasks);
1097
+ if (attempts.length === 0) return null;
1098
+ const costed = (a) => Object.keys(a.totals.reportedCost ?? {}).length > 0;
1099
+ const costless = [...new Set(attempts.filter((a) => a.endedAt !== void 0 && isMeasured(a.totals)).map((a) => a.runner))].filter((runner) => !attempts.some((a) => a.runner === runner && costed(a)));
1100
+ const note = costless.length === 0 ? [] : [`${costless.length === 1 ? costless[0] : `${costless.slice(0, -1).join(", ")} and ${costless.at(-1)}`} ${costless.length === 1 ? "reports" : "report"} no cost`];
1101
+ const parts = [...costParts(planUsage(tasks).totals), ...note];
1102
+ return parts.length > 0 ? `Tasks total: ${parts.join(" \xB7 ")}` : null;
1103
+ }
987
1104
 
988
1105
  // src/models/TaskLog.ts
989
1106
  var HEAD_LINES = 60;
@@ -7523,7 +7640,7 @@ var RunnerApprovals = class {
7523
7640
  },
7524
7641
  stop: (sessionId) => {
7525
7642
  this.denySession(sessionId);
7526
- runner.stop(sessionId);
7643
+ return runner.stop(sessionId);
7527
7644
  },
7528
7645
  stopAll: () => {
7529
7646
  this.approvals.clear(isRunnerApproval);
@@ -10811,6 +10928,8 @@ var RunnerProcess = class {
10811
10928
  markEnded;
10812
10929
  child = null;
10813
10930
  exitStatus = null;
10931
+ /** The process itself reported its end, before its pipes drained; see {@link hasExited}. */
10932
+ exitReported = false;
10814
10933
  stderrTail = "";
10815
10934
  spawnEnv = {};
10816
10935
  /** Stdout readers' end-of-stream flushes, for an end the pipe itself never reports. */
@@ -10861,6 +10980,7 @@ var RunnerProcess = class {
10861
10980
  });
10862
10981
  child.on("close", (code, signal) => this.end({ code, signal }));
10863
10982
  child.once("exit", (code, signal) => {
10983
+ this.exitReported = true;
10864
10984
  const grace = setTimeout(() => {
10865
10985
  for (const flush of [...this.stdoutEnds]) flush();
10866
10986
  this.end({ code, signal });
@@ -10915,6 +11035,15 @@ ${err.message}`);
10915
11035
  get exit() {
10916
11036
  return this.exitStatus;
10917
11037
  }
11038
+ /**
11039
+ * Whether the process has ended, for any reason. Unlike {@link exit} this is
11040
+ * true from its own 'exit', not only once 'close' has drained the pipes: a
11041
+ * runner that has exited can take no further turn even while a descendant
11042
+ * holds its stdout or stderr.
11043
+ */
11044
+ get hasExited() {
11045
+ return this.exitStatus !== null || this.exitReported;
11046
+ }
10918
11047
  /** The exit code a listener is told: -1 for a signal or a spawn failure. */
10919
11048
  get exitCode() {
10920
11049
  return this.exitStatus?.code ?? -1;
@@ -11113,6 +11242,9 @@ var StdioAgentAdapter = class {
11113
11242
  nativeSessionId() {
11114
11243
  return this.sessionId;
11115
11244
  }
11245
+ get exited() {
11246
+ return this.runner.hasExited;
11247
+ }
11116
11248
  onProcessExit(listener) {
11117
11249
  const fire = () => listener(this.runner.exitCode, this.exitMessage());
11118
11250
  if (this.runner.exit) queueMicrotask(fire);
@@ -11613,6 +11745,14 @@ function canUseToolAnswer({ input, suggestions }, decision) {
11613
11745
  }
11614
11746
  var MODEL_COMMAND = "model";
11615
11747
  var SUBAGENT_TOOLS = /* @__PURE__ */ new Set(["Agent", "Task"]);
11748
+ var VARIANT_SUFFIX = /^(.*?)(\[[^\]]*\])?$/;
11749
+ function sameModel(a, b) {
11750
+ if (a === void 0 || b === void 0) return false;
11751
+ if (a === b) return true;
11752
+ const [, aBase, aVariant] = VARIANT_SUFFIX.exec(a) ?? [];
11753
+ const [, bBase, bVariant] = VARIANT_SUFFIX.exec(b) ?? [];
11754
+ return aBase === bBase && (aVariant === void 0 || bVariant === void 0 || aVariant === bVariant);
11755
+ }
11616
11756
  function usageRecord(usage, model, subagentId) {
11617
11757
  const record = {
11618
11758
  source: "claude-code",
@@ -11621,6 +11761,7 @@ function usageRecord(usage, model, subagentId) {
11621
11761
  if (model) record.model = model;
11622
11762
  if (usage.output_tokens !== void 0) record.outputTokens = usage.output_tokens;
11623
11763
  if (subagentId) record.subagentId = subagentId;
11764
+ else if (record.inputTokens !== void 0) record.contextTokens = record.inputTokens + (record.outputTokens ?? 0);
11624
11765
  return record;
11625
11766
  }
11626
11767
  function subagentFinalCall(result) {
@@ -11670,16 +11811,27 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
11670
11811
  replyRuns = new ReplyRuns();
11671
11812
  /** The model answering the planner's current message, and the message's id, as its `message_start` named them. */
11672
11813
  plannerModel;
11814
+ /**
11815
+ * The id the turn runs as, as `init` names it. A `[1m]` variant is the
11816
+ * selected id, while a message names the API's canonical id without it.
11817
+ */
11818
+ sessionModel;
11819
+ billing;
11673
11820
  streamingMessage;
11674
11821
  /** The message the last `assistant` line was part of, and how many of its blocks the lines have carried. */
11675
11822
  lineMessage = null;
11676
11823
  /**
11677
- * The session's `total_cost_usd` as last counted. A resume starts from what
11678
- * was counted of that session before, and is undefined without it: the CLI
11679
- * restores the session's running total, which holds turns counted earlier.
11824
+ * What has been counted of the session's running cost: a resume starts from
11825
+ * it, and it is undefined without a kept count — the next result then only
11826
+ * sets the baseline, since the CLI restores spend from turns before it. It
11827
+ * grows by what each result is counted for, never by a total that includes
11828
+ * cost the runner marked unknown, so that spend can be counted when its basis
11829
+ * is reported; and it never lowers, since counted dollars are not un-spent.
11680
11830
  */
11681
11831
  reportedCostUsd = 0;
11682
11832
  reportedModelCostUsd = /* @__PURE__ */ new Map();
11833
+ /** What this turn's streamed calls already reported, so its result only adds what did not stream. */
11834
+ turnCallUsage = { inputTokens: 0, cachedInputTokens: 0, outputTokens: 0 };
11683
11835
  /**
11684
11836
  * Subagents started and not yet finished, keyed by the `Agent` call's id.
11685
11837
  * Kept across turns: a backgrounded one reports after its turn has ended.
@@ -11766,6 +11918,14 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
11766
11918
  if (!sessionId || this.reportedCostUsd === void 0) return null;
11767
11919
  return { sessionId, usd: this.reportedCostUsd, modelUsd: Object.fromEntries(this.reportedModelCostUsd) };
11768
11920
  }
11921
+ /**
11922
+ * A cleared conversation's totals start again from zero, so what follows is
11923
+ * all new spend, even for a resume that had no count kept.
11924
+ */
11925
+ restartCount() {
11926
+ this.reportedCostUsd = 0;
11927
+ this.reportedModelCostUsd = /* @__PURE__ */ new Map();
11928
+ }
11769
11929
  /** A count kept for another session says nothing of this one's. */
11770
11930
  countFrom(sessionId, counted) {
11771
11931
  const known = counted?.sessionId === sessionId ? counted : void 0;
@@ -11839,11 +11999,10 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
11839
11999
  * the alias resolves to now; that id is pinned before the first turn.
11840
12000
  */
11841
12001
  async handshake(opts) {
11842
- if (opts.kind !== "task") return;
11843
12002
  const initialized = (await this.control({ subtype: "initialize" }, MODEL_PIN_TIMEOUT_MS))?.response;
11844
12003
  this.listedCommands = initialized?.commands ?? [];
11845
12004
  this.listedModels = initialized?.models ?? [];
11846
- if (opts.flags.permissionMode !== "plan" || !opts.model) return;
12005
+ if (opts.kind !== "task" || opts.flags.permissionMode !== "plan" || !opts.model) return;
11847
12006
  const resolved = initialized?.models?.find((m) => m.value === opts.model)?.resolvedModel;
11848
12007
  if (resolved && resolved !== opts.model) await this.control({ subtype: "set_model", model: resolved }, MODEL_PIN_TIMEOUT_MS);
11849
12008
  }
@@ -11884,6 +12043,15 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
11884
12043
  this.interruptRequested = true;
11885
12044
  return (await this.control({ subtype: "interrupt" }, timeoutMs))?.subtype === "success";
11886
12045
  }
12046
+ /**
12047
+ * On stdin's end the CLI exits by itself once its turn has closed, and only
12048
+ * an exit of its own saves the session's running totals for a `--resume` to
12049
+ * restore (checked on 2.1.303: about a second later, code 0, or 1 after an
12050
+ * interrupted turn).
12051
+ */
12052
+ endInput() {
12053
+ this.process?.stdin?.end();
12054
+ }
11887
12055
  /**
11888
12056
  * A `user` line written mid-turn is Claude Code's own queue (ADR-0023): the
11889
12057
  * CLI attaches it to the next tool result, or runs it as a turn of its own
@@ -11971,6 +12139,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
11971
12139
  this.resultHeld = false;
11972
12140
  this.turnClosed = false;
11973
12141
  this.clearFollowOnTimer();
12142
+ this.plannerModel = void 0;
11974
12143
  }
11975
12144
  /**
11976
12145
  * The echo of a steered message: the model has it. After a closed turn it
@@ -12025,6 +12194,8 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
12025
12194
  this.permissions.withdraw(msg.request_id, emit);
12026
12195
  return;
12027
12196
  case "control_response": {
12197
+ const account = msg.response?.response?.account;
12198
+ if (account) this.reportBilling(account.apiKeySource, account.subscriptionType, emit);
12028
12199
  const requestId = msg.response?.request_id;
12029
12200
  if (requestId) this.pendingControl.get(requestId)?.(msg.response ?? null);
12030
12201
  return;
@@ -12068,12 +12239,24 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
12068
12239
  emit({ type: "tool_result", id, name: "", output: diff || output, success: block.is_error !== true, subagentId });
12069
12240
  }
12070
12241
  return;
12242
+ // `/clear` (and `/reset`, `/new`) emit this top-level line before their
12243
+ // own result, and a task's chat can send them. The CLI's totals restart.
12244
+ case "conversation_reset": {
12245
+ if (subagentId) return;
12246
+ this.restartCount();
12247
+ const resetModel = this.sessionModel ?? (this.plannerModel !== "<synthetic>" ? this.plannerModel : void 0);
12248
+ emit({ type: "usage", record: { source: "claude-code", ...resetModel ? { model: resetModel } : {}, compacted: true } });
12249
+ return;
12250
+ }
12071
12251
  case "system":
12252
+ if (subagentId && (msg.subtype === "init" || msg.subtype === "compact_boundary")) return;
12072
12253
  if (msg.subtype === "init") {
12073
12254
  this.clearFollowOnTimer();
12074
12255
  if (this.refuseOtherMode(msg, emit)) return;
12256
+ if (msg.model && msg.model !== "<synthetic>") this.sessionModel = msg.model;
12075
12257
  this.readLoaded(msg, emit);
12076
12258
  this.followModel(msg.model, emit);
12259
+ this.reportBilling(msg.apiKeySource, void 0, emit);
12077
12260
  }
12078
12261
  if (msg.subtype === "commands_changed" && Array.isArray(msg.commands)) this.listedCommands = msg.commands;
12079
12262
  if (msg.subtype === "compact_boundary") {
@@ -12084,6 +12267,8 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
12084
12267
  ...typeof meta?.pre_tokens === "number" ? { preTokens: meta.pre_tokens } : {},
12085
12268
  ...typeof meta?.post_tokens === "number" ? { postTokens: meta.post_tokens } : {}
12086
12269
  });
12270
+ const model = this.sessionModel ?? (this.plannerModel !== "<synthetic>" ? this.plannerModel : void 0);
12271
+ emit({ type: "usage", record: { source: "claude-code", ...model ? { model } : {}, compacted: true, ...typeof meta?.post_tokens === "number" ? { contextTokens: meta.post_tokens } : {} } });
12087
12272
  }
12088
12273
  if (msg.subtype === "background_tasks_changed" && Array.isArray(msg.tasks)) {
12089
12274
  this.backgroundTaskCount = msg.tasks.length;
@@ -12095,6 +12280,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
12095
12280
  }
12096
12281
  return;
12097
12282
  case "result":
12283
+ if (subagentId) return;
12098
12284
  this.reportSessionCost(msg, emit);
12099
12285
  for (const command of this.modelCommands) command.turnsLeft = Math.max(0, command.turnsLeft - 1);
12100
12286
  if (this.interruptRequested && (msg.is_error || msg.subtype !== "success")) {
@@ -12259,16 +12445,69 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
12259
12445
  */
12260
12446
  handleStreamEvent(event, emit) {
12261
12447
  if (event.type === "message_start") {
12262
- this.plannerModel = event.message?.model;
12448
+ this.plannerModel = this.modelId(event.message?.model);
12263
12449
  this.streamingMessage = event.message?.id;
12264
12450
  this.followModel(this.plannerModel, emit);
12265
- } else if (event.type === "message_delta" && event.usage) emit({ type: "usage", record: usageRecord(event.usage, this.plannerModel) });
12266
- else if (event.type === "content_block_delta" && event.delta?.type === "text_delta" && event.delta.text) {
12451
+ } else if (event.type === "message_delta" && event.usage) {
12452
+ const record = usageRecord(event.usage, this.plannerModel);
12453
+ this.turnCallUsage.inputTokens += record.inputTokens ?? 0;
12454
+ this.turnCallUsage.cachedInputTokens += record.cachedInputTokens ?? 0;
12455
+ this.turnCallUsage.outputTokens += record.outputTokens ?? 0;
12456
+ emit({ type: "usage", record });
12457
+ } else if (event.type === "content_block_delta" && event.delta?.type === "text_delta" && event.delta.text) {
12267
12458
  this.replyRuns.delta(`${this.streamingMessage ?? ""}:${event.index ?? 0}`, event.delta.text, emit);
12268
12459
  } else if (event.type === "content_block_delta" && event.delta?.type === "thinking_delta" && event.delta.thinking) {
12269
12460
  emit({ type: "thinking_delta", text: event.delta.thinking });
12270
12461
  }
12271
12462
  }
12463
+ /**
12464
+ * The id a reported model runs as. `init` names the selected value — a
12465
+ * `[1m]` variant included — while a message names the API's canonical id
12466
+ * without it. Keeping the variant is what lets the meter find the window
12467
+ * `modelUsage` keys it by, and keeps that window off the base model.
12468
+ */
12469
+ modelId(reported) {
12470
+ if (!reported) return void 0;
12471
+ if (!sameModel(this.sessionModel, reported)) return reported;
12472
+ return this.sessionModel?.includes("[") ? this.sessionModel : reported;
12473
+ }
12474
+ reportBilling(apiKeySource, plan, emit) {
12475
+ const type = apiKeySource ? apiKeySource === "none" ? "subscription" : "api-key" : plan ? "subscription" : void 0;
12476
+ if (!type) return;
12477
+ const knownPlan = plan ?? (this.billing?.type === type ? this.billing.plan : void 0);
12478
+ this.billing = { type, ...knownPlan ? { plan: knownPlan } : {} };
12479
+ emit({ type: "usage", record: { source: this.agentId, aggregate: true, sessionBilling: this.billing } });
12480
+ }
12481
+ /**
12482
+ * A turn's `result` states its own token sum. Every call that streamed its
12483
+ * `message_delta` usage has already reported, so only the remainder is added:
12484
+ * all of it when nothing streamed (a recording without partial messages), or
12485
+ * a lost call's share. It is a sum across calls, marked `aggregate` so it
12486
+ * never moves the meter — only a call says what the context was left at.
12487
+ */
12488
+ reportUnstreamedUsage(msg, emit) {
12489
+ const { inputTokens: streamedInput, cachedInputTokens: streamedCached, outputTokens: streamedOutput } = this.turnCallUsage;
12490
+ this.turnCallUsage = { inputTokens: 0, cachedInputTokens: 0, outputTokens: 0 };
12491
+ if (!msg.usage) return;
12492
+ const models = Object.keys(msg.modelUsage ?? {});
12493
+ const model = this.plannerModel ?? this.mainModel ?? (models.length === 1 ? models[0] : void 0);
12494
+ const total = usageRecord(msg.usage, model);
12495
+ const inputTokens = (total.inputTokens ?? 0) - streamedInput;
12496
+ const cachedInputTokens = (total.cachedInputTokens ?? 0) - streamedCached;
12497
+ const outputTokens = (total.outputTokens ?? 0) - streamedOutput;
12498
+ if (inputTokens <= 0 && outputTokens <= 0) return;
12499
+ emit({
12500
+ type: "usage",
12501
+ record: {
12502
+ source: "claude-code",
12503
+ ...model ? { model } : {},
12504
+ aggregate: true,
12505
+ ...inputTokens > 0 ? { inputTokens } : {},
12506
+ ...inputTokens > 0 && cachedInputTokens > 0 ? { cachedInputTokens } : {},
12507
+ ...outputTokens > 0 ? { outputTokens } : {}
12508
+ }
12509
+ });
12510
+ }
12272
12511
  /**
12273
12512
  * What only the result line knows: the cost, which covers every call the
12274
12513
  * session made — subagents included, since none of their lines carries one —
@@ -12277,24 +12516,54 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
12277
12516
  * the session before; with no count kept, the first turn only sets the
12278
12517
  * baseline: its total includes turns counted before, and a turn's own share
12279
12518
  * cannot be told apart from them.
12519
+ *
12520
+ * The running total never lowers: money already reported and counted is not
12521
+ * un-spent because a later or resumed process remembers less of it, and
12522
+ * counting its return would count the same dollars twice. A zero is not a
12523
+ * measurement at all — a crash can end with one — and only `/clear` restarts
12524
+ * the count, through {@link restartCount}. Per-model baselines never lower
12525
+ * either, for the same reason.
12526
+ *
12527
+ * A report without a model breakdown is counted as a session total; one with
12528
+ * a breakdown is counted per model. A breakdown that would count more than
12529
+ * the session total's own growth (the count before it had no model split, or
12530
+ * a lossy report) is counted as that growth alone, so the two shapes
12531
+ * together still add each dollar once.
12280
12532
  */
12281
12533
  reportSessionCost(msg, emit) {
12534
+ this.reportUnstreamedUsage(msg, emit);
12282
12535
  const total = msg.total_cost_usd;
12283
12536
  const canReport = this.reportedCostUsd !== void 0;
12284
- const modelCosts = Object.entries(msg.modelUsage ?? {}).filter((entry) => typeof entry[1].costUSD === "number");
12537
+ const modelCosts = Object.entries(msg.modelUsage ?? {}).filter((entry) => typeof entry[1].costUSD === "number" && entry[1].costUSD > 0 && entry[1].costBasis !== "unknown");
12538
+ const growth = canReport && typeof total === "number" && total > 0 ? total - this.reportedCostUsd : void 0;
12539
+ let emitted = 0;
12540
+ const emitCost = (record) => {
12541
+ if (!(record.amount > 0)) return;
12542
+ emitted += record.amount;
12543
+ emit({ type: "usage", record: { source: "claude-code", ...record.model ? { model: record.model } : {}, aggregate: true, ...this.billing ? { sessionBilling: this.billing } : {}, reportedCost: { amount: record.amount, currency: "USD", kind: "estimate", ...record.basis ? { basis: record.basis } : {} } } });
12544
+ };
12285
12545
  if (modelCosts.length > 0) {
12286
- for (const [model, usage] of modelCosts) {
12287
- const previous = this.reportedModelCostUsd.get(model) ?? 0;
12288
- const amount = usage.costUSD - previous;
12289
- if (canReport && amount >= 0) emit({ type: "usage", record: { source: "claude-code", model, aggregate: true, reportedCost: { amount, currency: "USD" } } });
12290
- this.reportedModelCostUsd.set(model, usage.costUSD);
12546
+ const modelGrowth = modelCosts.reduce((sum, [model2, usage]) => sum + Math.max(0, usage.costUSD - (this.reportedModelCostUsd.get(model2) ?? 0)), 0);
12547
+ if (canReport) {
12548
+ if (growth !== void 0 && modelGrowth > growth + 1e-9) {
12549
+ emitCost({ amount: growth });
12550
+ } else {
12551
+ for (const [model2, usage] of modelCosts) emitCost({ model: model2, amount: usage.costUSD - (this.reportedModelCostUsd.get(model2) ?? 0), ...usage.costBasis ? { basis: usage.costBasis } : {} });
12552
+ }
12291
12553
  }
12292
- } else if (typeof total === "number" && canReport && total >= this.reportedCostUsd) {
12293
- emit({ type: "usage", record: { source: "claude-code", aggregate: true, reportedCost: { amount: total - this.reportedCostUsd, currency: "USD" } } });
12554
+ for (const [model2, usage] of modelCosts) this.reportedModelCostUsd.set(model2, Math.max(this.reportedModelCostUsd.get(model2) ?? 0, usage.costUSD));
12555
+ } else if (!Object.values(msg.modelUsage ?? {}).some((usage) => usage.costBasis === "unknown") && typeof total === "number" && total > 0 && canReport && total >= this.reportedCostUsd) {
12556
+ emitCost({ amount: total - this.reportedCostUsd });
12294
12557
  }
12295
- if (typeof total === "number") this.reportedCostUsd = total;
12296
- const contextWindow = this.plannerModel ? msg.modelUsage?.[this.plannerModel]?.contextWindow : void 0;
12297
- if (contextWindow !== void 0) emit({ type: "usage", record: { source: "claude-code", model: this.plannerModel, contextWindow } });
12558
+ if (!canReport) {
12559
+ if (typeof total === "number" && total > 0) this.reportedCostUsd = total;
12560
+ else if (modelCosts.length > 0) this.reportedCostUsd = modelCosts.reduce((sum, [, usage]) => sum + usage.costUSD, 0);
12561
+ } else {
12562
+ this.reportedCostUsd = (this.reportedCostUsd ?? 0) + emitted;
12563
+ }
12564
+ const model = this.plannerModel && this.plannerModel !== "<synthetic>" ? this.plannerModel : this.sessionModel;
12565
+ const contextWindow = model ? msg.modelUsage?.[model]?.contextWindow : void 0;
12566
+ if (typeof contextWindow === "number" && contextWindow > 0) emit({ type: "usage", record: { source: "claude-code", model, contextWindow } });
12298
12567
  }
12299
12568
  };
12300
12569
 
@@ -12929,9 +13198,13 @@ function mergeCodexRateLimits(known, params) {
12929
13198
  const id = snapshot.limitId ?? "";
12930
13199
  const before = known.get(id);
12931
13200
  const next = new Map(known);
12932
- next.set(id, { limitId: id, primary: snapshot.primary ?? before?.primary, secondary: snapshot.secondary ?? before?.secondary });
13201
+ next.set(id, { limitId: id, planType: snapshot.planType ?? before?.planType, primary: snapshot.primary ?? before?.primary, secondary: snapshot.secondary ?? before?.secondary });
12933
13202
  return next;
12934
13203
  }
13204
+ function codexSessionBilling(limits) {
13205
+ const plan = [...limits.values()].find((snapshot) => snapshot.planType && snapshot.planType !== "unknown")?.planType;
13206
+ return plan ? { type: "subscription", plan } : void 0;
13207
+ }
12935
13208
  function isCodexUsageLimit(error) {
12936
13209
  const info = error?.codexErrorInfo;
12937
13210
  if (info === void 0 || info === null || info === "other") return void 0;
@@ -13490,8 +13763,21 @@ ${this.exitMessage()}`);
13490
13763
  case "thread/tokenUsage/updated":
13491
13764
  this.emitUsage(msg.params, emit);
13492
13765
  return;
13766
+ case "account/updated": {
13767
+ const account = msg.params;
13768
+ if (account?.authMode === "apikey") emit({ type: "usage", record: { source: this.agentId, aggregate: true, sessionBilling: { type: "api-key" } } });
13769
+ else if (account?.authMode === "chatgpt" || account?.authMode === "chatgptAuthTokens") {
13770
+ const plan = account.planType;
13771
+ emit({ type: "usage", record: { source: this.agentId, aggregate: true, sessionBilling: { type: "subscription", ...plan && plan !== "unknown" ? { plan } : {} } } });
13772
+ }
13773
+ return;
13774
+ }
13493
13775
  case "account/rateLimits/updated":
13494
13776
  this.rateLimits = mergeCodexRateLimits(this.rateLimits, msg.params);
13777
+ {
13778
+ const sessionBilling = codexSessionBilling(this.rateLimits);
13779
+ if (sessionBilling) emit({ type: "usage", record: { source: this.agentId, aggregate: true, sessionBilling } });
13780
+ }
13495
13781
  return;
13496
13782
  case "turn/started": {
13497
13783
  if (this.subagentOf(msg.params?.threadId)) return;
@@ -13620,6 +13906,13 @@ ${this.exitMessage()}`);
13620
13906
  * `last.outputTokens` already includes `reasoningOutputTokens` — the thread
13621
13907
  * total is `inputTokens + outputTokens`, not a sum of three — so reasoning is
13622
13908
  * not added a second time. Codex reports no price.
13909
+ *
13910
+ * The main thread's context is what Codex itself shows as in its window:
13911
+ * `TokenUsage::tokens_in_context_window`, which is `total_tokens` over
13912
+ * `last` as codex-rs defines it. `total_tokens` is input plus output, and
13913
+ * reasoning is part of output, so this is not `input + output − reasoning`
13914
+ * (checked against codex-rs and codex-cli 0.163.0's token_count events,
13915
+ * where `total_tokens` is `input_tokens + output_tokens`).
13623
13916
  */
13624
13917
  emitUsage(params, emit) {
13625
13918
  const last = params?.tokenUsage?.last;
@@ -13647,6 +13940,8 @@ ${this.exitMessage()}`);
13647
13940
  } else {
13648
13941
  const window = params?.tokenUsage?.modelContextWindow;
13649
13942
  if (typeof window === "number" && window > 0) record.contextWindow = window;
13943
+ const inContext = last.totalTokens !== void 0 && last.totalTokens > 0 ? last.totalTokens : last.inputTokens !== void 0 ? last.inputTokens + (last.outputTokens ?? 0) : void 0;
13944
+ if (inContext !== void 0) record.contextTokens = Math.max(0, inContext);
13650
13945
  }
13651
13946
  emit({ type: "usage", record });
13652
13947
  }
@@ -13808,7 +14103,9 @@ ${this.exitMessage()}`);
13808
14103
  }
13809
14104
  switch (item.type) {
13810
14105
  case "contextCompaction":
13811
- if (!subagentId) emit({ type: "compaction", trigger: this.command === "compact" ? "manual" : "auto" });
14106
+ if (subagentId) return;
14107
+ emit({ type: "compaction", trigger: this.command === "compact" ? "manual" : "auto" });
14108
+ emit({ type: "usage", record: { source: this.agentId, compacted: true } });
13812
14109
  return;
13813
14110
  // Codex announces the item started and completed; whichever lands first delivers.
13814
14111
  case "userMessage":
@@ -14104,8 +14401,9 @@ function usageRecord2(tokens, call) {
14104
14401
  if ((prompt.inputTokens ?? 0) + outputTokens === 0) return null;
14105
14402
  const record = { source: "opencode", ...prompt, outputTokens };
14106
14403
  if (call.model) record.model = call.model;
14107
- if (typeof call.cost === "number" && call.cost > 0) record.reportedCost = { amount: call.cost, currency: "USD" };
14404
+ if (typeof call.cost === "number" && call.cost > 0) record.reportedCost = { amount: call.cost, currency: "USD", kind: "estimate" };
14108
14405
  if (call.subagentId) record.subagentId = call.subagentId;
14406
+ else record.contextTokens = (prompt.inputTokens ?? 0) + outputTokens;
14109
14407
  return record;
14110
14408
  }
14111
14409
  function permissionReply(decision, key) {
@@ -14668,6 +14966,7 @@ var OpenCodeV2 = class {
14668
14966
  }
14669
14967
  if (frame.type === "session.compaction.ended") {
14670
14968
  onEvent({ type: "compaction", trigger: data.reason === "manual" ? "manual" : "auto" });
14969
+ onEvent({ type: "usage", record: { source: "opencode", compacted: true } });
14671
14970
  return;
14672
14971
  }
14673
14972
  if (frame.type === "session.execution.started") {
@@ -14926,7 +15225,7 @@ var OpenCodeAdapter = class {
14926
15225
  return this.runner.ended;
14927
15226
  }
14928
15227
  get exited() {
14929
- return this.runner.exit !== null;
15228
+ return this.runner.hasExited;
14930
15229
  }
14931
15230
  async start(opts) {
14932
15231
  try {
@@ -15533,6 +15832,7 @@ var OpenCodeAdapter = class {
15533
15832
  if (session && session !== this.sessionId && !turn.children.follows(session)) return;
15534
15833
  if (frame.type === "session.compacted" && session === this.sessionId) {
15535
15834
  onEvent({ type: "compaction", trigger: this.manualCompaction ? "manual" : "auto" });
15835
+ onEvent({ type: "usage", record: { source: "opencode", compacted: true } });
15536
15836
  return;
15537
15837
  }
15538
15838
  if (session === this.sessionId && this.taskTurn) this.followTaskTurn(frame, this.taskTurn, onEvent);
@@ -20555,6 +20855,7 @@ var PlannerConversation = class {
20555
20855
  const request = summaryRequest(this.currentPlanLines());
20556
20856
  const onProgress = (p) => {
20557
20857
  if (p.type === "liveness") this.host.onProgress(p);
20858
+ else if (p.type === "usage" && p.record) this.host.onProgress({ ...p, record: { ...p.record, aggregate: true } });
20558
20859
  };
20559
20860
  let turn;
20560
20861
  if (this.canContinueLive()) {
@@ -23103,10 +23404,21 @@ var GitWorktreeIsolation = class {
23103
23404
  const entry = record.repos[repo.path];
23104
23405
  if (!entry?.changed) continue;
23105
23406
  const merged = await this.tryGit(repo.root, ["merge-base", "--is-ancestor", entry.landedTip ?? repo.integrationBranch, "HEAD"]);
23106
- if (!merged.ok) return false;
23407
+ if (!merged.ok && !await this.integrationGone(repo)) return false;
23107
23408
  }
23108
23409
  return true;
23109
23410
  }
23411
+ /**
23412
+ * The repo's integration branch is gone — deleted by hand, or by a squash
23413
+ * merge's clean-up — in a repo git can read. Merge all merges that branch by
23414
+ * name, so no merge can bring the work in again, and it is not offered as
23415
+ * work to merge. A repo git cannot read at all (moved, or git failing)
23416
+ * proves nothing either way and counts as not merged.
23417
+ */
23418
+ async integrationGone(repo) {
23419
+ if (!(await this.tryGit(repo.root, ["rev-parse", "--verify", "-q", "HEAD"])).ok) return false;
23420
+ return !(await this.tryGit(repo.root, ["rev-parse", "--verify", "-q", `refs/heads/${repo.integrationBranch}`])).ok;
23421
+ }
23110
23422
  async snapshotTree(workspaceRoot, exclude) {
23111
23423
  const scan = await this.scanGroup(workspaceRoot);
23112
23424
  if ("refused" in scan) return null;
@@ -24234,6 +24546,9 @@ function nonPromptingFs(fs18) {
24234
24546
  }
24235
24547
  };
24236
24548
  }
24549
+ function withoutContext({ contextTokens: _context, ...record }) {
24550
+ return record;
24551
+ }
24237
24552
  async function runLoop(prompt, deps) {
24238
24553
  if (deps.signal?.aborted) return "[research agent aborted before starting]";
24239
24554
  const fs18 = nonPromptingFs(deps.fs);
@@ -24241,7 +24556,8 @@ async function runLoop(prompt, deps) {
24241
24556
  (delta) => deps.onProgress?.({ type: "thinking", text: delta, subagentId: deps.subagentId }),
24242
24557
  // The record says who made the call: unstamped, the ledger would read the
24243
24558
  // subagent's prompt as the planner's own and measure its context by it.
24244
- (record) => deps.onProgress?.({ type: "usage", record: deps.subagentId ? { ...record, subagentId: deps.subagentId } : record })
24559
+ // The chat states the context its call leaves; a subagent's is no meter's.
24560
+ (record) => deps.onProgress?.({ type: "usage", record: deps.subagentId ? { ...withoutContext(record), subagentId: deps.subagentId } : record })
24245
24561
  );
24246
24562
  let turn = await chat.sendMessage(prompt, deps.signal);
24247
24563
  for (let step = 0; turn.hasToolCalls && step < SUBAGENT_LIMITS.maxSteps; step++) {
@@ -24749,7 +25065,8 @@ function usageRecordFrom(usage, model) {
24749
25065
  model,
24750
25066
  ...inputTokens !== void 0 ? { inputTokens } : {},
24751
25067
  ...reportedOutput.length > 0 ? { outputTokens: reportedOutput.reduce((a, b) => a + b, 0) } : {},
24752
- ...tokenCount(usage.cachedContentTokenCount) !== void 0 ? { cachedInputTokens: tokenCount(usage.cachedContentTokenCount) } : {}
25068
+ ...tokenCount(usage.cachedContentTokenCount) !== void 0 ? { cachedInputTokens: tokenCount(usage.cachedContentTokenCount) } : {},
25069
+ ...inputTokens !== void 0 ? { contextTokens: inputTokens + (answerTokens ?? 0) } : {}
24753
25070
  };
24754
25071
  const anyReported = record.inputTokens !== void 0 || record.outputTokens !== void 0 || record.cachedInputTokens !== void 0;
24755
25072
  return anyReported ? record : void 0;
@@ -25152,7 +25469,9 @@ var OpenAiResearchChat = class {
25152
25469
  /**
25153
25470
  * The one record for this API call, built only from what the provider
25154
25471
  * reported. OpenRouter's `cost` is in USD (its credits are dollar-pegged);
25155
- * nothing is estimated. A field the provider left out stays absent.
25472
+ * nothing is estimated. A field the provider left out stays absent. The
25473
+ * context the call leaves is its prompt and its completion: the API's own
25474
+ * `total_tokens`, and the reply the next call's prompt carries.
25156
25475
  */
25157
25476
  usageRecord(reported) {
25158
25477
  if (!reported) return void 0;
@@ -25171,9 +25490,10 @@ var OpenAiResearchChat = class {
25171
25490
  hasMeasure = true;
25172
25491
  }
25173
25492
  if (reported.cost !== void 0) {
25174
- record.reportedCost = { amount: reported.cost, currency: "USD" };
25493
+ record.reportedCost = { amount: reported.cost, currency: "USD", ...record.source === "openrouter" ? { kind: "billed" } : {} };
25175
25494
  hasMeasure = true;
25176
25495
  }
25496
+ if (record.inputTokens !== void 0) record.contextTokens = record.inputTokens + (record.outputTokens ?? 0);
25177
25497
  return hasMeasure ? record : void 0;
25178
25498
  }
25179
25499
  };
@@ -26321,6 +26641,8 @@ var IsolationRunController = class {
26321
26641
  listener;
26322
26642
  liveTasks;
26323
26643
  run = null;
26644
+ /** An adoption stays scoped even while cleanup temporarily leaves no run record. */
26645
+ adoptions = 0;
26324
26646
  /** Copied paths already reported for the current run: every task gets the same copies. */
26325
26647
  reportedCopies = /* @__PURE__ */ new Set();
26326
26648
  reportedSetupFailures = /* @__PURE__ */ new Set();
@@ -26396,18 +26718,27 @@ var IsolationRunController = class {
26396
26718
  * Take over a plan's persisted isolation, or none for a plan that has not
26397
26719
  * isolated yet. Whatever a crashed process left behind for the run — a
26398
26720
  * worktree still marked active, a directory no record owns — is pruned,
26399
- * while kept, failed and conflicted worktrees stay for the user.
26721
+ * while kept, failed and conflicted worktrees stay for the user. Then git is
26722
+ * asked which landed work the user has merged since the record was saved:
26723
+ * a settled run whose landed work is all in HEAD has nothing left to hand
26724
+ * over, so it is cleared up as Merge all would have, unless it still holds
26725
+ * work that never landed (ADR-0013).
26400
26726
  *
26401
- * Deliberately not reported as a change: adopting is not a change to
26402
- * persist, and a host that adopts without persisting (VS Code's restore)
26403
- * would otherwise write a new session file on every reload.
26727
+ * Deliberately not reported to the listener: adopting is not a change of the
26728
+ * session's own, and a host that adopts without persisting (VS Code's
26729
+ * restore) would otherwise write a new session file on every reload. What
26730
+ * git changed is returned instead, for the caller to save where it can and
26731
+ * announce. A plan loaded meanwhile supersedes this adoption, which then
26732
+ * touches nothing and resolves `unchanged`.
26404
26733
  */
26405
26734
  async adopt(state) {
26735
+ const adoption = ++this.adoptions;
26406
26736
  this.run = state?.run ?? null;
26407
26737
  this.resolvers = { ...state?.resolvers ?? {} };
26408
- if (!this.run) return;
26738
+ const run = this.run;
26739
+ if (!run) return "unchanged";
26409
26740
  try {
26410
- const pruned = await this.isolation.pruneOrphans(this.run);
26741
+ const pruned = await this.isolation.pruneOrphans(run);
26411
26742
  for (const task of pruned.kept) {
26412
26743
  this.tell("warn", `Task "${task.title}" was still holding unlanded work when this plan was re-opened, so its worktree and branch were kept. Retry it to land the work, or review it by hand.`);
26413
26744
  }
@@ -26415,6 +26746,24 @@ var IsolationRunController = class {
26415
26746
  } catch (err) {
26416
26747
  this.tell("warn", `Could not prune leftover worktrees: ${err instanceof Error ? err.message : String(err)}`);
26417
26748
  }
26749
+ if (this.run !== run) return "unchanged";
26750
+ const found = await this.findLanded(run);
26751
+ if (this.run !== run) return "unchanged";
26752
+ const outcome = found ? "changed" : "unchanged";
26753
+ if (this.isOpen || !this.allMerged(run) || this.holdouts(run).length > 0) return outcome;
26754
+ const resolvers = this.resolvers;
26755
+ this.run = null;
26756
+ this.resolvers = {};
26757
+ const discarded = await this.discardMerged(run);
26758
+ if (this.adoptions !== adoption) return "unchanged";
26759
+ if (!discarded) {
26760
+ if (this.run === null) {
26761
+ this.run = run;
26762
+ this.resolvers = resolvers;
26763
+ }
26764
+ return outcome;
26765
+ }
26766
+ return this.isOpen ? "changed" : "cleared";
26418
26767
  }
26419
26768
  /** Open a run if none is, leaving whether it isolates to its first change task. */
26420
26769
  open() {
@@ -26571,8 +26920,7 @@ var IsolationRunController = class {
26571
26920
  const run = this.run;
26572
26921
  if (mode !== "isolated" || !run) return;
26573
26922
  await this.refreshInHead();
26574
- const records = Object.values(run.tasks);
26575
- if (records.some((r) => r.status === "merged") && handoffOf(run).landed.length === 0) {
26923
+ if (this.allMerged(run)) {
26576
26924
  this.tell("info", "Everything this run landed is merged into your branch already.");
26577
26925
  await this.clearMerged(run);
26578
26926
  return;
@@ -26599,11 +26947,21 @@ var IsolationRunController = class {
26599
26947
  */
26600
26948
  async refreshInHead() {
26601
26949
  const run = this.run;
26602
- if (!run || !Object.values(run.tasks).some((r) => r.status === "merged" && !r.inHead)) return false;
26950
+ if (!run) return false;
26951
+ const found = await this.findLanded(run);
26952
+ if (found) this.listener.changed();
26953
+ return found;
26954
+ }
26955
+ /** Mark the run's landed work git finds in HEAD; true when some was found. */
26956
+ async findLanded(run) {
26957
+ if (!Object.values(run.tasks).some((r) => r.status === "merged" && !r.inHead)) return false;
26603
26958
  const found = await this.isolation.findInHead(run).catch(() => []);
26604
- if (found.length > 0) this.listener.changed();
26605
26959
  return found.length > 0;
26606
26960
  }
26961
+ /** The run landed something and every landing is in the user's HEAD. */
26962
+ allMerged(run) {
26963
+ return Object.values(run.tasks).some((r) => r.status === "merged") && handoffOf(run).landed.length === 0;
26964
+ }
26607
26965
  /**
26608
26966
  * The tracked state of the workspace as an attempt whose tree is checked
26609
26967
  * starts (see `attemptKind.checksTree`), for {@link filesChangedSince}
@@ -26725,7 +27083,7 @@ var IsolationRunController = class {
26725
27083
  */
26726
27084
  async clearMerged(run) {
26727
27085
  const live2 = this.liveRecords(run);
26728
- const unlanded = Object.values(run.tasks).filter((r) => r.status !== "merged" && !live2.includes(r));
27086
+ const unlanded = this.holdouts(run).filter((r) => !live2.includes(r));
26729
27087
  if (live2.length > 0 || unlanded.length > 0) {
26730
27088
  const why = [
26731
27089
  ...live2.length > 0 ? [`${tasksNamed(live2)} ${live2.length === 1 ? "is" : "are"} still running or waiting on you`] : [],
@@ -26735,13 +27093,22 @@ var IsolationRunController = class {
26735
27093
  this.tell("info", `The run is not cleared up: ${why.join(", and ")}, so ${one ? "its worktree stays" : "their worktrees stay"}, and so do the run's branches.`);
26736
27094
  return;
26737
27095
  }
27096
+ if (await this.discardMerged(run)) this.forget();
27097
+ }
27098
+ /** The records that keep a settled run from being cleared up: live, or holding work that never landed. */
27099
+ holdouts(run) {
27100
+ const live2 = this.liveTasks();
27101
+ return Object.values(run.tasks).filter((r) => live2.has(r.taskId) || r.status !== "merged");
27102
+ }
27103
+ /** The run's worktrees and merged branches go; false, said, when git would not let them. */
27104
+ async discardMerged(run) {
26738
27105
  try {
26739
27106
  this.report(await this.isolation.discard(run, { integration: "delete-merged" }));
27107
+ return true;
26740
27108
  } catch (err) {
26741
27109
  this.tell("warn", `Merged, but could not clean up the run's worktrees and branches: ${err instanceof Error ? err.message : String(err)}`);
26742
- return;
27110
+ return false;
26743
27111
  }
26744
- this.forget();
26745
27112
  }
26746
27113
  /** Say what a removal kept back instead of deleting: where the work went, or which worktree stayed. */
26747
27114
  report(removal) {
@@ -27297,6 +27664,10 @@ var TaskOrchestrator = class _TaskOrchestrator {
27297
27664
  store;
27298
27665
  output;
27299
27666
  attempts = /* @__PURE__ */ new Map();
27667
+ /** Bumped by every plan load: an ended attempt's wind-down belongs to the scope before it. */
27668
+ planScope = 0;
27669
+ /** Each task's runners still winding down once stopped: a continue waits for them (ADR-0018, L1). */
27670
+ windDowns = /* @__PURE__ */ new Map();
27300
27671
  verifier = new VerdictEngine();
27301
27672
  /** Changed only by start, stop, a plan load, a usage-limit pause, a move off the limit, and {@link settleRun}. */
27302
27673
  runState = "idle";
@@ -27845,8 +28216,9 @@ var TaskOrchestrator = class _TaskOrchestrator {
27845
28216
  return blockingFailures(this.store.planTasks).get(taskId) ?? [];
27846
28217
  }
27847
28218
  loadPlan(tasks, planRunners = [...DEFAULT_RUNNERS]) {
27848
- this.store.load(tasks, planRunners);
27849
28219
  const repairs = this.endAllAttempts("load").filter((a) => a.kind.kind === "repair" && a.worktree);
28220
+ this.planScope += 1;
28221
+ this.store.load(tasks, planRunners);
27850
28222
  for (const a of repairs) this.unawaited(this.runs.release(a.taskId, { keep: true }, a.integration), `Could not keep the worktree of task "${this.store.get(a.taskId)?.title ?? a.taskId}"`);
27851
28223
  this.runs.interrupt({ keepOpen: this.isScheduling });
27852
28224
  if (this.runState !== "scheduling") this.runState = "idle";
@@ -27932,6 +28304,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
27932
28304
  if (reason === "user") this.stopGeneration++;
27933
28305
  this.runState = "idle";
27934
28306
  this.verifier.reset();
28307
+ for (const attempt of this.attempts.values()) this.stopRunner(attempt);
27935
28308
  this.runner.stopAll();
27936
28309
  const stopped = [];
27937
28310
  for (const a of this.endAllAttempts("stop")) {
@@ -28045,6 +28418,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
28045
28418
  if (!task || !attempt) return;
28046
28419
  attempt.decided = true;
28047
28420
  const stoppedMidTurn = endedByExit(verdict) ? saidInFlight(attempt) : null;
28421
+ this.stopRunner(attempt);
28048
28422
  console.error(`[TaskOrchestrator] Task ${taskRefOf(this.store.planTasks, task.id) ?? `#${task.order}`} "${task.title}" verdict=${verdict.outcome}`);
28049
28423
  console.error(`[TaskOrchestrator] Runner: ${task.assignedRunner}, Model: ${task.assignedModel?.modelId ?? "default"}`);
28050
28424
  console.error(`[TaskOrchestrator] Prompt preview: ${(task.prompt ?? "").slice(0, 200)}`);
@@ -28956,7 +29330,9 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
28956
29330
  autoResumed: opts.autoResumed === true,
28957
29331
  turnError: null,
28958
29332
  turnOutputFrom: 0,
28959
- reminded: false
29333
+ reminded: false,
29334
+ runnerStopped: false,
29335
+ planScope: this.planScope
28960
29336
  };
28961
29337
  this.spawnCounts.set(task.id, attempt.attempt);
28962
29338
  this.attempts.set(task.id, attempt);
@@ -28993,6 +29369,7 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
28993
29369
  const manifest = this.registry?.get(attempt.runner)?.manifest;
28994
29370
  const unavailable = manifest && unavailableTaskModeMessage(manifest, task.taskMode);
28995
29371
  if (unavailable) throw new Error(unavailable);
29372
+ if (kind.kind === "continuation") await this.windDowns.get(task.id);
28996
29373
  const { cwd, worktree, previousWork } = await attemptCwd(kind, task, this.runs);
28997
29374
  attempt.cwd = cwd;
28998
29375
  attempt.worktree = worktree;
@@ -29048,10 +29425,11 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
29048
29425
  }
29049
29426
  if (takesSkills(kind)) this.store.setTaskAttemptSkills(task.id, [...attempt.skills]);
29050
29427
  session.onEvent((event) => {
29051
- if (event.type !== "usage" || this.attempts.get(task.id) !== attempt) return;
29428
+ if (event.type !== "usage" || attempt.planScope !== this.planScope || this.attempts.get(task.id) !== attempt && !attempt.runnerStopped) return;
29052
29429
  this.store.recordAttemptUsage(task.id, attempt.attempt, event.record, (/* @__PURE__ */ new Date()).toISOString());
29053
29430
  this.emit("onUsageChanged");
29054
29431
  });
29432
+ session.onExit(() => this.woundDown(attempt, session));
29055
29433
  this.output.attach(task.id, session);
29056
29434
  this.verifier.watch(task, session);
29057
29435
  session.onEvent((event) => {
@@ -29220,10 +29598,35 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
29220
29598
  if (stopsRunner(reason)) {
29221
29599
  const task = this.store.get(taskId);
29222
29600
  if (task) this.verifier.clear(task);
29223
- if (session) this.runner.stop(session.id);
29601
+ if (attempt) this.stopRunner(attempt);
29224
29602
  }
29225
29603
  return attempt;
29226
29604
  }
29605
+ /**
29606
+ * Let the attempt's runner go the way it ends a turn (ADR-0018, L1): it
29607
+ * winds down on its own time, bounded by its interrupt grace, and nothing
29608
+ * here waits on it but a continue of the same task.
29609
+ */
29610
+ stopRunner(attempt) {
29611
+ const session = attempt.session;
29612
+ if (!session || attempt.runnerStopped) return;
29613
+ attempt.runnerStopped = true;
29614
+ const { taskId } = attempt;
29615
+ const windDown = Promise.all([this.windDowns.get(taskId), this.runner.stop(session.id)]).catch(() => void 0).then(() => {
29616
+ if (this.windDowns.get(taskId) === windDown) this.windDowns.delete(taskId);
29617
+ });
29618
+ this.windDowns.set(taskId, windDown);
29619
+ }
29620
+ /**
29621
+ * An ended attempt's runner is gone: the count its session ended on is the
29622
+ * one a continue of that session counts from. One still current is ended
29623
+ * with that count instead.
29624
+ */
29625
+ woundDown(attempt, session) {
29626
+ if (attempt.planScope !== this.planScope || !attempt.runnerStopped || this.attempts.get(attempt.taskId) === attempt) return;
29627
+ this.store.endAttemptUsage(attempt.taskId, attempt.attempt, (/* @__PURE__ */ new Date()).toISOString(), session.sessionCost?.() ?? void 0, session.nativeSessionId() ?? void 0);
29628
+ this.emit("onUsageChanged");
29629
+ }
29227
29630
  /** Kept on the task once the attempt ends, so a continue can resume it after a reload (ADR-0018, K1). */
29228
29631
  saveNativeSession(taskId, nativeSessionId, runner) {
29229
29632
  if (nativeSessionId) this.store.setTaskRunnerSessionId(taskId, nativeSessionId, runner);
@@ -29581,7 +29984,7 @@ function taskReadPage(text3, { lines: lines2, since }) {
29581
29984
 
29582
29985
  // src/services/plannerTools.ts
29583
29986
  var PLANNER_TURN_ENDED = "The planning turn this call was made in has ended, so nothing was submitted.";
29584
- function runnersOf(tasks) {
29987
+ function runnersOf2(tasks) {
29585
29988
  return [...new Set(flattenTasks(tasks).filter((t) => t.type !== "user").map((t) => t.assignedRunner))];
29586
29989
  }
29587
29990
  function plannerToolHandler(host) {
@@ -30396,17 +30799,37 @@ var CliAgentAiService = class {
30396
30799
  * `startConversation` reseeds from Ordewell's transcript, which is the same
30397
30800
  * degradation `restoreChat` already performs on every surface.
30398
30801
  *
30802
+ * A process can end while idle without Ordewell stopping it — the OS, the
30803
+ * CLI itself, a sleeping machine, a signal sent to its group — and every
30804
+ * path of ours that kills one also drops its adapter. So an adapter still
30805
+ * held whose process has ended died on its own, and is restarted here
30806
+ * rather than answering the next message with its exit code.
30807
+ *
30399
30808
  * Also restarted when the policy's decision on the runner's own search has
30400
30809
  * changed since the process was spawned: the running thread would otherwise
30401
30810
  * keep searching unasked after Full turned Guarded. Codex 0.162 applies
30402
30811
  * `thread/resume`'s `config.web_search` either way (checked live).
30403
30812
  */
30404
30813
  async ensureAdapter(session) {
30405
- if (session.adapter && session.nativeSearch === this.nativeSearch(session)) return session.adapter;
30814
+ const live2 = session.adapter;
30815
+ if (live2 && !live2.exited && session.nativeSearch === this.nativeSearch(session)) return live2;
30406
30816
  session.stop();
30407
30817
  const start = session.start;
30408
30818
  if (!start) throw new Error("No active planner conversation.");
30409
- return this.startAdapter({ ...start.startOptions, resumeSessionId: session.nativeSessionId ?? void 0, resumeCost: session.sessionCost ?? void 0 }, start.tools, session);
30819
+ const resumeSessionId = session.nativeSessionId ?? void 0;
30820
+ try {
30821
+ const adapter = await this.startAdapter({ ...start.startOptions, resumeSessionId, resumeCost: session.sessionCost ?? void 0 }, start.tools, session);
30822
+ const resumed = adapter.nativeSessionId();
30823
+ if (resumeSessionId && resumed && resumed !== resumeSessionId) {
30824
+ throw new Error("The runner started a different session instead.");
30825
+ }
30826
+ return adapter;
30827
+ } catch (err) {
30828
+ if (!resumeSessionId || session.discarded.aborted) throw err;
30829
+ if (this.session === session) this.session = null;
30830
+ session.discard();
30831
+ throw new Error(`The ${this.runner} planner's session could not be resumed (${err instanceof Error ? err.message : String(err)}). Send your message again: the planner picks the conversation up from its transcript.`);
30832
+ }
30410
30833
  }
30411
30834
  /**
30412
30835
  * What the planner gets of the repository's config, its runner being
@@ -31532,6 +31955,12 @@ function withoutIsolation(tasks) {
31532
31955
  function handoffOf2({ type: _type, ...handoff }) {
31533
31956
  return handoff;
31534
31957
  }
31958
+ function stillToMerge(handoff, runMerge) {
31959
+ if (!handoff?.landed.length) return handoff;
31960
+ if (!runMerge) return null;
31961
+ const same = runMerge.landed.length === handoff.landed.length && runMerge.landed.every((t, i) => t.taskId === handoff.landed[i].taskId);
31962
+ return same ? handoff : { ...handoff, repos: runMerge.repos, landed: runMerge.landed };
31963
+ }
31535
31964
  function planTasks(plan) {
31536
31965
  if ("tasks" in plan) return plan.tasks;
31537
31966
  return rejoinPlanTree(plan.pendingTasks, plan.phase === "executing" ? plan.executionLog : []);
@@ -31552,6 +31981,7 @@ function reduceRun(view, input) {
31552
31981
  ...view,
31553
31982
  tasks: applyStatus(view.tasks, new Map(input.tasks.map((t) => [t.id, t]))),
31554
31983
  runMerge: input.runMerge ?? null,
31984
+ handoff: stillToMerge(view.handoff, input.runMerge),
31555
31985
  usage: input.usage
31556
31986
  };
31557
31987
  case "task_started":
@@ -32104,19 +32534,50 @@ var TaskLogRecorder = class {
32104
32534
  };
32105
32535
 
32106
32536
  // src/services/ContextWindowFill.ts
32107
- var ContextWindowFill = class {
32108
- constructor(catalog, last = {}) {
32537
+ var SessionWindows = class {
32538
+ constructor(catalog = () => void 0) {
32109
32539
  this.catalog = catalog;
32110
- if (last.contextWindow !== void 0) this.known.set(modelKey(last.source, last.model), last.contextWindow);
32111
32540
  }
32112
32541
  catalog;
32113
- /** The window known for each model of the stream, by reporter and model. */
32542
+ reported = /* @__PURE__ */ new Map();
32543
+ report(source, model, contextWindow) {
32544
+ if (contextWindow > 0) this.reported.set(modelKey(source, model), { source, model, contextWindow });
32545
+ }
32546
+ get(source, model) {
32547
+ return this.reported.get(modelKey(source, model))?.contextWindow;
32548
+ }
32549
+ snapshot() {
32550
+ return [...this.reported.values()];
32551
+ }
32552
+ restore(saved) {
32553
+ this.reported.clear();
32554
+ for (const { source, model, contextWindow } of saved ?? []) this.report(source, model, contextWindow);
32555
+ }
32556
+ clear() {
32557
+ this.reported.clear();
32558
+ }
32559
+ };
32560
+ var ContextWindowFill = class {
32561
+ constructor(windows, last = {}) {
32562
+ this.windows = windows;
32563
+ if (last.contextWindow !== void 0) this.known.set(modelKey(last.source, last.model), last.contextWindow);
32564
+ }
32565
+ windows;
32566
+ /** The window this stream used for each of its models, by reporter and model. */
32114
32567
  known = /* @__PURE__ */ new Map();
32115
32568
  fill(record) {
32116
32569
  if (record.subagentId || record.aggregate) return record;
32117
32570
  const key = modelKey(record.source, record.model);
32571
+ if (record.compacted) {
32572
+ if (!record.model) return record;
32573
+ const known = this.windows.get(record.source, record.model) ?? this.known.get(key) ?? positive(this.windows.catalog(record.source, record.model));
32574
+ if (known === void 0) return record;
32575
+ this.known.set(key, known);
32576
+ return { ...record, contextWindow: known };
32577
+ }
32118
32578
  const reported = record.contextWindow !== void 0 && record.contextWindow > 0 ? record.contextWindow : void 0;
32119
- const window = reported ?? this.known.get(key) ?? (record.model ? positive(this.catalog(record.source, record.model)) : void 0);
32579
+ if (reported !== void 0 && record.model) this.windows.report(record.source, record.model, reported);
32580
+ const window = reported ?? (record.model ? this.windows.get(record.source, record.model) : void 0) ?? this.known.get(key) ?? (record.model ? positive(this.windows.catalog(record.source, record.model)) : void 0);
32120
32581
  if (window !== void 0) this.known.set(key, window);
32121
32582
  return window === void 0 || window === record.contextWindow ? record : { ...record, contextWindow: window };
32122
32583
  }
@@ -32124,9 +32585,9 @@ var ContextWindowFill = class {
32124
32585
  function modelKey(source, model) {
32125
32586
  return JSON.stringify([source ?? null, model ?? null]);
32126
32587
  }
32127
- function withContextWindows(runner, catalog) {
32588
+ function withContextWindows(runner, windows) {
32128
32589
  return {
32129
- spawn: async (opts) => windowedSession(await runner.spawn(opts), new ContextWindowFill(catalog, opts.resumeWindow)),
32590
+ spawn: async (opts) => windowedSession(await runner.spawn(opts), new ContextWindowFill(windows, opts.resumeWindow)),
32130
32591
  stop: (sessionId) => runner.stop(sessionId),
32131
32592
  stopAll: () => runner.stopAll(),
32132
32593
  get activeCount() {
@@ -32157,12 +32618,12 @@ function positive(n) {
32157
32618
 
32158
32619
  // src/services/PlannerUsage.ts
32159
32620
  var PlannerUsageLedger = class {
32160
- /** `catalogWindow` serves the window of a planner model whose backend reports none. */
32161
- constructor(catalogWindow = () => void 0) {
32162
- this.catalogWindow = catalogWindow;
32163
- this.windows = new ContextWindowFill(catalogWindow);
32621
+ /** `sessionWindows` serves the window of a planner model whose backend reports none. */
32622
+ constructor(sessionWindows = new SessionWindows()) {
32623
+ this.sessionWindows = sessionWindows;
32624
+ this.windows = new ContextWindowFill(sessionWindows);
32164
32625
  }
32165
- catalogWindow;
32626
+ sessionWindows;
32166
32627
  usage = { totals: {} };
32167
32628
  windows;
32168
32629
  /** Fold one model call's record into the totals; returns the running ledger. */
@@ -32173,16 +32634,16 @@ var PlannerUsageLedger = class {
32173
32634
  /** Adopt a persisted ledger (a reopened session) or start from zero. */
32174
32635
  restore(usage) {
32175
32636
  this.usage = usage ?? { totals: {} };
32176
- this.windows = new ContextWindowFill(this.catalogWindow, this.usage);
32637
+ this.windows = new ContextWindowFill(this.sessionWindows, this.usage);
32177
32638
  }
32178
32639
  /** Start a fresh session's ledger from zero. */
32179
32640
  clear() {
32180
32641
  this.usage = { totals: {} };
32181
- this.windows = new ContextWindowFill(this.catalogWindow);
32642
+ this.windows = new ContextWindowFill(this.sessionWindows);
32182
32643
  }
32183
- /** Whether anything has been recorded — a plan with no usage says nothing. */
32644
+ /** Whether the ledger has measures or session billing facts to broadcast. */
32184
32645
  get hasUsage() {
32185
- return isMeasured(this.usage.totals);
32646
+ return isMeasured(this.usage.totals) || this.usage.sessionBilling !== void 0;
32186
32647
  }
32187
32648
  /** The value persisted onto the plan state. */
32188
32649
  snapshot() {
@@ -33631,7 +34092,7 @@ function createSession(deps) {
33631
34092
  const backend = new PlannerBackend(deps.aiService ? { pinned: deps.aiService, busy } : { provider: () => deps.config.aiProvider, create: () => createAiService(deps.config, { workspaceRoot: deps.workspaceRoot, mcpServer }), busy });
33632
34093
  const aiService = () => backend.current();
33633
34094
  const store = new PlanStore();
33634
- const catalogWindow = (source, model) => deps.modelResolver.contextWindowFor?.(source, model);
34095
+ const windows = new SessionWindows((source, model) => deps.modelResolver.contextWindowFor?.(source, model));
33635
34096
  const taskLogs = new TaskLogRecorder({
33636
34097
  broadcast: deps.broadcast,
33637
34098
  // Read through the Session, whose id and workspace are the ones it saves
@@ -33664,7 +34125,7 @@ function createSession(deps) {
33664
34125
  const orchestrator = TaskOrchestrator.compose({
33665
34126
  config: deps.config,
33666
34127
  notifications: deps.notifications,
33667
- runner: taskLogs.wrap(runnerApprovals.wrap(withContextWindows(deps.runner, catalogWindow))),
34128
+ runner: taskLogs.wrap(runnerApprovals.wrap(withContextWindows(deps.runner, windows))),
33668
34129
  store,
33669
34130
  output: deps.taskOutput,
33670
34131
  isolation: deps.isolation,
@@ -33676,7 +34137,7 @@ function createSession(deps) {
33676
34137
  runSettings: () => session.planState?.runSettings,
33677
34138
  isApproved: (taskId) => session.isApproved(taskId)
33678
34139
  });
33679
- const usage = new PlannerUsageLedger(catalogWindow);
34140
+ const usage = new PlannerUsageLedger(windows);
33680
34141
  const events = new SessionEventRelay({
33681
34142
  broadcast: deps.broadcast,
33682
34143
  onNotice: deps.onNotice,
@@ -33746,6 +34207,7 @@ function createSession(deps) {
33746
34207
  runs: orchestrator.runs,
33747
34208
  events,
33748
34209
  usage,
34210
+ windows,
33749
34211
  approvals,
33750
34212
  approvalPolicy,
33751
34213
  taskLogs,
@@ -33773,6 +34235,7 @@ function configSecrets(config) {
33773
34235
  var Session = class {
33774
34236
  backend;
33775
34237
  usage;
34238
+ windows;
33776
34239
  events;
33777
34240
  planner;
33778
34241
  orchestrator;
@@ -33878,6 +34341,7 @@ var Session = class {
33878
34341
  this.runs = parts.runs;
33879
34342
  this.events = parts.events;
33880
34343
  this.usage = parts.usage;
34344
+ this.windows = parts.windows;
33881
34345
  this.approvals = parts.approvals;
33882
34346
  this.approvalPolicy = parts.approvalPolicy;
33883
34347
  this.taskLogs = parts.taskLogs;
@@ -34356,6 +34820,7 @@ var Session = class {
34356
34820
  this.syncPlanTasks();
34357
34821
  this.plan.isolation = this.runs.planIsolation ?? void 0;
34358
34822
  this.plan.plannerUsage = this.usage.snapshot();
34823
+ this.plan.contextWindows = this.windows.snapshot();
34359
34824
  this.plan.supervision = this.supervision.snapshot();
34360
34825
  this.plan.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
34361
34826
  this.save(this.plan, this.goal, this.workspace, this.currentSessionId);
@@ -34385,6 +34850,7 @@ var Session = class {
34385
34850
  this.owingEffects.clear();
34386
34851
  this.conversation.reset();
34387
34852
  this.usage.clear();
34853
+ this.windows.clear();
34388
34854
  this.approvals.clear(isPlannerApproval);
34389
34855
  if (!keepGrants) this.approvalPolicy.reset();
34390
34856
  this.store.clearLog();
@@ -34725,7 +35191,18 @@ var Session = class {
34725
35191
  if (!this.plan) throw new ConversationEditError("No planning conversation to condense");
34726
35192
  const opening = this.plannerTurnMayOpen(signal);
34727
35193
  if (opening) await opening;
34728
- return this.conversation.compact(signal);
35194
+ const compaction = await this.conversation.compact(signal);
35195
+ const usage = this.usage.snapshot();
35196
+ if (usage.lastPromptTokens !== void 0) {
35197
+ this.usage.record({
35198
+ source: usage.source ?? this.config.aiProvider,
35199
+ ...usage.model ? { model: usage.model } : {},
35200
+ compacted: true
35201
+ });
35202
+ this.persist();
35203
+ this.broadcast(this.usage.message());
35204
+ }
35205
+ return compaction;
34729
35206
  }
34730
35207
  rewindTargets() {
34731
35208
  return this.conversation.rewindTargets();
@@ -34812,7 +35289,7 @@ var Session = class {
34812
35289
  */
34813
35290
  adoptPlannerTasks(tasks, how) {
34814
35291
  this.turnEffects.landed(how);
34815
- for (const runner of runnersOf(tasks)) this.editor.admitRunner(runner, this.catalog.known(runner));
35292
+ for (const runner of runnersOf2(tasks)) this.editor.admitRunner(runner, this.catalog.known(runner));
34816
35293
  const runners = this.plan.runners;
34817
35294
  const before = this.store.planTasks;
34818
35295
  let coerced = resolveAssignments(tasks, before, this.catalog.edit(runners)).tasks;
@@ -35035,9 +35512,9 @@ var Session = class {
35035
35512
  runSnapshot() {
35036
35513
  if (!this.plan) return EMPTY_RUN;
35037
35514
  const planned = reduceRun(EMPTY_RUN, { type: "plan_snapshot", plan: surfacePlan({ ...this.plan, tasks: this.store.snapshot() }) });
35038
- const view = reduceRun(planned, this.events.statusMessage());
35039
- const held2 = this.sentHeld ? reduceRun(view, { type: "plan_edit_held", held: this.sentHeld }) : view;
35040
- return this.sentHandoff ? reduceRun(held2, { type: "isolation_handoff", ...this.sentHandoff }) : held2;
35515
+ const handed = this.sentHandoff ? reduceRun(planned, { type: "isolation_handoff", ...this.sentHandoff }) : planned;
35516
+ const view = reduceRun(handed, this.events.statusMessage());
35517
+ return this.sentHeld ? reduceRun(view, { type: "plan_edit_held", held: this.sentHeld }) : view;
35041
35518
  }
35042
35519
  /** The ancestors a task waits on at its merge gate (ADR-0020); empty when it waits for no Merge all. */
35043
35520
  mergeGate(taskId) {
@@ -35213,7 +35690,10 @@ var Session = class {
35213
35690
  if (!plan.approvedTasks && plan.approved) {
35214
35691
  plan.approvedTasks = flattenTasks(plan.tasks).map((t) => t.id);
35215
35692
  }
35216
- if (adopting) this.usage.restore(plan.plannerUsage);
35693
+ if (adopting) {
35694
+ this.usage.restore(plan.plannerUsage);
35695
+ this.windows.restore(plan.contextWindows);
35696
+ }
35217
35697
  if (opts?.sessionId) this.currentSessionId = opts.sessionId;
35218
35698
  if (adopting) {
35219
35699
  for (const task of flattenTasks(plan.tasks)) delete task.resumeAt;
@@ -35227,12 +35707,31 @@ var Session = class {
35227
35707
  }
35228
35708
  carryOverQueuedEdits(plan);
35229
35709
  if (adopting) {
35230
- void this.runs.adopt(plan.isolation ?? null);
35710
+ void this.adoptRun(plan, opts?.persist !== false || opts?.sessionId !== void 0);
35231
35711
  this.sentHandoff = this.owedHandoff();
35232
35712
  }
35233
35713
  if (opts?.persist !== false) this.persist();
35234
35714
  if (this.usage.hasUsage) this.broadcast(this.usage.message());
35235
35715
  }
35716
+ /**
35717
+ * Take the loaded plan's run over, then correct what the surfaces were told
35718
+ * by what git found: the saved record cannot know what the user merged
35719
+ * since, and its handoff would offer Merge all for work already in their
35720
+ * branch. The status update says what landed work is still unmerged, which
35721
+ * a handoff folds to (see `reduceRun`), and `run_closed` that the run was
35722
+ * cleared up. Saved only where the load itself may save (`persists`), so a
35723
+ * restore with no session file to write keeps the correction in memory for
35724
+ * its next save. A plan loaded meanwhile is not this one's to correct.
35725
+ * Starts nothing (ADR-0044).
35726
+ */
35727
+ async adoptRun(plan, persists) {
35728
+ const outcome = await this.runs.adopt(plan.isolation ?? null);
35729
+ if (this.closed || this.plan !== plan || outcome === "unchanged") return;
35730
+ plan.isolation = this.runs.planIsolation ?? void 0;
35731
+ if (persists) this.persist();
35732
+ this.events.status(plan);
35733
+ if (outcome === "cleared") this.broadcast({ type: "run_closed" });
35734
+ }
35236
35735
  destroy() {
35237
35736
  this.closed = true;
35238
35737
  this.dropPlanScope();
@@ -35276,7 +35775,7 @@ function openTurn(view, { turnId, prompt = "", skills = [], wakeUps = [], forced
35276
35775
  wakeUps = wakeUps.slice(adopted.wakeUps);
35277
35776
  if (wakeUps.length === 0) return adopted.prompt || prompt === "" && adopted.wakeUps > 0 ? view : promptLine(view, turnId, prompt, skills, forced);
35278
35777
  const said2 = prompt === "" || adopted.prompt ? view : promptLine(view, turnId, prompt, skills, forced);
35279
- const at = prompt === "" ? said2.blocks.length : findLastIndex(said2.blocks, (b) => b.type === "message" && b.role === "user" && b.turnId === turnId);
35778
+ const at = prompt === "" ? said2.blocks.length - (said2.blocks.at(-1)?.type === "usage" ? 1 : 0) : findLastIndex(said2.blocks, (b) => b.type === "message" && b.role === "user" && b.turnId === turnId);
35280
35779
  const updates = wakeUps.map((text3, n) => settledMessage(`b${said2.nextId + n}`, "wake_up", text3, turnId));
35281
35780
  return { ...said2, blocks: [...said2.blocks.slice(0, at), ...updates, ...said2.blocks.slice(at)], nextId: said2.nextId + updates.length };
35282
35781
  }
@@ -35963,17 +36462,12 @@ var AbstractRunner = class {
35963
36462
  get activeCount() {
35964
36463
  return this.sessions.size;
35965
36464
  }
36465
+ /** A session winding down stays registered until it exits, so a second stop gets the same wind-down. */
35966
36466
  stop(sessionId) {
35967
- const session = this.sessions.get(sessionId);
35968
- if (!session) return;
35969
- session.kill();
35970
- this.sessions.delete(sessionId);
36467
+ return this.sessions.get(sessionId)?.stop() ?? Promise.resolve();
35971
36468
  }
35972
36469
  stopAll() {
35973
- for (const [, session] of this.sessions) {
35974
- session.kill();
35975
- }
35976
- this.sessions.clear();
36470
+ for (const session of this.sessions.values()) void session.stop();
35977
36471
  }
35978
36472
  registerSession(id, session) {
35979
36473
  this.sessions.set(id, session);
@@ -36131,6 +36625,8 @@ var StructuredSession = class extends AbstractRunnerSession {
36131
36625
  queue = [];
36132
36626
  messageCount = 0;
36133
36627
  interrupting = null;
36628
+ /** The wind-down once {@link stop} was asked for: the same for every caller, and nothing new is sent meanwhile. */
36629
+ stopping = null;
36134
36630
  lastSessionId = null;
36135
36631
  lastSessionCost = null;
36136
36632
  structuredEmitter = new import_events2.EventEmitter();
@@ -36156,7 +36652,7 @@ var StructuredSession = class extends AbstractRunnerSession {
36156
36652
  await this.startAttached(this.launch.startOptions);
36157
36653
  this.state = "working";
36158
36654
  setImmediate(() => {
36159
- if (!this.exited) this.deliver(prompt);
36655
+ if (!this.exited && !this.stopping) this.deliver(prompt);
36160
36656
  });
36161
36657
  }
36162
36658
  getOutput() {
@@ -36236,6 +36732,53 @@ var StructuredSession = class extends AbstractRunnerSession {
36236
36732
  this.withdrawPermissions();
36237
36733
  this.baseHandleExit(-1);
36238
36734
  }
36735
+ stop() {
36736
+ if (this.stopping) return this.stopping;
36737
+ if (this.exited) return Promise.resolve();
36738
+ let woundDown;
36739
+ this.stopping = new Promise((resolve9) => {
36740
+ woundDown = resolve9;
36741
+ });
36742
+ void this.windDown().catch(() => this.kill()).then(woundDown);
36743
+ return this.stopping;
36744
+ }
36745
+ /**
36746
+ * The attempt is over (ADR-0018, L1), so what is still owed goes undelivered
36747
+ * and every tool request unanswered at once. A turn still open is
36748
+ * interrupted, and its close — where Claude Code reports the turn's cost and
36749
+ * its model's window — is waited for; then the runner is told nothing more
36750
+ * is coming and left to exit by itself. All of it within the interrupt
36751
+ * grace: a runner still there once it runs out is killed.
36752
+ */
36753
+ async windDown() {
36754
+ const adapter = this.adapterAttached ? this.adapter : null;
36755
+ const deadline = Date.now() + this.launch.interruptGraceMs;
36756
+ this.revokeTools();
36757
+ this.undeliverQueued();
36758
+ this.withdrawPermissions();
36759
+ const open = this.turn && !this.turn.ended ? this.turn : null;
36760
+ if (adapter && (!open || await this.turnClosed(open, adapter, deadline)) && adapter.endInput && !this.exited) {
36761
+ adapter.endInput();
36762
+ await this.within(this.gone(), deadline);
36763
+ }
36764
+ if (this.exited) adapter?.dispose();
36765
+ else this.kill();
36766
+ }
36767
+ /** Whether `turn` closed within the deadline once interrupted. */
36768
+ async turnClosed(turn, adapter, deadline) {
36769
+ const closed = new Promise((resolve9) => this.structuredEmitter.once("turnEnd", () => resolve9()));
36770
+ adapter.interrupt(Math.max(0, deadline - Date.now())).catch(() => false);
36771
+ return await this.within(closed, deadline) && turn.ended;
36772
+ }
36773
+ /** Whether `done` settled before the deadline, or the session ended meanwhile. */
36774
+ within(done, deadline) {
36775
+ return settleWithin(done.then(() => true), { timeoutMs: Math.max(0, deadline - Date.now()), onTimeout: () => false, ended: this.gone(), onEnded: () => true });
36776
+ }
36777
+ gone() {
36778
+ const { signal } = this.ended;
36779
+ if (signal.aborted) return Promise.resolve();
36780
+ return new Promise((resolve9) => signal.addEventListener("abort", () => resolve9(), { once: true }));
36781
+ }
36239
36782
  turnState() {
36240
36783
  return this.state;
36241
36784
  }
@@ -36261,7 +36804,7 @@ var StructuredSession = class extends AbstractRunnerSession {
36261
36804
  this.messageCount += 1;
36262
36805
  const id = `msg-${this.messageCount}`;
36263
36806
  const sender2 = opening.from ? { from: opening.from } : {};
36264
- if (this.exited) this.emitEvent({ type: "message_undelivered", messageId: id, text: text3, ...sender2 });
36807
+ if (this.exited || this.stopping) this.emitEvent({ type: "message_undelivered", messageId: id, text: text3, ...sender2 });
36265
36808
  else if (this.state === "idle" && this.adapterStarted && !this.sending) this.deliver(text3, opening);
36266
36809
  else {
36267
36810
  this.state = "working";
@@ -36316,7 +36859,7 @@ var StructuredSession = class extends AbstractRunnerSession {
36316
36859
  });
36317
36860
  }
36318
36861
  runningTurn() {
36319
- return !this.exited && this.adapter !== null && this.turn !== null && !this.turn.ended;
36862
+ return !this.exited && !this.stopping && this.adapter !== null && this.turn !== null && !this.turn.ended;
36320
36863
  }
36321
36864
  /** No runner can recall a message once offered to it, so only a queued one comes back. */
36322
36865
  removeQueued(id) {
@@ -36377,7 +36920,7 @@ var StructuredSession = class extends AbstractRunnerSession {
36377
36920
  if (this.interrupting) return this.interrupting;
36378
36921
  const turn = this.turn;
36379
36922
  const adapter = this.adapter;
36380
- if (!turn || turn.ended || !adapter) return Promise.resolve();
36923
+ if (!turn || turn.ended || !adapter || this.stopping) return Promise.resolve();
36381
36924
  this.interrupting = this.interruptTurn(turn, adapter).finally(() => {
36382
36925
  this.interrupting = null;
36383
36926
  this.offerNext();
@@ -36391,7 +36934,7 @@ var StructuredSession = class extends AbstractRunnerSession {
36391
36934
  const settled = await settleWithin(ended.then(() => true), { timeoutMs: grace, onTimeout: () => false });
36392
36935
  if (settled) return;
36393
36936
  }
36394
- if (turn.ended || this.exited) return;
36937
+ if (turn.ended || this.exited || this.stopping) return;
36395
36938
  await this.restartInterrupted(turn);
36396
36939
  }
36397
36940
  /**
@@ -36495,7 +37038,7 @@ var StructuredSession = class extends AbstractRunnerSession {
36495
37038
  offerNext() {
36496
37039
  const adapter = this.adapter;
36497
37040
  const turn = this.turn;
36498
- if (!adapter?.steer || !turn || turn.ended || turn.steerRefused || this.interrupting || this.exited) return;
37041
+ if (!adapter?.steer || !turn || turn.ended || turn.steerRefused || this.interrupting || this.exited || this.stopping) return;
36499
37042
  if (this.queue.some((m) => m.stage === "steering")) return;
36500
37043
  const next = this.queue.find((m) => m.stage === "queued");
36501
37044
  if (!next || next.command) return;
@@ -36570,7 +37113,7 @@ var StructuredSession = class extends AbstractRunnerSession {
36570
37113
  }
36571
37114
  return;
36572
37115
  }
36573
- if (!turn && TURN_OPENING_EVENTS.has(event.type) && !("subagentId" in event && event.subagentId)) {
37116
+ if (!turn && !this.stopping && TURN_OPENING_EVENTS.has(event.type) && !("subagentId" in event && event.subagentId)) {
36574
37117
  turn = this.openTurn("");
36575
37118
  this.offerNext();
36576
37119
  }
@@ -36641,7 +37184,7 @@ var StructuredSession = class extends AbstractRunnerSession {
36641
37184
  * back in Ordewell's hands: while it is on offer or owed, nothing overtakes it.
36642
37185
  */
36643
37186
  deliverNext() {
36644
- if (this.exited || this.sending || this.turn && !this.turn.ended) return;
37187
+ if (this.exited || this.stopping || this.sending || this.turn && !this.turn.ended) return;
36645
37188
  const head = this.queue[0];
36646
37189
  if (!head || this.queue.some((message) => message.stage !== "queued")) return;
36647
37190
  this.queue.shift();
@@ -36810,11 +37353,13 @@ var STATE_FILE = "state.json";
36810
37353
  function getStatePath(baseDir) {
36811
37354
  return path34.join(getStateDir(baseDir), STATE_FILE);
36812
37355
  }
36813
- function saveState(plan, baseDir) {
37356
+ var SESSION_ID_KEY = "sessionId";
37357
+ function saveState(plan, baseDir, sessionId) {
36814
37358
  const statePath = getStatePath(baseDir ?? process.cwd());
36815
37359
  ensureDir(path34.dirname(statePath));
36816
37360
  plan.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
36817
- fs17.writeFileSync(statePath, JSON.stringify(plan, null, 2));
37361
+ const file = sessionId ? { ...plan, [SESSION_ID_KEY]: sessionId } : plan;
37362
+ fs17.writeFileSync(statePath, JSON.stringify(file, null, 2));
36818
37363
  }
36819
37364
  function loadState(baseDir, logger = defaultLogger) {
36820
37365
  const dir = baseDir ?? process.cwd();
@@ -36827,10 +37372,12 @@ function loadState(baseDir, logger = defaultLogger) {
36827
37372
  logger.warn("stateStore", `rejecting state from ${statePath}: old format (scalar "runner" instead of "runners" array). Please re-plan.`);
36828
37373
  return null;
36829
37374
  }
37375
+ const sessionId = parsed[SESSION_ID_KEY];
37376
+ delete parsed[SESSION_ID_KEY];
36830
37377
  const plan = parsed;
36831
37378
  migratePlanStateIsolation(plan);
36832
37379
  migrateLoadedTasks(plan.tasks);
36833
- return plan;
37380
+ return typeof sessionId === "string" && sessionId ? { plan, sessionId } : { plan };
36834
37381
  } catch (err) {
36835
37382
  logger.warn("stateStore", `failed to load state from ${statePath}; falling back to empty state`, err);
36836
37383
  return null;
@@ -37087,7 +37634,10 @@ var PlannerModelMemory = class {
37087
37634
  collectProviderCredentials,
37088
37635
  commandAllowedBy,
37089
37636
  configuredProviders,
37637
+ contextMeterText,
37638
+ contextMeterTitle,
37090
37639
  continuability,
37640
+ costText,
37091
37641
  countedSessionCost,
37092
37642
  createAiService,
37093
37643
  createEmptyPlan,
@@ -37130,7 +37680,9 @@ var PlannerModelMemory = class {
37130
37680
  flattenTasksWithParents,
37131
37681
  followTaskLog,
37132
37682
  followTurn,
37683
+ formatCost,
37133
37684
  formatSearchOutput,
37685
+ formatTokenCount,
37134
37686
  fromTranscript,
37135
37687
  getLatestSession,
37136
37688
  getProviderMeta,
@@ -37182,6 +37734,7 @@ var PlannerModelMemory = class {
37182
37734
  migratePlanState,
37183
37735
  mintDaemonToken,
37184
37736
  mintSessionId,
37737
+ modelCostTitle,
37185
37738
  namedRepos,
37186
37739
  newTaskFields,
37187
37740
  normalizeAgentArgs,
@@ -37267,6 +37820,7 @@ var PlannerModelMemory = class {
37267
37820
  stopTurn,
37268
37821
  stripAnsi,
37269
37822
  stripModelPrefix,
37823
+ subagentCostTitle,
37270
37824
  subtreeIds,
37271
37825
  summarizeToolCall,
37272
37826
  supervisorSummary,
@@ -37285,6 +37839,7 @@ var PlannerModelMemory = class {
37285
37839
  taskStartedNotice,
37286
37840
  taskStatusKind,
37287
37841
  taskUsageSummary,
37842
+ tasksTotalText,
37288
37843
  textHasTaskOps,
37289
37844
  textHasTaskQuery,
37290
37845
  titledRefs,
@@ -37302,6 +37857,7 @@ var PlannerModelMemory = class {
37302
37857
  unsendAll,
37303
37858
  unsendLatest,
37304
37859
  usageLine,
37860
+ usageText,
37305
37861
  waitsForApproval,
37306
37862
  walkPlan,
37307
37863
  wellKnownBinDirs,