@ordewell/core 0.10.9 → 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-C9W5mRYA.d.mts → IFileSystem-1PqRCjhu.d.mts} +21 -3
  2. package/dist/{IFileSystem-B8qRNC_6.d.ts → IFileSystem-lTRL_G9Z.d.ts} +21 -3
  3. package/dist/{ModeResolver-C1PN5FYG.d.mts → ModeResolver-6TQXVsde.d.mts} +1 -1
  4. package/dist/{ModeResolver-CaPnoU8N.d.ts → ModeResolver-CoJAJD-f.d.ts} +1 -1
  5. package/dist/{Task-hFfRS9rN.d.mts → Task-CfBM0wkV.d.mts} +94 -21
  6. package/dist/{Task-hFfRS9rN.d.ts → Task-CfBM0wkV.d.ts} +94 -21
  7. package/dist/{chunk-H7QX32BH.mjs → chunk-24EL4Z2W.mjs} +2 -2
  8. package/dist/{chunk-O6LQU3W6.mjs → chunk-ESKK5I4F.mjs} +138 -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 +119 -17
  14. package/dist/index.d.ts +119 -17
  15. package/dist/index.js +521 -88
  16. package/dist/index.js.map +1 -1
  17. package/dist/index.mjs +397 -72
  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-rLoYQlUH.d.mts → runSettingsText-C1i9wBG1.d.mts} +26 -15
  29. package/dist/{runSettingsText-CGKX3aeV.d.ts → runSettingsText-CWNrmF5U.d.ts} +26 -15
  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-EEE2GBLG.mjs.map +0 -1
  38. package/dist/chunk-O6LQU3W6.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.mjs CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  looksLikePlanAttempt,
3
3
  parsePlanJson,
4
4
  validatePlanTasks
5
- } from "./chunk-LTHKFBKM.mjs";
5
+ } from "./chunk-KWS3VEER.mjs";
6
6
  import {
7
7
  ACT_RISK,
8
8
  AUTORUN_RISK,
@@ -38,7 +38,10 @@ import {
38
38
  canSplitTask,
39
39
  capConflictFiles,
40
40
  classifyOutcome,
41
+ contextMeterText,
42
+ contextMeterTitle,
41
43
  continuability,
44
+ costText,
42
45
  countedSessionCost,
43
46
  dependentsNotice,
44
47
  diffRows,
@@ -49,6 +52,8 @@ import {
49
52
  executionSummary,
50
53
  followTaskLog,
51
54
  followTurn,
55
+ formatCost,
56
+ formatTokenCount,
52
57
  fromTranscript,
53
58
  hasHiddenDetail,
54
59
  holdPrompt,
@@ -59,6 +64,7 @@ import {
59
64
  markRequestFor,
60
65
  markdownHtml,
61
66
  markdownTokens,
67
+ modelCostTitle,
62
68
  namedRepos,
63
69
  newTaskFields,
64
70
  normalizeAgentArgs,
@@ -90,6 +96,7 @@ import {
90
96
  skippedLabel,
91
97
  startAttemptUsage,
92
98
  stopTurn,
99
+ subagentCostTitle,
93
100
  summarizeToolCall,
94
101
  supervisorSummary,
95
102
  surfacePlan,
@@ -103,6 +110,7 @@ import {
103
110
  taskStartedNotice,
104
111
  taskStatusKind,
105
112
  taskUsageSummary,
113
+ tasksTotalText,
106
114
  textHasTaskOps,
107
115
  titledRefs,
108
116
  toolHeadline,
@@ -110,8 +118,9 @@ import {
110
118
  unsendAll,
111
119
  unsendLatest,
112
120
  usageLine,
121
+ usageText,
113
122
  validateTaskEdit
114
- } from "./chunk-O6LQU3W6.mjs";
123
+ } from "./chunk-ESKK5I4F.mjs";
115
124
  import {
116
125
  PLAN_ENVELOPE_KEY,
117
126
  PlanParseError,
@@ -137,7 +146,7 @@ import {
137
146
  runnerModesFrom,
138
147
  stripTrailingCommas,
139
148
  unavailableTaskModeMessage
140
- } from "./chunk-H7QX32BH.mjs";
149
+ } from "./chunk-24EL4Z2W.mjs";
141
150
  import {
142
151
  SELF_REPO,
143
152
  absorbRemoval,
@@ -190,7 +199,7 @@ import {
190
199
  waitsForApproval,
191
200
  waitsForReset,
192
201
  walkPlan
193
- } from "./chunk-EEE2GBLG.mjs";
202
+ } from "./chunk-PFH7WFIB.mjs";
194
203
 
195
204
  // src/models/TaskLog.ts
196
205
  var HEAD_LINES = 60;
@@ -6028,7 +6037,7 @@ var RunnerApprovals = class {
6028
6037
  },
6029
6038
  stop: (sessionId) => {
6030
6039
  this.denySession(sessionId);
6031
- runner.stop(sessionId);
6040
+ return runner.stop(sessionId);
6032
6041
  },
6033
6042
  stopAll: () => {
6034
6043
  this.approvals.clear(isRunnerApproval);
@@ -9889,6 +9898,14 @@ function canUseToolAnswer({ input, suggestions }, decision) {
9889
9898
  }
9890
9899
  var MODEL_COMMAND = "model";
9891
9900
  var SUBAGENT_TOOLS = /* @__PURE__ */ new Set(["Agent", "Task"]);
9901
+ var VARIANT_SUFFIX = /^(.*?)(\[[^\]]*\])?$/;
9902
+ function sameModel(a, b) {
9903
+ if (a === void 0 || b === void 0) return false;
9904
+ if (a === b) return true;
9905
+ const [, aBase, aVariant] = VARIANT_SUFFIX.exec(a) ?? [];
9906
+ const [, bBase, bVariant] = VARIANT_SUFFIX.exec(b) ?? [];
9907
+ return aBase === bBase && (aVariant === void 0 || bVariant === void 0 || aVariant === bVariant);
9908
+ }
9892
9909
  function usageRecord(usage, model, subagentId) {
9893
9910
  const record = {
9894
9911
  source: "claude-code",
@@ -9897,6 +9914,7 @@ function usageRecord(usage, model, subagentId) {
9897
9914
  if (model) record.model = model;
9898
9915
  if (usage.output_tokens !== void 0) record.outputTokens = usage.output_tokens;
9899
9916
  if (subagentId) record.subagentId = subagentId;
9917
+ else if (record.inputTokens !== void 0) record.contextTokens = record.inputTokens + (record.outputTokens ?? 0);
9900
9918
  return record;
9901
9919
  }
9902
9920
  function subagentFinalCall(result) {
@@ -9946,16 +9964,27 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
9946
9964
  replyRuns = new ReplyRuns();
9947
9965
  /** The model answering the planner's current message, and the message's id, as its `message_start` named them. */
9948
9966
  plannerModel;
9967
+ /**
9968
+ * The id the turn runs as, as `init` names it. A `[1m]` variant is the
9969
+ * selected id, while a message names the API's canonical id without it.
9970
+ */
9971
+ sessionModel;
9972
+ billing;
9949
9973
  streamingMessage;
9950
9974
  /** The message the last `assistant` line was part of, and how many of its blocks the lines have carried. */
9951
9975
  lineMessage = null;
9952
9976
  /**
9953
- * The session's `total_cost_usd` as last counted. A resume starts from what
9954
- * was counted of that session before, and is undefined without it: the CLI
9955
- * restores the session's running total, which holds turns counted earlier.
9977
+ * What has been counted of the session's running cost: a resume starts from
9978
+ * it, and it is undefined without a kept count — the next result then only
9979
+ * sets the baseline, since the CLI restores spend from turns before it. It
9980
+ * grows by what each result is counted for, never by a total that includes
9981
+ * cost the runner marked unknown, so that spend can be counted when its basis
9982
+ * is reported; and it never lowers, since counted dollars are not un-spent.
9956
9983
  */
9957
9984
  reportedCostUsd = 0;
9958
9985
  reportedModelCostUsd = /* @__PURE__ */ new Map();
9986
+ /** What this turn's streamed calls already reported, so its result only adds what did not stream. */
9987
+ turnCallUsage = { inputTokens: 0, cachedInputTokens: 0, outputTokens: 0 };
9959
9988
  /**
9960
9989
  * Subagents started and not yet finished, keyed by the `Agent` call's id.
9961
9990
  * Kept across turns: a backgrounded one reports after its turn has ended.
@@ -10042,6 +10071,14 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
10042
10071
  if (!sessionId || this.reportedCostUsd === void 0) return null;
10043
10072
  return { sessionId, usd: this.reportedCostUsd, modelUsd: Object.fromEntries(this.reportedModelCostUsd) };
10044
10073
  }
10074
+ /**
10075
+ * A cleared conversation's totals start again from zero, so what follows is
10076
+ * all new spend, even for a resume that had no count kept.
10077
+ */
10078
+ restartCount() {
10079
+ this.reportedCostUsd = 0;
10080
+ this.reportedModelCostUsd = /* @__PURE__ */ new Map();
10081
+ }
10045
10082
  /** A count kept for another session says nothing of this one's. */
10046
10083
  countFrom(sessionId, counted) {
10047
10084
  const known = counted?.sessionId === sessionId ? counted : void 0;
@@ -10115,11 +10152,10 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
10115
10152
  * the alias resolves to now; that id is pinned before the first turn.
10116
10153
  */
10117
10154
  async handshake(opts) {
10118
- if (opts.kind !== "task") return;
10119
10155
  const initialized = (await this.control({ subtype: "initialize" }, MODEL_PIN_TIMEOUT_MS))?.response;
10120
10156
  this.listedCommands = initialized?.commands ?? [];
10121
10157
  this.listedModels = initialized?.models ?? [];
10122
- if (opts.flags.permissionMode !== "plan" || !opts.model) return;
10158
+ if (opts.kind !== "task" || opts.flags.permissionMode !== "plan" || !opts.model) return;
10123
10159
  const resolved = initialized?.models?.find((m) => m.value === opts.model)?.resolvedModel;
10124
10160
  if (resolved && resolved !== opts.model) await this.control({ subtype: "set_model", model: resolved }, MODEL_PIN_TIMEOUT_MS);
10125
10161
  }
@@ -10160,6 +10196,15 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
10160
10196
  this.interruptRequested = true;
10161
10197
  return (await this.control({ subtype: "interrupt" }, timeoutMs))?.subtype === "success";
10162
10198
  }
10199
+ /**
10200
+ * On stdin's end the CLI exits by itself once its turn has closed, and only
10201
+ * an exit of its own saves the session's running totals for a `--resume` to
10202
+ * restore (checked on 2.1.303: about a second later, code 0, or 1 after an
10203
+ * interrupted turn).
10204
+ */
10205
+ endInput() {
10206
+ this.process?.stdin?.end();
10207
+ }
10163
10208
  /**
10164
10209
  * A `user` line written mid-turn is Claude Code's own queue (ADR-0023): the
10165
10210
  * CLI attaches it to the next tool result, or runs it as a turn of its own
@@ -10247,6 +10292,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
10247
10292
  this.resultHeld = false;
10248
10293
  this.turnClosed = false;
10249
10294
  this.clearFollowOnTimer();
10295
+ this.plannerModel = void 0;
10250
10296
  }
10251
10297
  /**
10252
10298
  * The echo of a steered message: the model has it. After a closed turn it
@@ -10301,6 +10347,8 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
10301
10347
  this.permissions.withdraw(msg.request_id, emit);
10302
10348
  return;
10303
10349
  case "control_response": {
10350
+ const account = msg.response?.response?.account;
10351
+ if (account) this.reportBilling(account.apiKeySource, account.subscriptionType, emit);
10304
10352
  const requestId = msg.response?.request_id;
10305
10353
  if (requestId) this.pendingControl.get(requestId)?.(msg.response ?? null);
10306
10354
  return;
@@ -10344,12 +10392,24 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
10344
10392
  emit({ type: "tool_result", id, name: "", output: diff || output, success: block.is_error !== true, subagentId });
10345
10393
  }
10346
10394
  return;
10395
+ // `/clear` (and `/reset`, `/new`) emit this top-level line before their
10396
+ // own result, and a task's chat can send them. The CLI's totals restart.
10397
+ case "conversation_reset": {
10398
+ if (subagentId) return;
10399
+ this.restartCount();
10400
+ const resetModel = this.sessionModel ?? (this.plannerModel !== "<synthetic>" ? this.plannerModel : void 0);
10401
+ emit({ type: "usage", record: { source: "claude-code", ...resetModel ? { model: resetModel } : {}, compacted: true } });
10402
+ return;
10403
+ }
10347
10404
  case "system":
10405
+ if (subagentId && (msg.subtype === "init" || msg.subtype === "compact_boundary")) return;
10348
10406
  if (msg.subtype === "init") {
10349
10407
  this.clearFollowOnTimer();
10350
10408
  if (this.refuseOtherMode(msg, emit)) return;
10409
+ if (msg.model && msg.model !== "<synthetic>") this.sessionModel = msg.model;
10351
10410
  this.readLoaded(msg, emit);
10352
10411
  this.followModel(msg.model, emit);
10412
+ this.reportBilling(msg.apiKeySource, void 0, emit);
10353
10413
  }
10354
10414
  if (msg.subtype === "commands_changed" && Array.isArray(msg.commands)) this.listedCommands = msg.commands;
10355
10415
  if (msg.subtype === "compact_boundary") {
@@ -10360,6 +10420,8 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
10360
10420
  ...typeof meta?.pre_tokens === "number" ? { preTokens: meta.pre_tokens } : {},
10361
10421
  ...typeof meta?.post_tokens === "number" ? { postTokens: meta.post_tokens } : {}
10362
10422
  });
10423
+ const model = this.sessionModel ?? (this.plannerModel !== "<synthetic>" ? this.plannerModel : void 0);
10424
+ emit({ type: "usage", record: { source: "claude-code", ...model ? { model } : {}, compacted: true, ...typeof meta?.post_tokens === "number" ? { contextTokens: meta.post_tokens } : {} } });
10363
10425
  }
10364
10426
  if (msg.subtype === "background_tasks_changed" && Array.isArray(msg.tasks)) {
10365
10427
  this.backgroundTaskCount = msg.tasks.length;
@@ -10371,6 +10433,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
10371
10433
  }
10372
10434
  return;
10373
10435
  case "result":
10436
+ if (subagentId) return;
10374
10437
  this.reportSessionCost(msg, emit);
10375
10438
  for (const command of this.modelCommands) command.turnsLeft = Math.max(0, command.turnsLeft - 1);
10376
10439
  if (this.interruptRequested && (msg.is_error || msg.subtype !== "success")) {
@@ -10535,16 +10598,69 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
10535
10598
  */
10536
10599
  handleStreamEvent(event, emit) {
10537
10600
  if (event.type === "message_start") {
10538
- this.plannerModel = event.message?.model;
10601
+ this.plannerModel = this.modelId(event.message?.model);
10539
10602
  this.streamingMessage = event.message?.id;
10540
10603
  this.followModel(this.plannerModel, emit);
10541
- } else if (event.type === "message_delta" && event.usage) emit({ type: "usage", record: usageRecord(event.usage, this.plannerModel) });
10542
- else if (event.type === "content_block_delta" && event.delta?.type === "text_delta" && event.delta.text) {
10604
+ } else if (event.type === "message_delta" && event.usage) {
10605
+ const record = usageRecord(event.usage, this.plannerModel);
10606
+ this.turnCallUsage.inputTokens += record.inputTokens ?? 0;
10607
+ this.turnCallUsage.cachedInputTokens += record.cachedInputTokens ?? 0;
10608
+ this.turnCallUsage.outputTokens += record.outputTokens ?? 0;
10609
+ emit({ type: "usage", record });
10610
+ } else if (event.type === "content_block_delta" && event.delta?.type === "text_delta" && event.delta.text) {
10543
10611
  this.replyRuns.delta(`${this.streamingMessage ?? ""}:${event.index ?? 0}`, event.delta.text, emit);
10544
10612
  } else if (event.type === "content_block_delta" && event.delta?.type === "thinking_delta" && event.delta.thinking) {
10545
10613
  emit({ type: "thinking_delta", text: event.delta.thinking });
10546
10614
  }
10547
10615
  }
10616
+ /**
10617
+ * The id a reported model runs as. `init` names the selected value — a
10618
+ * `[1m]` variant included — while a message names the API's canonical id
10619
+ * without it. Keeping the variant is what lets the meter find the window
10620
+ * `modelUsage` keys it by, and keeps that window off the base model.
10621
+ */
10622
+ modelId(reported) {
10623
+ if (!reported) return void 0;
10624
+ if (!sameModel(this.sessionModel, reported)) return reported;
10625
+ return this.sessionModel?.includes("[") ? this.sessionModel : reported;
10626
+ }
10627
+ reportBilling(apiKeySource, plan, emit) {
10628
+ const type = apiKeySource ? apiKeySource === "none" ? "subscription" : "api-key" : plan ? "subscription" : void 0;
10629
+ if (!type) return;
10630
+ const knownPlan = plan ?? (this.billing?.type === type ? this.billing.plan : void 0);
10631
+ this.billing = { type, ...knownPlan ? { plan: knownPlan } : {} };
10632
+ emit({ type: "usage", record: { source: this.agentId, aggregate: true, sessionBilling: this.billing } });
10633
+ }
10634
+ /**
10635
+ * A turn's `result` states its own token sum. Every call that streamed its
10636
+ * `message_delta` usage has already reported, so only the remainder is added:
10637
+ * all of it when nothing streamed (a recording without partial messages), or
10638
+ * a lost call's share. It is a sum across calls, marked `aggregate` so it
10639
+ * never moves the meter — only a call says what the context was left at.
10640
+ */
10641
+ reportUnstreamedUsage(msg, emit) {
10642
+ const { inputTokens: streamedInput, cachedInputTokens: streamedCached, outputTokens: streamedOutput } = this.turnCallUsage;
10643
+ this.turnCallUsage = { inputTokens: 0, cachedInputTokens: 0, outputTokens: 0 };
10644
+ if (!msg.usage) return;
10645
+ const models = Object.keys(msg.modelUsage ?? {});
10646
+ const model = this.plannerModel ?? this.mainModel ?? (models.length === 1 ? models[0] : void 0);
10647
+ const total = usageRecord(msg.usage, model);
10648
+ const inputTokens = (total.inputTokens ?? 0) - streamedInput;
10649
+ const cachedInputTokens = (total.cachedInputTokens ?? 0) - streamedCached;
10650
+ const outputTokens = (total.outputTokens ?? 0) - streamedOutput;
10651
+ if (inputTokens <= 0 && outputTokens <= 0) return;
10652
+ emit({
10653
+ type: "usage",
10654
+ record: {
10655
+ source: "claude-code",
10656
+ ...model ? { model } : {},
10657
+ aggregate: true,
10658
+ ...inputTokens > 0 ? { inputTokens } : {},
10659
+ ...inputTokens > 0 && cachedInputTokens > 0 ? { cachedInputTokens } : {},
10660
+ ...outputTokens > 0 ? { outputTokens } : {}
10661
+ }
10662
+ });
10663
+ }
10548
10664
  /**
10549
10665
  * What only the result line knows: the cost, which covers every call the
10550
10666
  * session made — subagents included, since none of their lines carries one —
@@ -10553,24 +10669,54 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
10553
10669
  * the session before; with no count kept, the first turn only sets the
10554
10670
  * baseline: its total includes turns counted before, and a turn's own share
10555
10671
  * cannot be told apart from them.
10672
+ *
10673
+ * The running total never lowers: money already reported and counted is not
10674
+ * un-spent because a later or resumed process remembers less of it, and
10675
+ * counting its return would count the same dollars twice. A zero is not a
10676
+ * measurement at all — a crash can end with one — and only `/clear` restarts
10677
+ * the count, through {@link restartCount}. Per-model baselines never lower
10678
+ * either, for the same reason.
10679
+ *
10680
+ * A report without a model breakdown is counted as a session total; one with
10681
+ * a breakdown is counted per model. A breakdown that would count more than
10682
+ * the session total's own growth (the count before it had no model split, or
10683
+ * a lossy report) is counted as that growth alone, so the two shapes
10684
+ * together still add each dollar once.
10556
10685
  */
10557
10686
  reportSessionCost(msg, emit) {
10687
+ this.reportUnstreamedUsage(msg, emit);
10558
10688
  const total = msg.total_cost_usd;
10559
10689
  const canReport = this.reportedCostUsd !== void 0;
10560
- const modelCosts = Object.entries(msg.modelUsage ?? {}).filter((entry) => typeof entry[1].costUSD === "number");
10690
+ const modelCosts = Object.entries(msg.modelUsage ?? {}).filter((entry) => typeof entry[1].costUSD === "number" && entry[1].costUSD > 0 && entry[1].costBasis !== "unknown");
10691
+ const growth = canReport && typeof total === "number" && total > 0 ? total - this.reportedCostUsd : void 0;
10692
+ let emitted = 0;
10693
+ const emitCost = (record) => {
10694
+ if (!(record.amount > 0)) return;
10695
+ emitted += record.amount;
10696
+ 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 } : {} } } });
10697
+ };
10561
10698
  if (modelCosts.length > 0) {
10562
- for (const [model, usage] of modelCosts) {
10563
- const previous = this.reportedModelCostUsd.get(model) ?? 0;
10564
- const amount = usage.costUSD - previous;
10565
- if (canReport && amount >= 0) emit({ type: "usage", record: { source: "claude-code", model, aggregate: true, reportedCost: { amount, currency: "USD" } } });
10566
- this.reportedModelCostUsd.set(model, usage.costUSD);
10699
+ const modelGrowth = modelCosts.reduce((sum, [model2, usage]) => sum + Math.max(0, usage.costUSD - (this.reportedModelCostUsd.get(model2) ?? 0)), 0);
10700
+ if (canReport) {
10701
+ if (growth !== void 0 && modelGrowth > growth + 1e-9) {
10702
+ emitCost({ amount: growth });
10703
+ } else {
10704
+ for (const [model2, usage] of modelCosts) emitCost({ model: model2, amount: usage.costUSD - (this.reportedModelCostUsd.get(model2) ?? 0), ...usage.costBasis ? { basis: usage.costBasis } : {} });
10705
+ }
10567
10706
  }
10568
- } else if (typeof total === "number" && canReport && total >= this.reportedCostUsd) {
10569
- emit({ type: "usage", record: { source: "claude-code", aggregate: true, reportedCost: { amount: total - this.reportedCostUsd, currency: "USD" } } });
10707
+ for (const [model2, usage] of modelCosts) this.reportedModelCostUsd.set(model2, Math.max(this.reportedModelCostUsd.get(model2) ?? 0, usage.costUSD));
10708
+ } else if (!Object.values(msg.modelUsage ?? {}).some((usage) => usage.costBasis === "unknown") && typeof total === "number" && total > 0 && canReport && total >= this.reportedCostUsd) {
10709
+ emitCost({ amount: total - this.reportedCostUsd });
10710
+ }
10711
+ if (!canReport) {
10712
+ if (typeof total === "number" && total > 0) this.reportedCostUsd = total;
10713
+ else if (modelCosts.length > 0) this.reportedCostUsd = modelCosts.reduce((sum, [, usage]) => sum + usage.costUSD, 0);
10714
+ } else {
10715
+ this.reportedCostUsd = (this.reportedCostUsd ?? 0) + emitted;
10570
10716
  }
10571
- if (typeof total === "number") this.reportedCostUsd = total;
10572
- const contextWindow = this.plannerModel ? msg.modelUsage?.[this.plannerModel]?.contextWindow : void 0;
10573
- if (contextWindow !== void 0) emit({ type: "usage", record: { source: "claude-code", model: this.plannerModel, contextWindow } });
10717
+ const model = this.plannerModel && this.plannerModel !== "<synthetic>" ? this.plannerModel : this.sessionModel;
10718
+ const contextWindow = model ? msg.modelUsage?.[model]?.contextWindow : void 0;
10719
+ if (typeof contextWindow === "number" && contextWindow > 0) emit({ type: "usage", record: { source: "claude-code", model, contextWindow } });
10574
10720
  }
10575
10721
  };
10576
10722
 
@@ -11205,9 +11351,13 @@ function mergeCodexRateLimits(known, params) {
11205
11351
  const id = snapshot.limitId ?? "";
11206
11352
  const before = known.get(id);
11207
11353
  const next = new Map(known);
11208
- next.set(id, { limitId: id, primary: snapshot.primary ?? before?.primary, secondary: snapshot.secondary ?? before?.secondary });
11354
+ next.set(id, { limitId: id, planType: snapshot.planType ?? before?.planType, primary: snapshot.primary ?? before?.primary, secondary: snapshot.secondary ?? before?.secondary });
11209
11355
  return next;
11210
11356
  }
11357
+ function codexSessionBilling(limits) {
11358
+ const plan = [...limits.values()].find((snapshot) => snapshot.planType && snapshot.planType !== "unknown")?.planType;
11359
+ return plan ? { type: "subscription", plan } : void 0;
11360
+ }
11211
11361
  function isCodexUsageLimit(error) {
11212
11362
  const info = error?.codexErrorInfo;
11213
11363
  if (info === void 0 || info === null || info === "other") return void 0;
@@ -11766,8 +11916,21 @@ ${this.exitMessage()}`);
11766
11916
  case "thread/tokenUsage/updated":
11767
11917
  this.emitUsage(msg.params, emit);
11768
11918
  return;
11919
+ case "account/updated": {
11920
+ const account = msg.params;
11921
+ if (account?.authMode === "apikey") emit({ type: "usage", record: { source: this.agentId, aggregate: true, sessionBilling: { type: "api-key" } } });
11922
+ else if (account?.authMode === "chatgpt" || account?.authMode === "chatgptAuthTokens") {
11923
+ const plan = account.planType;
11924
+ emit({ type: "usage", record: { source: this.agentId, aggregate: true, sessionBilling: { type: "subscription", ...plan && plan !== "unknown" ? { plan } : {} } } });
11925
+ }
11926
+ return;
11927
+ }
11769
11928
  case "account/rateLimits/updated":
11770
11929
  this.rateLimits = mergeCodexRateLimits(this.rateLimits, msg.params);
11930
+ {
11931
+ const sessionBilling = codexSessionBilling(this.rateLimits);
11932
+ if (sessionBilling) emit({ type: "usage", record: { source: this.agentId, aggregate: true, sessionBilling } });
11933
+ }
11771
11934
  return;
11772
11935
  case "turn/started": {
11773
11936
  if (this.subagentOf(msg.params?.threadId)) return;
@@ -11896,6 +12059,13 @@ ${this.exitMessage()}`);
11896
12059
  * `last.outputTokens` already includes `reasoningOutputTokens` — the thread
11897
12060
  * total is `inputTokens + outputTokens`, not a sum of three — so reasoning is
11898
12061
  * not added a second time. Codex reports no price.
12062
+ *
12063
+ * The main thread's context is what Codex itself shows as in its window:
12064
+ * `TokenUsage::tokens_in_context_window`, which is `total_tokens` over
12065
+ * `last` as codex-rs defines it. `total_tokens` is input plus output, and
12066
+ * reasoning is part of output, so this is not `input + output − reasoning`
12067
+ * (checked against codex-rs and codex-cli 0.163.0's token_count events,
12068
+ * where `total_tokens` is `input_tokens + output_tokens`).
11899
12069
  */
11900
12070
  emitUsage(params, emit) {
11901
12071
  const last = params?.tokenUsage?.last;
@@ -11923,6 +12093,8 @@ ${this.exitMessage()}`);
11923
12093
  } else {
11924
12094
  const window = params?.tokenUsage?.modelContextWindow;
11925
12095
  if (typeof window === "number" && window > 0) record.contextWindow = window;
12096
+ const inContext = last.totalTokens !== void 0 && last.totalTokens > 0 ? last.totalTokens : last.inputTokens !== void 0 ? last.inputTokens + (last.outputTokens ?? 0) : void 0;
12097
+ if (inContext !== void 0) record.contextTokens = Math.max(0, inContext);
11926
12098
  }
11927
12099
  emit({ type: "usage", record });
11928
12100
  }
@@ -12084,7 +12256,9 @@ ${this.exitMessage()}`);
12084
12256
  }
12085
12257
  switch (item.type) {
12086
12258
  case "contextCompaction":
12087
- if (!subagentId) emit({ type: "compaction", trigger: this.command === "compact" ? "manual" : "auto" });
12259
+ if (subagentId) return;
12260
+ emit({ type: "compaction", trigger: this.command === "compact" ? "manual" : "auto" });
12261
+ emit({ type: "usage", record: { source: this.agentId, compacted: true } });
12088
12262
  return;
12089
12263
  // Codex announces the item started and completed; whichever lands first delivers.
12090
12264
  case "userMessage":
@@ -12380,8 +12554,9 @@ function usageRecord2(tokens, call) {
12380
12554
  if ((prompt.inputTokens ?? 0) + outputTokens === 0) return null;
12381
12555
  const record = { source: "opencode", ...prompt, outputTokens };
12382
12556
  if (call.model) record.model = call.model;
12383
- if (typeof call.cost === "number" && call.cost > 0) record.reportedCost = { amount: call.cost, currency: "USD" };
12557
+ if (typeof call.cost === "number" && call.cost > 0) record.reportedCost = { amount: call.cost, currency: "USD", kind: "estimate" };
12384
12558
  if (call.subagentId) record.subagentId = call.subagentId;
12559
+ else record.contextTokens = (prompt.inputTokens ?? 0) + outputTokens;
12385
12560
  return record;
12386
12561
  }
12387
12562
  function permissionReply(decision, key) {
@@ -12944,6 +13119,7 @@ var OpenCodeV2 = class {
12944
13119
  }
12945
13120
  if (frame.type === "session.compaction.ended") {
12946
13121
  onEvent({ type: "compaction", trigger: data.reason === "manual" ? "manual" : "auto" });
13122
+ onEvent({ type: "usage", record: { source: "opencode", compacted: true } });
12947
13123
  return;
12948
13124
  }
12949
13125
  if (frame.type === "session.execution.started") {
@@ -13809,6 +13985,7 @@ var OpenCodeAdapter = class {
13809
13985
  if (session && session !== this.sessionId && !turn.children.follows(session)) return;
13810
13986
  if (frame.type === "session.compacted" && session === this.sessionId) {
13811
13987
  onEvent({ type: "compaction", trigger: this.manualCompaction ? "manual" : "auto" });
13988
+ onEvent({ type: "usage", record: { source: "opencode", compacted: true } });
13812
13989
  return;
13813
13990
  }
13814
13991
  if (session === this.sessionId && this.taskTurn) this.followTaskTurn(frame, this.taskTurn, onEvent);
@@ -17363,6 +17540,7 @@ var PlannerConversation = class {
17363
17540
  const request = summaryRequest(this.currentPlanLines());
17364
17541
  const onProgress = (p) => {
17365
17542
  if (p.type === "liveness") this.host.onProgress(p);
17543
+ else if (p.type === "usage" && p.record) this.host.onProgress({ ...p, record: { ...p.record, aggregate: true } });
17366
17544
  };
17367
17545
  let turn;
17368
17546
  if (this.canContinueLive()) {
@@ -20979,6 +21157,9 @@ function nonPromptingFs(fs18) {
20979
21157
  }
20980
21158
  };
20981
21159
  }
21160
+ function withoutContext({ contextTokens: _context, ...record }) {
21161
+ return record;
21162
+ }
20982
21163
  async function runLoop(prompt, deps) {
20983
21164
  if (deps.signal?.aborted) return "[research agent aborted before starting]";
20984
21165
  const fs18 = nonPromptingFs(deps.fs);
@@ -20986,7 +21167,8 @@ async function runLoop(prompt, deps) {
20986
21167
  (delta) => deps.onProgress?.({ type: "thinking", text: delta, subagentId: deps.subagentId }),
20987
21168
  // The record says who made the call: unstamped, the ledger would read the
20988
21169
  // subagent's prompt as the planner's own and measure its context by it.
20989
- (record) => deps.onProgress?.({ type: "usage", record: deps.subagentId ? { ...record, subagentId: deps.subagentId } : record })
21170
+ // The chat states the context its call leaves; a subagent's is no meter's.
21171
+ (record) => deps.onProgress?.({ type: "usage", record: deps.subagentId ? { ...withoutContext(record), subagentId: deps.subagentId } : record })
20990
21172
  );
20991
21173
  let turn = await chat.sendMessage(prompt, deps.signal);
20992
21174
  for (let step = 0; turn.hasToolCalls && step < SUBAGENT_LIMITS.maxSteps; step++) {
@@ -21496,7 +21678,8 @@ function usageRecordFrom(usage, model) {
21496
21678
  model,
21497
21679
  ...inputTokens !== void 0 ? { inputTokens } : {},
21498
21680
  ...reportedOutput.length > 0 ? { outputTokens: reportedOutput.reduce((a, b) => a + b, 0) } : {},
21499
- ...tokenCount(usage.cachedContentTokenCount) !== void 0 ? { cachedInputTokens: tokenCount(usage.cachedContentTokenCount) } : {}
21681
+ ...tokenCount(usage.cachedContentTokenCount) !== void 0 ? { cachedInputTokens: tokenCount(usage.cachedContentTokenCount) } : {},
21682
+ ...inputTokens !== void 0 ? { contextTokens: inputTokens + (answerTokens ?? 0) } : {}
21500
21683
  };
21501
21684
  const anyReported = record.inputTokens !== void 0 || record.outputTokens !== void 0 || record.cachedInputTokens !== void 0;
21502
21685
  return anyReported ? record : void 0;
@@ -21899,7 +22082,9 @@ var OpenAiResearchChat = class {
21899
22082
  /**
21900
22083
  * The one record for this API call, built only from what the provider
21901
22084
  * reported. OpenRouter's `cost` is in USD (its credits are dollar-pegged);
21902
- * nothing is estimated. A field the provider left out stays absent.
22085
+ * nothing is estimated. A field the provider left out stays absent. The
22086
+ * context the call leaves is its prompt and its completion: the API's own
22087
+ * `total_tokens`, and the reply the next call's prompt carries.
21903
22088
  */
21904
22089
  usageRecord(reported) {
21905
22090
  if (!reported) return void 0;
@@ -21918,9 +22103,10 @@ var OpenAiResearchChat = class {
21918
22103
  hasMeasure = true;
21919
22104
  }
21920
22105
  if (reported.cost !== void 0) {
21921
- record.reportedCost = { amount: reported.cost, currency: "USD" };
22106
+ record.reportedCost = { amount: reported.cost, currency: "USD", ...record.source === "openrouter" ? { kind: "billed" } : {} };
21922
22107
  hasMeasure = true;
21923
22108
  }
22109
+ if (record.inputTokens !== void 0) record.contextTokens = record.inputTokens + (record.outputTokens ?? 0);
21924
22110
  return hasMeasure ? record : void 0;
21925
22111
  }
21926
22112
  };
@@ -24091,6 +24277,10 @@ var TaskOrchestrator = class _TaskOrchestrator {
24091
24277
  store;
24092
24278
  output;
24093
24279
  attempts = /* @__PURE__ */ new Map();
24280
+ /** Bumped by every plan load: an ended attempt's wind-down belongs to the scope before it. */
24281
+ planScope = 0;
24282
+ /** Each task's runners still winding down once stopped: a continue waits for them (ADR-0018, L1). */
24283
+ windDowns = /* @__PURE__ */ new Map();
24094
24284
  verifier = new VerdictEngine();
24095
24285
  /** Changed only by start, stop, a plan load, a usage-limit pause, a move off the limit, and {@link settleRun}. */
24096
24286
  runState = "idle";
@@ -24639,8 +24829,9 @@ var TaskOrchestrator = class _TaskOrchestrator {
24639
24829
  return blockingFailures(this.store.planTasks).get(taskId) ?? [];
24640
24830
  }
24641
24831
  loadPlan(tasks, planRunners = [...DEFAULT_RUNNERS]) {
24642
- this.store.load(tasks, planRunners);
24643
24832
  const repairs = this.endAllAttempts("load").filter((a) => a.kind.kind === "repair" && a.worktree);
24833
+ this.planScope += 1;
24834
+ this.store.load(tasks, planRunners);
24644
24835
  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}"`);
24645
24836
  this.runs.interrupt({ keepOpen: this.isScheduling });
24646
24837
  if (this.runState !== "scheduling") this.runState = "idle";
@@ -24726,6 +24917,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
24726
24917
  if (reason === "user") this.stopGeneration++;
24727
24918
  this.runState = "idle";
24728
24919
  this.verifier.reset();
24920
+ for (const attempt of this.attempts.values()) this.stopRunner(attempt);
24729
24921
  this.runner.stopAll();
24730
24922
  const stopped = [];
24731
24923
  for (const a of this.endAllAttempts("stop")) {
@@ -24839,6 +25031,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
24839
25031
  if (!task || !attempt) return;
24840
25032
  attempt.decided = true;
24841
25033
  const stoppedMidTurn = endedByExit(verdict) ? saidInFlight(attempt) : null;
25034
+ this.stopRunner(attempt);
24842
25035
  console.error(`[TaskOrchestrator] Task ${taskRefOf(this.store.planTasks, task.id) ?? `#${task.order}`} "${task.title}" verdict=${verdict.outcome}`);
24843
25036
  console.error(`[TaskOrchestrator] Runner: ${task.assignedRunner}, Model: ${task.assignedModel?.modelId ?? "default"}`);
24844
25037
  console.error(`[TaskOrchestrator] Prompt preview: ${(task.prompt ?? "").slice(0, 200)}`);
@@ -25750,7 +25943,9 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
25750
25943
  autoResumed: opts.autoResumed === true,
25751
25944
  turnError: null,
25752
25945
  turnOutputFrom: 0,
25753
- reminded: false
25946
+ reminded: false,
25947
+ runnerStopped: false,
25948
+ planScope: this.planScope
25754
25949
  };
25755
25950
  this.spawnCounts.set(task.id, attempt.attempt);
25756
25951
  this.attempts.set(task.id, attempt);
@@ -25787,6 +25982,7 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
25787
25982
  const manifest = this.registry?.get(attempt.runner)?.manifest;
25788
25983
  const unavailable = manifest && unavailableTaskModeMessage(manifest, task.taskMode);
25789
25984
  if (unavailable) throw new Error(unavailable);
25985
+ if (kind.kind === "continuation") await this.windDowns.get(task.id);
25790
25986
  const { cwd, worktree, previousWork } = await attemptCwd(kind, task, this.runs);
25791
25987
  attempt.cwd = cwd;
25792
25988
  attempt.worktree = worktree;
@@ -25842,10 +26038,11 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
25842
26038
  }
25843
26039
  if (takesSkills(kind)) this.store.setTaskAttemptSkills(task.id, [...attempt.skills]);
25844
26040
  session.onEvent((event) => {
25845
- if (event.type !== "usage" || this.attempts.get(task.id) !== attempt) return;
26041
+ if (event.type !== "usage" || attempt.planScope !== this.planScope || this.attempts.get(task.id) !== attempt && !attempt.runnerStopped) return;
25846
26042
  this.store.recordAttemptUsage(task.id, attempt.attempt, event.record, (/* @__PURE__ */ new Date()).toISOString());
25847
26043
  this.emit("onUsageChanged");
25848
26044
  });
26045
+ session.onExit(() => this.woundDown(attempt, session));
25849
26046
  this.output.attach(task.id, session);
25850
26047
  this.verifier.watch(task, session);
25851
26048
  session.onEvent((event) => {
@@ -26014,10 +26211,35 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
26014
26211
  if (stopsRunner(reason)) {
26015
26212
  const task = this.store.get(taskId);
26016
26213
  if (task) this.verifier.clear(task);
26017
- if (session) this.runner.stop(session.id);
26214
+ if (attempt) this.stopRunner(attempt);
26018
26215
  }
26019
26216
  return attempt;
26020
26217
  }
26218
+ /**
26219
+ * Let the attempt's runner go the way it ends a turn (ADR-0018, L1): it
26220
+ * winds down on its own time, bounded by its interrupt grace, and nothing
26221
+ * here waits on it but a continue of the same task.
26222
+ */
26223
+ stopRunner(attempt) {
26224
+ const session = attempt.session;
26225
+ if (!session || attempt.runnerStopped) return;
26226
+ attempt.runnerStopped = true;
26227
+ const { taskId } = attempt;
26228
+ const windDown = Promise.all([this.windDowns.get(taskId), this.runner.stop(session.id)]).catch(() => void 0).then(() => {
26229
+ if (this.windDowns.get(taskId) === windDown) this.windDowns.delete(taskId);
26230
+ });
26231
+ this.windDowns.set(taskId, windDown);
26232
+ }
26233
+ /**
26234
+ * An ended attempt's runner is gone: the count its session ended on is the
26235
+ * one a continue of that session counts from. One still current is ended
26236
+ * with that count instead.
26237
+ */
26238
+ woundDown(attempt, session) {
26239
+ if (attempt.planScope !== this.planScope || !attempt.runnerStopped || this.attempts.get(attempt.taskId) === attempt) return;
26240
+ this.store.endAttemptUsage(attempt.taskId, attempt.attempt, (/* @__PURE__ */ new Date()).toISOString(), session.sessionCost?.() ?? void 0, session.nativeSessionId() ?? void 0);
26241
+ this.emit("onUsageChanged");
26242
+ }
26021
26243
  /** Kept on the task once the attempt ends, so a continue can resume it after a reload (ADR-0018, K1). */
26022
26244
  saveNativeSession(taskId, nativeSessionId, runner) {
26023
26245
  if (nativeSessionId) this.store.setTaskRunnerSessionId(taskId, nativeSessionId, runner);
@@ -28752,19 +28974,50 @@ var TaskLogRecorder = class {
28752
28974
  };
28753
28975
 
28754
28976
  // src/services/ContextWindowFill.ts
28755
- var ContextWindowFill = class {
28756
- constructor(catalog, last = {}) {
28977
+ var SessionWindows = class {
28978
+ constructor(catalog = () => void 0) {
28757
28979
  this.catalog = catalog;
28758
- if (last.contextWindow !== void 0) this.known.set(modelKey(last.source, last.model), last.contextWindow);
28759
28980
  }
28760
28981
  catalog;
28761
- /** The window known for each model of the stream, by reporter and model. */
28982
+ reported = /* @__PURE__ */ new Map();
28983
+ report(source, model, contextWindow) {
28984
+ if (contextWindow > 0) this.reported.set(modelKey(source, model), { source, model, contextWindow });
28985
+ }
28986
+ get(source, model) {
28987
+ return this.reported.get(modelKey(source, model))?.contextWindow;
28988
+ }
28989
+ snapshot() {
28990
+ return [...this.reported.values()];
28991
+ }
28992
+ restore(saved) {
28993
+ this.reported.clear();
28994
+ for (const { source, model, contextWindow } of saved ?? []) this.report(source, model, contextWindow);
28995
+ }
28996
+ clear() {
28997
+ this.reported.clear();
28998
+ }
28999
+ };
29000
+ var ContextWindowFill = class {
29001
+ constructor(windows, last = {}) {
29002
+ this.windows = windows;
29003
+ if (last.contextWindow !== void 0) this.known.set(modelKey(last.source, last.model), last.contextWindow);
29004
+ }
29005
+ windows;
29006
+ /** The window this stream used for each of its models, by reporter and model. */
28762
29007
  known = /* @__PURE__ */ new Map();
28763
29008
  fill(record) {
28764
29009
  if (record.subagentId || record.aggregate) return record;
28765
29010
  const key = modelKey(record.source, record.model);
29011
+ if (record.compacted) {
29012
+ if (!record.model) return record;
29013
+ const known = this.windows.get(record.source, record.model) ?? this.known.get(key) ?? positive(this.windows.catalog(record.source, record.model));
29014
+ if (known === void 0) return record;
29015
+ this.known.set(key, known);
29016
+ return { ...record, contextWindow: known };
29017
+ }
28766
29018
  const reported = record.contextWindow !== void 0 && record.contextWindow > 0 ? record.contextWindow : void 0;
28767
- const window = reported ?? this.known.get(key) ?? (record.model ? positive(this.catalog(record.source, record.model)) : void 0);
29019
+ if (reported !== void 0 && record.model) this.windows.report(record.source, record.model, reported);
29020
+ 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);
28768
29021
  if (window !== void 0) this.known.set(key, window);
28769
29022
  return window === void 0 || window === record.contextWindow ? record : { ...record, contextWindow: window };
28770
29023
  }
@@ -28772,9 +29025,9 @@ var ContextWindowFill = class {
28772
29025
  function modelKey(source, model) {
28773
29026
  return JSON.stringify([source ?? null, model ?? null]);
28774
29027
  }
28775
- function withContextWindows(runner, catalog) {
29028
+ function withContextWindows(runner, windows) {
28776
29029
  return {
28777
- spawn: async (opts) => windowedSession(await runner.spawn(opts), new ContextWindowFill(catalog, opts.resumeWindow)),
29030
+ spawn: async (opts) => windowedSession(await runner.spawn(opts), new ContextWindowFill(windows, opts.resumeWindow)),
28778
29031
  stop: (sessionId) => runner.stop(sessionId),
28779
29032
  stopAll: () => runner.stopAll(),
28780
29033
  get activeCount() {
@@ -28805,12 +29058,12 @@ function positive(n) {
28805
29058
 
28806
29059
  // src/services/PlannerUsage.ts
28807
29060
  var PlannerUsageLedger = class {
28808
- /** `catalogWindow` serves the window of a planner model whose backend reports none. */
28809
- constructor(catalogWindow = () => void 0) {
28810
- this.catalogWindow = catalogWindow;
28811
- this.windows = new ContextWindowFill(catalogWindow);
29061
+ /** `sessionWindows` serves the window of a planner model whose backend reports none. */
29062
+ constructor(sessionWindows = new SessionWindows()) {
29063
+ this.sessionWindows = sessionWindows;
29064
+ this.windows = new ContextWindowFill(sessionWindows);
28812
29065
  }
28813
- catalogWindow;
29066
+ sessionWindows;
28814
29067
  usage = { totals: {} };
28815
29068
  windows;
28816
29069
  /** Fold one model call's record into the totals; returns the running ledger. */
@@ -28821,16 +29074,16 @@ var PlannerUsageLedger = class {
28821
29074
  /** Adopt a persisted ledger (a reopened session) or start from zero. */
28822
29075
  restore(usage) {
28823
29076
  this.usage = usage ?? { totals: {} };
28824
- this.windows = new ContextWindowFill(this.catalogWindow, this.usage);
29077
+ this.windows = new ContextWindowFill(this.sessionWindows, this.usage);
28825
29078
  }
28826
29079
  /** Start a fresh session's ledger from zero. */
28827
29080
  clear() {
28828
29081
  this.usage = { totals: {} };
28829
- this.windows = new ContextWindowFill(this.catalogWindow);
29082
+ this.windows = new ContextWindowFill(this.sessionWindows);
28830
29083
  }
28831
- /** Whether anything has been recorded — a plan with no usage says nothing. */
29084
+ /** Whether the ledger has measures or session billing facts to broadcast. */
28832
29085
  get hasUsage() {
28833
- return isMeasured(this.usage.totals);
29086
+ return isMeasured(this.usage.totals) || this.usage.sessionBilling !== void 0;
28834
29087
  }
28835
29088
  /** The value persisted onto the plan state. */
28836
29089
  snapshot() {
@@ -30279,7 +30532,7 @@ function createSession(deps) {
30279
30532
  const backend = new PlannerBackend(deps.aiService ? { pinned: deps.aiService, busy } : { provider: () => deps.config.aiProvider, create: () => createAiService(deps.config, { workspaceRoot: deps.workspaceRoot, mcpServer }), busy });
30280
30533
  const aiService = () => backend.current();
30281
30534
  const store = new PlanStore();
30282
- const catalogWindow = (source, model) => deps.modelResolver.contextWindowFor?.(source, model);
30535
+ const windows = new SessionWindows((source, model) => deps.modelResolver.contextWindowFor?.(source, model));
30283
30536
  const taskLogs = new TaskLogRecorder({
30284
30537
  broadcast: deps.broadcast,
30285
30538
  // Read through the Session, whose id and workspace are the ones it saves
@@ -30312,7 +30565,7 @@ function createSession(deps) {
30312
30565
  const orchestrator = TaskOrchestrator.compose({
30313
30566
  config: deps.config,
30314
30567
  notifications: deps.notifications,
30315
- runner: taskLogs.wrap(runnerApprovals.wrap(withContextWindows(deps.runner, catalogWindow))),
30568
+ runner: taskLogs.wrap(runnerApprovals.wrap(withContextWindows(deps.runner, windows))),
30316
30569
  store,
30317
30570
  output: deps.taskOutput,
30318
30571
  isolation: deps.isolation,
@@ -30324,7 +30577,7 @@ function createSession(deps) {
30324
30577
  runSettings: () => session.planState?.runSettings,
30325
30578
  isApproved: (taskId) => session.isApproved(taskId)
30326
30579
  });
30327
- const usage = new PlannerUsageLedger(catalogWindow);
30580
+ const usage = new PlannerUsageLedger(windows);
30328
30581
  const events = new SessionEventRelay({
30329
30582
  broadcast: deps.broadcast,
30330
30583
  onNotice: deps.onNotice,
@@ -30394,6 +30647,7 @@ function createSession(deps) {
30394
30647
  runs: orchestrator.runs,
30395
30648
  events,
30396
30649
  usage,
30650
+ windows,
30397
30651
  approvals,
30398
30652
  approvalPolicy,
30399
30653
  taskLogs,
@@ -30421,6 +30675,7 @@ function configSecrets(config) {
30421
30675
  var Session = class {
30422
30676
  backend;
30423
30677
  usage;
30678
+ windows;
30424
30679
  events;
30425
30680
  planner;
30426
30681
  orchestrator;
@@ -30526,6 +30781,7 @@ var Session = class {
30526
30781
  this.runs = parts.runs;
30527
30782
  this.events = parts.events;
30528
30783
  this.usage = parts.usage;
30784
+ this.windows = parts.windows;
30529
30785
  this.approvals = parts.approvals;
30530
30786
  this.approvalPolicy = parts.approvalPolicy;
30531
30787
  this.taskLogs = parts.taskLogs;
@@ -31004,6 +31260,7 @@ var Session = class {
31004
31260
  this.syncPlanTasks();
31005
31261
  this.plan.isolation = this.runs.planIsolation ?? void 0;
31006
31262
  this.plan.plannerUsage = this.usage.snapshot();
31263
+ this.plan.contextWindows = this.windows.snapshot();
31007
31264
  this.plan.supervision = this.supervision.snapshot();
31008
31265
  this.plan.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
31009
31266
  this.save(this.plan, this.goal, this.workspace, this.currentSessionId);
@@ -31033,6 +31290,7 @@ var Session = class {
31033
31290
  this.owingEffects.clear();
31034
31291
  this.conversation.reset();
31035
31292
  this.usage.clear();
31293
+ this.windows.clear();
31036
31294
  this.approvals.clear(isPlannerApproval);
31037
31295
  if (!keepGrants) this.approvalPolicy.reset();
31038
31296
  this.store.clearLog();
@@ -31373,7 +31631,18 @@ var Session = class {
31373
31631
  if (!this.plan) throw new ConversationEditError("No planning conversation to condense");
31374
31632
  const opening = this.plannerTurnMayOpen(signal);
31375
31633
  if (opening) await opening;
31376
- return this.conversation.compact(signal);
31634
+ const compaction = await this.conversation.compact(signal);
31635
+ const usage = this.usage.snapshot();
31636
+ if (usage.lastPromptTokens !== void 0) {
31637
+ this.usage.record({
31638
+ source: usage.source ?? this.config.aiProvider,
31639
+ ...usage.model ? { model: usage.model } : {},
31640
+ compacted: true
31641
+ });
31642
+ this.persist();
31643
+ this.broadcast(this.usage.message());
31644
+ }
31645
+ return compaction;
31377
31646
  }
31378
31647
  rewindTargets() {
31379
31648
  return this.conversation.rewindTargets();
@@ -31861,7 +32130,10 @@ var Session = class {
31861
32130
  if (!plan.approvedTasks && plan.approved) {
31862
32131
  plan.approvedTasks = flattenTasks(plan.tasks).map((t) => t.id);
31863
32132
  }
31864
- if (adopting) this.usage.restore(plan.plannerUsage);
32133
+ if (adopting) {
32134
+ this.usage.restore(plan.plannerUsage);
32135
+ this.windows.restore(plan.contextWindows);
32136
+ }
31865
32137
  if (opts?.sessionId) this.currentSessionId = opts.sessionId;
31866
32138
  if (adopting) {
31867
32139
  for (const task of flattenTasks(plan.tasks)) delete task.resumeAt;
@@ -31948,17 +32220,12 @@ var AbstractRunner = class {
31948
32220
  get activeCount() {
31949
32221
  return this.sessions.size;
31950
32222
  }
32223
+ /** A session winding down stays registered until it exits, so a second stop gets the same wind-down. */
31951
32224
  stop(sessionId) {
31952
- const session = this.sessions.get(sessionId);
31953
- if (!session) return;
31954
- session.kill();
31955
- this.sessions.delete(sessionId);
32225
+ return this.sessions.get(sessionId)?.stop() ?? Promise.resolve();
31956
32226
  }
31957
32227
  stopAll() {
31958
- for (const [, session] of this.sessions) {
31959
- session.kill();
31960
- }
31961
- this.sessions.clear();
32228
+ for (const session of this.sessions.values()) void session.stop();
31962
32229
  }
31963
32230
  registerSession(id, session) {
31964
32231
  this.sessions.set(id, session);
@@ -32116,6 +32383,8 @@ var StructuredSession = class extends AbstractRunnerSession {
32116
32383
  queue = [];
32117
32384
  messageCount = 0;
32118
32385
  interrupting = null;
32386
+ /** The wind-down once {@link stop} was asked for: the same for every caller, and nothing new is sent meanwhile. */
32387
+ stopping = null;
32119
32388
  lastSessionId = null;
32120
32389
  lastSessionCost = null;
32121
32390
  structuredEmitter = new EventEmitter2();
@@ -32141,7 +32410,7 @@ var StructuredSession = class extends AbstractRunnerSession {
32141
32410
  await this.startAttached(this.launch.startOptions);
32142
32411
  this.state = "working";
32143
32412
  setImmediate(() => {
32144
- if (!this.exited) this.deliver(prompt);
32413
+ if (!this.exited && !this.stopping) this.deliver(prompt);
32145
32414
  });
32146
32415
  }
32147
32416
  getOutput() {
@@ -32221,6 +32490,53 @@ var StructuredSession = class extends AbstractRunnerSession {
32221
32490
  this.withdrawPermissions();
32222
32491
  this.baseHandleExit(-1);
32223
32492
  }
32493
+ stop() {
32494
+ if (this.stopping) return this.stopping;
32495
+ if (this.exited) return Promise.resolve();
32496
+ let woundDown;
32497
+ this.stopping = new Promise((resolve9) => {
32498
+ woundDown = resolve9;
32499
+ });
32500
+ void this.windDown().catch(() => this.kill()).then(woundDown);
32501
+ return this.stopping;
32502
+ }
32503
+ /**
32504
+ * The attempt is over (ADR-0018, L1), so what is still owed goes undelivered
32505
+ * and every tool request unanswered at once. A turn still open is
32506
+ * interrupted, and its close — where Claude Code reports the turn's cost and
32507
+ * its model's window — is waited for; then the runner is told nothing more
32508
+ * is coming and left to exit by itself. All of it within the interrupt
32509
+ * grace: a runner still there once it runs out is killed.
32510
+ */
32511
+ async windDown() {
32512
+ const adapter = this.adapterAttached ? this.adapter : null;
32513
+ const deadline = Date.now() + this.launch.interruptGraceMs;
32514
+ this.revokeTools();
32515
+ this.undeliverQueued();
32516
+ this.withdrawPermissions();
32517
+ const open = this.turn && !this.turn.ended ? this.turn : null;
32518
+ if (adapter && (!open || await this.turnClosed(open, adapter, deadline)) && adapter.endInput && !this.exited) {
32519
+ adapter.endInput();
32520
+ await this.within(this.gone(), deadline);
32521
+ }
32522
+ if (this.exited) adapter?.dispose();
32523
+ else this.kill();
32524
+ }
32525
+ /** Whether `turn` closed within the deadline once interrupted. */
32526
+ async turnClosed(turn, adapter, deadline) {
32527
+ const closed = new Promise((resolve9) => this.structuredEmitter.once("turnEnd", () => resolve9()));
32528
+ adapter.interrupt(Math.max(0, deadline - Date.now())).catch(() => false);
32529
+ return await this.within(closed, deadline) && turn.ended;
32530
+ }
32531
+ /** Whether `done` settled before the deadline, or the session ended meanwhile. */
32532
+ within(done, deadline) {
32533
+ return settleWithin(done.then(() => true), { timeoutMs: Math.max(0, deadline - Date.now()), onTimeout: () => false, ended: this.gone(), onEnded: () => true });
32534
+ }
32535
+ gone() {
32536
+ const { signal } = this.ended;
32537
+ if (signal.aborted) return Promise.resolve();
32538
+ return new Promise((resolve9) => signal.addEventListener("abort", () => resolve9(), { once: true }));
32539
+ }
32224
32540
  turnState() {
32225
32541
  return this.state;
32226
32542
  }
@@ -32246,7 +32562,7 @@ var StructuredSession = class extends AbstractRunnerSession {
32246
32562
  this.messageCount += 1;
32247
32563
  const id = `msg-${this.messageCount}`;
32248
32564
  const sender2 = opening.from ? { from: opening.from } : {};
32249
- if (this.exited) this.emitEvent({ type: "message_undelivered", messageId: id, text, ...sender2 });
32565
+ if (this.exited || this.stopping) this.emitEvent({ type: "message_undelivered", messageId: id, text, ...sender2 });
32250
32566
  else if (this.state === "idle" && this.adapterStarted && !this.sending) this.deliver(text, opening);
32251
32567
  else {
32252
32568
  this.state = "working";
@@ -32301,7 +32617,7 @@ var StructuredSession = class extends AbstractRunnerSession {
32301
32617
  });
32302
32618
  }
32303
32619
  runningTurn() {
32304
- return !this.exited && this.adapter !== null && this.turn !== null && !this.turn.ended;
32620
+ return !this.exited && !this.stopping && this.adapter !== null && this.turn !== null && !this.turn.ended;
32305
32621
  }
32306
32622
  /** No runner can recall a message once offered to it, so only a queued one comes back. */
32307
32623
  removeQueued(id) {
@@ -32362,7 +32678,7 @@ var StructuredSession = class extends AbstractRunnerSession {
32362
32678
  if (this.interrupting) return this.interrupting;
32363
32679
  const turn = this.turn;
32364
32680
  const adapter = this.adapter;
32365
- if (!turn || turn.ended || !adapter) return Promise.resolve();
32681
+ if (!turn || turn.ended || !adapter || this.stopping) return Promise.resolve();
32366
32682
  this.interrupting = this.interruptTurn(turn, adapter).finally(() => {
32367
32683
  this.interrupting = null;
32368
32684
  this.offerNext();
@@ -32376,7 +32692,7 @@ var StructuredSession = class extends AbstractRunnerSession {
32376
32692
  const settled = await settleWithin(ended.then(() => true), { timeoutMs: grace, onTimeout: () => false });
32377
32693
  if (settled) return;
32378
32694
  }
32379
- if (turn.ended || this.exited) return;
32695
+ if (turn.ended || this.exited || this.stopping) return;
32380
32696
  await this.restartInterrupted(turn);
32381
32697
  }
32382
32698
  /**
@@ -32480,7 +32796,7 @@ var StructuredSession = class extends AbstractRunnerSession {
32480
32796
  offerNext() {
32481
32797
  const adapter = this.adapter;
32482
32798
  const turn = this.turn;
32483
- if (!adapter?.steer || !turn || turn.ended || turn.steerRefused || this.interrupting || this.exited) return;
32799
+ if (!adapter?.steer || !turn || turn.ended || turn.steerRefused || this.interrupting || this.exited || this.stopping) return;
32484
32800
  if (this.queue.some((m) => m.stage === "steering")) return;
32485
32801
  const next = this.queue.find((m) => m.stage === "queued");
32486
32802
  if (!next || next.command) return;
@@ -32555,7 +32871,7 @@ var StructuredSession = class extends AbstractRunnerSession {
32555
32871
  }
32556
32872
  return;
32557
32873
  }
32558
- if (!turn && TURN_OPENING_EVENTS.has(event.type) && !("subagentId" in event && event.subagentId)) {
32874
+ if (!turn && !this.stopping && TURN_OPENING_EVENTS.has(event.type) && !("subagentId" in event && event.subagentId)) {
32559
32875
  turn = this.openTurn("");
32560
32876
  this.offerNext();
32561
32877
  }
@@ -32626,7 +32942,7 @@ var StructuredSession = class extends AbstractRunnerSession {
32626
32942
  * back in Ordewell's hands: while it is on offer or owed, nothing overtakes it.
32627
32943
  */
32628
32944
  deliverNext() {
32629
- if (this.exited || this.sending || this.turn && !this.turn.ended) return;
32945
+ if (this.exited || this.stopping || this.sending || this.turn && !this.turn.ended) return;
32630
32946
  const head = this.queue[0];
32631
32947
  if (!head || this.queue.some((message) => message.stage !== "queued")) return;
32632
32948
  this.queue.shift();
@@ -33075,7 +33391,10 @@ export {
33075
33391
  collectProviderCredentials,
33076
33392
  commandAllowedBy,
33077
33393
  configuredProviders,
33394
+ contextMeterText,
33395
+ contextMeterTitle,
33078
33396
  continuability,
33397
+ costText,
33079
33398
  countedSessionCost,
33080
33399
  createAiService,
33081
33400
  createEmptyPlan,
@@ -33118,7 +33437,9 @@ export {
33118
33437
  flattenTasksWithParents,
33119
33438
  followTaskLog,
33120
33439
  followTurn,
33440
+ formatCost,
33121
33441
  formatSearchOutput,
33442
+ formatTokenCount,
33122
33443
  fromTranscript,
33123
33444
  getLatestSession,
33124
33445
  getProviderMeta,
@@ -33170,6 +33491,7 @@ export {
33170
33491
  migratePlanState,
33171
33492
  mintDaemonToken,
33172
33493
  mintSessionId,
33494
+ modelCostTitle,
33173
33495
  namedRepos,
33174
33496
  newTaskFields,
33175
33497
  normalizeAgentArgs,
@@ -33255,6 +33577,7 @@ export {
33255
33577
  stopTurn,
33256
33578
  stripAnsi,
33257
33579
  stripModelPrefix,
33580
+ subagentCostTitle,
33258
33581
  subtreeIds,
33259
33582
  summarizeToolCall,
33260
33583
  supervisorSummary,
@@ -33273,6 +33596,7 @@ export {
33273
33596
  taskStartedNotice,
33274
33597
  taskStatusKind,
33275
33598
  taskUsageSummary,
33599
+ tasksTotalText,
33276
33600
  textHasTaskOps,
33277
33601
  textHasTaskQuery,
33278
33602
  titledRefs,
@@ -33290,6 +33614,7 @@ export {
33290
33614
  unsendAll,
33291
33615
  unsendLatest,
33292
33616
  usageLine,
33617
+ usageText,
33293
33618
  waitsForApproval,
33294
33619
  walkPlan,
33295
33620
  wellKnownBinDirs,