@ordewell/core 0.10.9 → 0.10.11

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 +129 -20
  14. package/dist/index.d.ts +129 -20
  15. package/dist/index.js +556 -92
  16. package/dist/index.js.map +1 -1
  17. package/dist/index.mjs +432 -76
  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-CGKX3aeV.d.ts → runSettingsText-C5uv2axI.d.ts} +29 -18
  29. package/dist/{runSettingsText-rLoYQlUH.d.mts → runSettingsText-Jb_efnI4.d.mts} +29 -18
  30. package/dist/testing.d.mts +12 -3
  31. package/dist/testing.d.ts +12 -3
  32. package/dist/testing.js +16 -1
  33. package/dist/testing.js.map +1 -1
  34. package/dist/testing.mjs +17 -2
  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++) {
@@ -21446,9 +21628,11 @@ function plannerNotReadyFor(config) {
21446
21628
  var TERMINAL_UI_COMMANDS = {
21447
21629
  pickPlanner: "/planner",
21448
21630
  setKey: () => "/key",
21449
- setModel: "/model"
21631
+ // `/planner` walks harness, model and effort in one flow; there is no
21632
+ // separate `/model` (or `/planner-effort`) to point at.
21633
+ setModel: "/planner"
21450
21634
  };
21451
- var CHAT_COMMANDS = { ...TERMINAL_UI_COMMANDS, setModel: "/model set" };
21635
+ var CHAT_COMMANDS = { ...TERMINAL_UI_COMMANDS };
21452
21636
  var HEADLESS_CLI_COMMANDS = {
21453
21637
  pickPlanner: "ordewell planner <id>",
21454
21638
  setKey: (provider) => `ordewell key set ${provider} <key>`,
@@ -21462,7 +21646,7 @@ function harnessPlannerNames() {
21462
21646
  var PLANNER_ESCAPE_HINT = `or pick a coding agent with ${TERMINAL_UI_COMMANDS.pickPlanner}: it plans with no API key`;
21463
21647
  function describePlannerNotReady(reason, commands) {
21464
21648
  if (reason.code === "no_model") {
21465
- return `No planner model selected for ${reason.label}. Pick one with ${commands.setModel}, or switch planner with ${commands.pickPlanner}.`;
21649
+ return commands.setModel === commands.pickPlanner ? `No planner model selected for ${reason.label}. Pick one with ${commands.setModel}.` : `No planner model selected for ${reason.label}. Pick one with ${commands.setModel}, or switch planner with ${commands.pickPlanner}.`;
21466
21650
  }
21467
21651
  const lead = `No planner is ready. Pick one with ${commands.pickPlanner}: ${harnessPlannerNames()} plan with no API key.`;
21468
21652
  if (reason.code === "no_base_url") {
@@ -21496,7 +21680,8 @@ function usageRecordFrom(usage, model) {
21496
21680
  model,
21497
21681
  ...inputTokens !== void 0 ? { inputTokens } : {},
21498
21682
  ...reportedOutput.length > 0 ? { outputTokens: reportedOutput.reduce((a, b) => a + b, 0) } : {},
21499
- ...tokenCount(usage.cachedContentTokenCount) !== void 0 ? { cachedInputTokens: tokenCount(usage.cachedContentTokenCount) } : {}
21683
+ ...tokenCount(usage.cachedContentTokenCount) !== void 0 ? { cachedInputTokens: tokenCount(usage.cachedContentTokenCount) } : {},
21684
+ ...inputTokens !== void 0 ? { contextTokens: inputTokens + (answerTokens ?? 0) } : {}
21500
21685
  };
21501
21686
  const anyReported = record.inputTokens !== void 0 || record.outputTokens !== void 0 || record.cachedInputTokens !== void 0;
21502
21687
  return anyReported ? record : void 0;
@@ -21899,7 +22084,9 @@ var OpenAiResearchChat = class {
21899
22084
  /**
21900
22085
  * The one record for this API call, built only from what the provider
21901
22086
  * reported. OpenRouter's `cost` is in USD (its credits are dollar-pegged);
21902
- * nothing is estimated. A field the provider left out stays absent.
22087
+ * nothing is estimated. A field the provider left out stays absent. The
22088
+ * context the call leaves is its prompt and its completion: the API's own
22089
+ * `total_tokens`, and the reply the next call's prompt carries.
21903
22090
  */
21904
22091
  usageRecord(reported) {
21905
22092
  if (!reported) return void 0;
@@ -21918,9 +22105,10 @@ var OpenAiResearchChat = class {
21918
22105
  hasMeasure = true;
21919
22106
  }
21920
22107
  if (reported.cost !== void 0) {
21921
- record.reportedCost = { amount: reported.cost, currency: "USD" };
22108
+ record.reportedCost = { amount: reported.cost, currency: "USD", ...record.source === "openrouter" ? { kind: "billed" } : {} };
21922
22109
  hasMeasure = true;
21923
22110
  }
22111
+ if (record.inputTokens !== void 0) record.contextTokens = record.inputTokens + (record.outputTokens ?? 0);
21924
22112
  return hasMeasure ? record : void 0;
21925
22113
  }
21926
22114
  };
@@ -24091,6 +24279,10 @@ var TaskOrchestrator = class _TaskOrchestrator {
24091
24279
  store;
24092
24280
  output;
24093
24281
  attempts = /* @__PURE__ */ new Map();
24282
+ /** Bumped by every plan load: an ended attempt's wind-down belongs to the scope before it. */
24283
+ planScope = 0;
24284
+ /** Each task's runners still winding down once stopped: a continue waits for them (ADR-0018, L1). */
24285
+ windDowns = /* @__PURE__ */ new Map();
24094
24286
  verifier = new VerdictEngine();
24095
24287
  /** Changed only by start, stop, a plan load, a usage-limit pause, a move off the limit, and {@link settleRun}. */
24096
24288
  runState = "idle";
@@ -24639,8 +24831,9 @@ var TaskOrchestrator = class _TaskOrchestrator {
24639
24831
  return blockingFailures(this.store.planTasks).get(taskId) ?? [];
24640
24832
  }
24641
24833
  loadPlan(tasks, planRunners = [...DEFAULT_RUNNERS]) {
24642
- this.store.load(tasks, planRunners);
24643
24834
  const repairs = this.endAllAttempts("load").filter((a) => a.kind.kind === "repair" && a.worktree);
24835
+ this.planScope += 1;
24836
+ this.store.load(tasks, planRunners);
24644
24837
  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
24838
  this.runs.interrupt({ keepOpen: this.isScheduling });
24646
24839
  if (this.runState !== "scheduling") this.runState = "idle";
@@ -24726,6 +24919,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
24726
24919
  if (reason === "user") this.stopGeneration++;
24727
24920
  this.runState = "idle";
24728
24921
  this.verifier.reset();
24922
+ for (const attempt of this.attempts.values()) this.stopRunner(attempt);
24729
24923
  this.runner.stopAll();
24730
24924
  const stopped = [];
24731
24925
  for (const a of this.endAllAttempts("stop")) {
@@ -24839,6 +25033,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
24839
25033
  if (!task || !attempt) return;
24840
25034
  attempt.decided = true;
24841
25035
  const stoppedMidTurn = endedByExit(verdict) ? saidInFlight(attempt) : null;
25036
+ this.stopRunner(attempt);
24842
25037
  console.error(`[TaskOrchestrator] Task ${taskRefOf(this.store.planTasks, task.id) ?? `#${task.order}`} "${task.title}" verdict=${verdict.outcome}`);
24843
25038
  console.error(`[TaskOrchestrator] Runner: ${task.assignedRunner}, Model: ${task.assignedModel?.modelId ?? "default"}`);
24844
25039
  console.error(`[TaskOrchestrator] Prompt preview: ${(task.prompt ?? "").slice(0, 200)}`);
@@ -25750,7 +25945,9 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
25750
25945
  autoResumed: opts.autoResumed === true,
25751
25946
  turnError: null,
25752
25947
  turnOutputFrom: 0,
25753
- reminded: false
25948
+ reminded: false,
25949
+ runnerStopped: false,
25950
+ planScope: this.planScope
25754
25951
  };
25755
25952
  this.spawnCounts.set(task.id, attempt.attempt);
25756
25953
  this.attempts.set(task.id, attempt);
@@ -25787,6 +25984,7 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
25787
25984
  const manifest = this.registry?.get(attempt.runner)?.manifest;
25788
25985
  const unavailable = manifest && unavailableTaskModeMessage(manifest, task.taskMode);
25789
25986
  if (unavailable) throw new Error(unavailable);
25987
+ if (kind.kind === "continuation") await this.windDowns.get(task.id);
25790
25988
  const { cwd, worktree, previousWork } = await attemptCwd(kind, task, this.runs);
25791
25989
  attempt.cwd = cwd;
25792
25990
  attempt.worktree = worktree;
@@ -25842,10 +26040,11 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
25842
26040
  }
25843
26041
  if (takesSkills(kind)) this.store.setTaskAttemptSkills(task.id, [...attempt.skills]);
25844
26042
  session.onEvent((event) => {
25845
- if (event.type !== "usage" || this.attempts.get(task.id) !== attempt) return;
26043
+ if (event.type !== "usage" || attempt.planScope !== this.planScope || this.attempts.get(task.id) !== attempt && !attempt.runnerStopped) return;
25846
26044
  this.store.recordAttemptUsage(task.id, attempt.attempt, event.record, (/* @__PURE__ */ new Date()).toISOString());
25847
26045
  this.emit("onUsageChanged");
25848
26046
  });
26047
+ session.onExit(() => this.woundDown(attempt, session));
25849
26048
  this.output.attach(task.id, session);
25850
26049
  this.verifier.watch(task, session);
25851
26050
  session.onEvent((event) => {
@@ -26014,10 +26213,35 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
26014
26213
  if (stopsRunner(reason)) {
26015
26214
  const task = this.store.get(taskId);
26016
26215
  if (task) this.verifier.clear(task);
26017
- if (session) this.runner.stop(session.id);
26216
+ if (attempt) this.stopRunner(attempt);
26018
26217
  }
26019
26218
  return attempt;
26020
26219
  }
26220
+ /**
26221
+ * Let the attempt's runner go the way it ends a turn (ADR-0018, L1): it
26222
+ * winds down on its own time, bounded by its interrupt grace, and nothing
26223
+ * here waits on it but a continue of the same task.
26224
+ */
26225
+ stopRunner(attempt) {
26226
+ const session = attempt.session;
26227
+ if (!session || attempt.runnerStopped) return;
26228
+ attempt.runnerStopped = true;
26229
+ const { taskId } = attempt;
26230
+ const windDown = Promise.all([this.windDowns.get(taskId), this.runner.stop(session.id)]).catch(() => void 0).then(() => {
26231
+ if (this.windDowns.get(taskId) === windDown) this.windDowns.delete(taskId);
26232
+ });
26233
+ this.windDowns.set(taskId, windDown);
26234
+ }
26235
+ /**
26236
+ * An ended attempt's runner is gone: the count its session ended on is the
26237
+ * one a continue of that session counts from. One still current is ended
26238
+ * with that count instead.
26239
+ */
26240
+ woundDown(attempt, session) {
26241
+ if (attempt.planScope !== this.planScope || !attempt.runnerStopped || this.attempts.get(attempt.taskId) === attempt) return;
26242
+ this.store.endAttemptUsage(attempt.taskId, attempt.attempt, (/* @__PURE__ */ new Date()).toISOString(), session.sessionCost?.() ?? void 0, session.nativeSessionId() ?? void 0);
26243
+ this.emit("onUsageChanged");
26244
+ }
26021
26245
  /** Kept on the task once the attempt ends, so a continue can resume it after a reload (ADR-0018, K1). */
26022
26246
  saveNativeSession(taskId, nativeSessionId, runner) {
26023
26247
  if (nativeSessionId) this.store.setTaskRunnerSessionId(taskId, nativeSessionId, runner);
@@ -28752,19 +28976,50 @@ var TaskLogRecorder = class {
28752
28976
  };
28753
28977
 
28754
28978
  // src/services/ContextWindowFill.ts
28755
- var ContextWindowFill = class {
28756
- constructor(catalog, last = {}) {
28979
+ var SessionWindows = class {
28980
+ constructor(catalog = () => void 0) {
28757
28981
  this.catalog = catalog;
28758
- if (last.contextWindow !== void 0) this.known.set(modelKey(last.source, last.model), last.contextWindow);
28759
28982
  }
28760
28983
  catalog;
28761
- /** The window known for each model of the stream, by reporter and model. */
28984
+ reported = /* @__PURE__ */ new Map();
28985
+ report(source, model, contextWindow) {
28986
+ if (contextWindow > 0) this.reported.set(modelKey(source, model), { source, model, contextWindow });
28987
+ }
28988
+ get(source, model) {
28989
+ return this.reported.get(modelKey(source, model))?.contextWindow;
28990
+ }
28991
+ snapshot() {
28992
+ return [...this.reported.values()];
28993
+ }
28994
+ restore(saved) {
28995
+ this.reported.clear();
28996
+ for (const { source, model, contextWindow } of saved ?? []) this.report(source, model, contextWindow);
28997
+ }
28998
+ clear() {
28999
+ this.reported.clear();
29000
+ }
29001
+ };
29002
+ var ContextWindowFill = class {
29003
+ constructor(windows, last = {}) {
29004
+ this.windows = windows;
29005
+ if (last.contextWindow !== void 0) this.known.set(modelKey(last.source, last.model), last.contextWindow);
29006
+ }
29007
+ windows;
29008
+ /** The window this stream used for each of its models, by reporter and model. */
28762
29009
  known = /* @__PURE__ */ new Map();
28763
29010
  fill(record) {
28764
29011
  if (record.subagentId || record.aggregate) return record;
28765
29012
  const key = modelKey(record.source, record.model);
29013
+ if (record.compacted) {
29014
+ if (!record.model) return record;
29015
+ const known = this.windows.get(record.source, record.model) ?? this.known.get(key) ?? positive(this.windows.catalog(record.source, record.model));
29016
+ if (known === void 0) return record;
29017
+ this.known.set(key, known);
29018
+ return { ...record, contextWindow: known };
29019
+ }
28766
29020
  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);
29021
+ if (reported !== void 0 && record.model) this.windows.report(record.source, record.model, reported);
29022
+ 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
29023
  if (window !== void 0) this.known.set(key, window);
28769
29024
  return window === void 0 || window === record.contextWindow ? record : { ...record, contextWindow: window };
28770
29025
  }
@@ -28772,9 +29027,9 @@ var ContextWindowFill = class {
28772
29027
  function modelKey(source, model) {
28773
29028
  return JSON.stringify([source ?? null, model ?? null]);
28774
29029
  }
28775
- function withContextWindows(runner, catalog) {
29030
+ function withContextWindows(runner, windows) {
28776
29031
  return {
28777
- spawn: async (opts) => windowedSession(await runner.spawn(opts), new ContextWindowFill(catalog, opts.resumeWindow)),
29032
+ spawn: async (opts) => windowedSession(await runner.spawn(opts), new ContextWindowFill(windows, opts.resumeWindow)),
28778
29033
  stop: (sessionId) => runner.stop(sessionId),
28779
29034
  stopAll: () => runner.stopAll(),
28780
29035
  get activeCount() {
@@ -28805,12 +29060,12 @@ function positive(n) {
28805
29060
 
28806
29061
  // src/services/PlannerUsage.ts
28807
29062
  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);
29063
+ /** `sessionWindows` serves the window of a planner model whose backend reports none. */
29064
+ constructor(sessionWindows = new SessionWindows()) {
29065
+ this.sessionWindows = sessionWindows;
29066
+ this.windows = new ContextWindowFill(sessionWindows);
28812
29067
  }
28813
- catalogWindow;
29068
+ sessionWindows;
28814
29069
  usage = { totals: {} };
28815
29070
  windows;
28816
29071
  /** Fold one model call's record into the totals; returns the running ledger. */
@@ -28821,16 +29076,16 @@ var PlannerUsageLedger = class {
28821
29076
  /** Adopt a persisted ledger (a reopened session) or start from zero. */
28822
29077
  restore(usage) {
28823
29078
  this.usage = usage ?? { totals: {} };
28824
- this.windows = new ContextWindowFill(this.catalogWindow, this.usage);
29079
+ this.windows = new ContextWindowFill(this.sessionWindows, this.usage);
28825
29080
  }
28826
29081
  /** Start a fresh session's ledger from zero. */
28827
29082
  clear() {
28828
29083
  this.usage = { totals: {} };
28829
- this.windows = new ContextWindowFill(this.catalogWindow);
29084
+ this.windows = new ContextWindowFill(this.sessionWindows);
28830
29085
  }
28831
- /** Whether anything has been recorded — a plan with no usage says nothing. */
29086
+ /** Whether the ledger has measures or session billing facts to broadcast. */
28832
29087
  get hasUsage() {
28833
- return isMeasured(this.usage.totals);
29088
+ return isMeasured(this.usage.totals) || this.usage.sessionBilling !== void 0;
28834
29089
  }
28835
29090
  /** The value persisted onto the plan state. */
28836
29091
  snapshot() {
@@ -30279,7 +30534,7 @@ function createSession(deps) {
30279
30534
  const backend = new PlannerBackend(deps.aiService ? { pinned: deps.aiService, busy } : { provider: () => deps.config.aiProvider, create: () => createAiService(deps.config, { workspaceRoot: deps.workspaceRoot, mcpServer }), busy });
30280
30535
  const aiService = () => backend.current();
30281
30536
  const store = new PlanStore();
30282
- const catalogWindow = (source, model) => deps.modelResolver.contextWindowFor?.(source, model);
30537
+ const windows = new SessionWindows((source, model) => deps.modelResolver.contextWindowFor?.(source, model));
30283
30538
  const taskLogs = new TaskLogRecorder({
30284
30539
  broadcast: deps.broadcast,
30285
30540
  // Read through the Session, whose id and workspace are the ones it saves
@@ -30312,7 +30567,7 @@ function createSession(deps) {
30312
30567
  const orchestrator = TaskOrchestrator.compose({
30313
30568
  config: deps.config,
30314
30569
  notifications: deps.notifications,
30315
- runner: taskLogs.wrap(runnerApprovals.wrap(withContextWindows(deps.runner, catalogWindow))),
30570
+ runner: taskLogs.wrap(runnerApprovals.wrap(withContextWindows(deps.runner, windows))),
30316
30571
  store,
30317
30572
  output: deps.taskOutput,
30318
30573
  isolation: deps.isolation,
@@ -30324,7 +30579,7 @@ function createSession(deps) {
30324
30579
  runSettings: () => session.planState?.runSettings,
30325
30580
  isApproved: (taskId) => session.isApproved(taskId)
30326
30581
  });
30327
- const usage = new PlannerUsageLedger(catalogWindow);
30582
+ const usage = new PlannerUsageLedger(windows);
30328
30583
  const events = new SessionEventRelay({
30329
30584
  broadcast: deps.broadcast,
30330
30585
  onNotice: deps.onNotice,
@@ -30394,6 +30649,7 @@ function createSession(deps) {
30394
30649
  runs: orchestrator.runs,
30395
30650
  events,
30396
30651
  usage,
30652
+ windows,
30397
30653
  approvals,
30398
30654
  approvalPolicy,
30399
30655
  taskLogs,
@@ -30421,6 +30677,7 @@ function configSecrets(config) {
30421
30677
  var Session = class {
30422
30678
  backend;
30423
30679
  usage;
30680
+ windows;
30424
30681
  events;
30425
30682
  planner;
30426
30683
  orchestrator;
@@ -30526,6 +30783,7 @@ var Session = class {
30526
30783
  this.runs = parts.runs;
30527
30784
  this.events = parts.events;
30528
30785
  this.usage = parts.usage;
30786
+ this.windows = parts.windows;
30529
30787
  this.approvals = parts.approvals;
30530
30788
  this.approvalPolicy = parts.approvalPolicy;
30531
30789
  this.taskLogs = parts.taskLogs;
@@ -31004,6 +31262,7 @@ var Session = class {
31004
31262
  this.syncPlanTasks();
31005
31263
  this.plan.isolation = this.runs.planIsolation ?? void 0;
31006
31264
  this.plan.plannerUsage = this.usage.snapshot();
31265
+ this.plan.contextWindows = this.windows.snapshot();
31007
31266
  this.plan.supervision = this.supervision.snapshot();
31008
31267
  this.plan.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
31009
31268
  this.save(this.plan, this.goal, this.workspace, this.currentSessionId);
@@ -31033,6 +31292,7 @@ var Session = class {
31033
31292
  this.owingEffects.clear();
31034
31293
  this.conversation.reset();
31035
31294
  this.usage.clear();
31295
+ this.windows.clear();
31036
31296
  this.approvals.clear(isPlannerApproval);
31037
31297
  if (!keepGrants) this.approvalPolicy.reset();
31038
31298
  this.store.clearLog();
@@ -31373,7 +31633,18 @@ var Session = class {
31373
31633
  if (!this.plan) throw new ConversationEditError("No planning conversation to condense");
31374
31634
  const opening = this.plannerTurnMayOpen(signal);
31375
31635
  if (opening) await opening;
31376
- return this.conversation.compact(signal);
31636
+ const compaction = await this.conversation.compact(signal);
31637
+ const usage = this.usage.snapshot();
31638
+ if (usage.lastPromptTokens !== void 0) {
31639
+ this.usage.record({
31640
+ source: usage.source ?? this.config.aiProvider,
31641
+ ...usage.model ? { model: usage.model } : {},
31642
+ compacted: true
31643
+ });
31644
+ this.persist();
31645
+ this.broadcast(this.usage.message());
31646
+ }
31647
+ return compaction;
31377
31648
  }
31378
31649
  rewindTargets() {
31379
31650
  return this.conversation.rewindTargets();
@@ -31861,7 +32132,10 @@ var Session = class {
31861
32132
  if (!plan.approvedTasks && plan.approved) {
31862
32133
  plan.approvedTasks = flattenTasks(plan.tasks).map((t) => t.id);
31863
32134
  }
31864
- if (adopting) this.usage.restore(plan.plannerUsage);
32135
+ if (adopting) {
32136
+ this.usage.restore(plan.plannerUsage);
32137
+ this.windows.restore(plan.contextWindows);
32138
+ }
31865
32139
  if (opts?.sessionId) this.currentSessionId = opts.sessionId;
31866
32140
  if (adopting) {
31867
32141
  for (const task of flattenTasks(plan.tasks)) delete task.resumeAt;
@@ -31948,17 +32222,12 @@ var AbstractRunner = class {
31948
32222
  get activeCount() {
31949
32223
  return this.sessions.size;
31950
32224
  }
32225
+ /** A session winding down stays registered until it exits, so a second stop gets the same wind-down. */
31951
32226
  stop(sessionId) {
31952
- const session = this.sessions.get(sessionId);
31953
- if (!session) return;
31954
- session.kill();
31955
- this.sessions.delete(sessionId);
32227
+ return this.sessions.get(sessionId)?.stop() ?? Promise.resolve();
31956
32228
  }
31957
32229
  stopAll() {
31958
- for (const [, session] of this.sessions) {
31959
- session.kill();
31960
- }
31961
- this.sessions.clear();
32230
+ for (const session of this.sessions.values()) void session.stop();
31962
32231
  }
31963
32232
  registerSession(id, session) {
31964
32233
  this.sessions.set(id, session);
@@ -32110,12 +32379,17 @@ var StructuredSession = class extends AbstractRunnerSession {
32110
32379
  text;
32111
32380
  state = "idle";
32112
32381
  turn = null;
32382
+ /** A `task_complete` report waiting for its turn to close; the verdict is published then. */
32383
+ completionReport = null;
32384
+ completionTimer = null;
32113
32385
  /** A terminal event ends the turn before the adapter releases its send. */
32114
32386
  sending = null;
32115
32387
  turnCount = 0;
32116
32388
  queue = [];
32117
32389
  messageCount = 0;
32118
32390
  interrupting = null;
32391
+ /** The wind-down once {@link stop} was asked for: the same for every caller, and nothing new is sent meanwhile. */
32392
+ stopping = null;
32119
32393
  lastSessionId = null;
32120
32394
  lastSessionCost = null;
32121
32395
  structuredEmitter = new EventEmitter2();
@@ -32141,7 +32415,7 @@ var StructuredSession = class extends AbstractRunnerSession {
32141
32415
  await this.startAttached(this.launch.startOptions);
32142
32416
  this.state = "working";
32143
32417
  setImmediate(() => {
32144
- if (!this.exited) this.deliver(prompt);
32418
+ if (!this.exited && !this.stopping) this.deliver(prompt);
32145
32419
  });
32146
32420
  }
32147
32421
  getOutput() {
@@ -32166,7 +32440,9 @@ var StructuredSession = class extends AbstractRunnerSession {
32166
32440
  create: () => this.launch.createAdapter(this.launch.runner, this.launch.deps),
32167
32441
  issueToken: () => server4.issueTaskToken(scope, {
32168
32442
  taskComplete: async (report) => {
32169
- this.structuredEmitter.emit("taskComplete", report);
32443
+ this.completionReport = report;
32444
+ if (this.completionTimer) clearTimeout(this.completionTimer);
32445
+ this.completionTimer = setTimeout(() => this.flushCompletion(), this.launch.interruptGraceMs);
32170
32446
  return { text: "Recorded. End your turn now." };
32171
32447
  },
32172
32448
  checkpoint: async ({ question }, { signal }) => {
@@ -32198,6 +32474,8 @@ var StructuredSession = class extends AbstractRunnerSession {
32198
32474
  this.toolToken = null;
32199
32475
  }
32200
32476
  baseHandleExit(code) {
32477
+ this.undeliverQueued();
32478
+ this.flushCompletion();
32201
32479
  this.ended.abort();
32202
32480
  this.revokeTools();
32203
32481
  this.undeliverQueued();
@@ -32221,6 +32499,53 @@ var StructuredSession = class extends AbstractRunnerSession {
32221
32499
  this.withdrawPermissions();
32222
32500
  this.baseHandleExit(-1);
32223
32501
  }
32502
+ stop() {
32503
+ if (this.stopping) return this.stopping;
32504
+ if (this.exited) return Promise.resolve();
32505
+ let woundDown;
32506
+ this.stopping = new Promise((resolve9) => {
32507
+ woundDown = resolve9;
32508
+ });
32509
+ void this.windDown().catch(() => this.kill()).then(woundDown);
32510
+ return this.stopping;
32511
+ }
32512
+ /**
32513
+ * The attempt is over (ADR-0018, L1), so what is still owed goes undelivered
32514
+ * and every tool request unanswered at once. A turn still open is
32515
+ * interrupted, and its close — where Claude Code reports the turn's cost and
32516
+ * its model's window — is waited for; then the runner is told nothing more
32517
+ * is coming and left to exit by itself. All of it within the interrupt
32518
+ * grace: a runner still there once it runs out is killed.
32519
+ */
32520
+ async windDown() {
32521
+ const adapter = this.adapterAttached ? this.adapter : null;
32522
+ const deadline = Date.now() + this.launch.interruptGraceMs;
32523
+ this.revokeTools();
32524
+ this.undeliverQueued();
32525
+ this.withdrawPermissions();
32526
+ const open = this.turn && !this.turn.ended ? this.turn : null;
32527
+ if (adapter && (!open || await this.turnClosed(open, adapter, deadline)) && adapter.endInput && !this.exited) {
32528
+ adapter.endInput();
32529
+ await this.within(this.gone(), deadline);
32530
+ }
32531
+ if (this.exited) adapter?.dispose();
32532
+ else this.kill();
32533
+ }
32534
+ /** Whether `turn` closed within the deadline once interrupted. */
32535
+ async turnClosed(turn, adapter, deadline) {
32536
+ const closed = new Promise((resolve9) => this.structuredEmitter.once("turnEnd", () => resolve9()));
32537
+ adapter.interrupt(Math.max(0, deadline - Date.now())).catch(() => false);
32538
+ return await this.within(closed, deadline) && turn.ended;
32539
+ }
32540
+ /** Whether `done` settled before the deadline, or the session ended meanwhile. */
32541
+ within(done, deadline) {
32542
+ return settleWithin(done.then(() => true), { timeoutMs: Math.max(0, deadline - Date.now()), onTimeout: () => false, ended: this.gone(), onEnded: () => true });
32543
+ }
32544
+ gone() {
32545
+ const { signal } = this.ended;
32546
+ if (signal.aborted) return Promise.resolve();
32547
+ return new Promise((resolve9) => signal.addEventListener("abort", () => resolve9(), { once: true }));
32548
+ }
32224
32549
  turnState() {
32225
32550
  return this.state;
32226
32551
  }
@@ -32246,7 +32571,7 @@ var StructuredSession = class extends AbstractRunnerSession {
32246
32571
  this.messageCount += 1;
32247
32572
  const id = `msg-${this.messageCount}`;
32248
32573
  const sender2 = opening.from ? { from: opening.from } : {};
32249
- if (this.exited) this.emitEvent({ type: "message_undelivered", messageId: id, text, ...sender2 });
32574
+ if (this.exited || this.stopping) this.emitEvent({ type: "message_undelivered", messageId: id, text, ...sender2 });
32250
32575
  else if (this.state === "idle" && this.adapterStarted && !this.sending) this.deliver(text, opening);
32251
32576
  else {
32252
32577
  this.state = "working";
@@ -32291,6 +32616,7 @@ var StructuredSession = class extends AbstractRunnerSession {
32291
32616
  * "waiting for input" between the two (F3).
32292
32617
  */
32293
32618
  force(message) {
32619
+ this.discardCompletion();
32294
32620
  message.forced = true;
32295
32621
  const at = this.queue.findIndex((m) => !m.forced);
32296
32622
  this.queue.splice(at < 0 ? this.queue.length : at, 0, message);
@@ -32301,7 +32627,7 @@ var StructuredSession = class extends AbstractRunnerSession {
32301
32627
  });
32302
32628
  }
32303
32629
  runningTurn() {
32304
- return !this.exited && this.adapter !== null && this.turn !== null && !this.turn.ended;
32630
+ return !this.exited && !this.stopping && this.adapter !== null && this.turn !== null && !this.turn.ended;
32305
32631
  }
32306
32632
  /** No runner can recall a message once offered to it, so only a queued one comes back. */
32307
32633
  removeQueued(id) {
@@ -32362,7 +32688,7 @@ var StructuredSession = class extends AbstractRunnerSession {
32362
32688
  if (this.interrupting) return this.interrupting;
32363
32689
  const turn = this.turn;
32364
32690
  const adapter = this.adapter;
32365
- if (!turn || turn.ended || !adapter) return Promise.resolve();
32691
+ if (!turn || turn.ended || !adapter || this.stopping) return Promise.resolve();
32366
32692
  this.interrupting = this.interruptTurn(turn, adapter).finally(() => {
32367
32693
  this.interrupting = null;
32368
32694
  this.offerNext();
@@ -32376,7 +32702,7 @@ var StructuredSession = class extends AbstractRunnerSession {
32376
32702
  const settled = await settleWithin(ended.then(() => true), { timeoutMs: grace, onTimeout: () => false });
32377
32703
  if (settled) return;
32378
32704
  }
32379
- if (turn.ended || this.exited) return;
32705
+ if (turn.ended || this.exited || this.stopping) return;
32380
32706
  await this.restartInterrupted(turn);
32381
32707
  }
32382
32708
  /**
@@ -32432,6 +32758,7 @@ var StructuredSession = class extends AbstractRunnerSession {
32432
32758
  /** `message` is what opened the turn: a queued one has an id, one delivered straight from idle only its sender. */
32433
32759
  openTurn(text, message) {
32434
32760
  this.turnCount += 1;
32761
+ this.discardCompletion();
32435
32762
  const turn = { id: this.turnCount, abort: new AbortController(), ended: false, reason: "completed", steerRefused: false };
32436
32763
  this.turn = turn;
32437
32764
  this.state = "working";
@@ -32480,7 +32807,7 @@ var StructuredSession = class extends AbstractRunnerSession {
32480
32807
  offerNext() {
32481
32808
  const adapter = this.adapter;
32482
32809
  const turn = this.turn;
32483
- if (!adapter?.steer || !turn || turn.ended || turn.steerRefused || this.interrupting || this.exited) return;
32810
+ if (!adapter?.steer || !turn || turn.ended || turn.steerRefused || this.interrupting || this.exited || this.stopping) return;
32484
32811
  if (this.queue.some((m) => m.stage === "steering")) return;
32485
32812
  const next = this.queue.find((m) => m.stage === "queued");
32486
32813
  if (!next || next.command) return;
@@ -32524,6 +32851,7 @@ var StructuredSession = class extends AbstractRunnerSession {
32524
32851
  } else if (event.type === "permission_request") this.openPermission(adapter, event);
32525
32852
  else if (event.type === "permission_cancelled") this.cancelPermission(adapter, event.id);
32526
32853
  else if (event.type === "message_delivered") {
32854
+ this.discardCompletion();
32527
32855
  const message = this.delivered(event.id);
32528
32856
  if (message) this.emitEvent({ type: "message_delivered", messageId: message.id, text: message.text, ...message.from ? { from: message.from } : {} });
32529
32857
  } else if (event.type === "message_dropped") {
@@ -32555,7 +32883,7 @@ var StructuredSession = class extends AbstractRunnerSession {
32555
32883
  }
32556
32884
  return;
32557
32885
  }
32558
- if (!turn && TURN_OPENING_EVENTS.has(event.type) && !("subagentId" in event && event.subagentId)) {
32886
+ if (!turn && !this.stopping && TURN_OPENING_EVENTS.has(event.type) && !("subagentId" in event && event.subagentId)) {
32559
32887
  turn = this.openTurn("");
32560
32888
  this.offerNext();
32561
32889
  }
@@ -32604,6 +32932,24 @@ var StructuredSession = class extends AbstractRunnerSession {
32604
32932
  }
32605
32933
  this.emitEvent(event);
32606
32934
  }
32935
+ /** Publish a `task_complete` report whose turn has closed, or that waited out the grace. */
32936
+ flushCompletion() {
32937
+ if (this.completionTimer) {
32938
+ clearTimeout(this.completionTimer);
32939
+ this.completionTimer = null;
32940
+ }
32941
+ const report = this.completionReport;
32942
+ this.completionReport = null;
32943
+ if (report) this.structuredEmitter.emit("taskComplete", report);
32944
+ }
32945
+ /** A message the runner reads, or a turn it opens, voids a report made before it (ADR-0023, D6). */
32946
+ discardCompletion() {
32947
+ if (this.completionTimer) {
32948
+ clearTimeout(this.completionTimer);
32949
+ this.completionTimer = null;
32950
+ }
32951
+ this.completionReport = null;
32952
+ }
32607
32953
  /**
32608
32954
  * A queued message goes out as the turn closes, and the state never passes
32609
32955
  * through `idle` on the way, so a listener told the turn ended can already
@@ -32616,6 +32962,7 @@ var StructuredSession = class extends AbstractRunnerSession {
32616
32962
  turn.ended = true;
32617
32963
  this.text.endTurn();
32618
32964
  this.emitEvent({ type: "turn_end", reason });
32965
+ this.flushCompletion();
32619
32966
  if (reason === "failed") this.undeliverQueued();
32620
32967
  this.state = !this.exited && this.queue.length ? "working" : "idle";
32621
32968
  this.structuredEmitter.emit("turnEnd", reason);
@@ -32626,7 +32973,7 @@ var StructuredSession = class extends AbstractRunnerSession {
32626
32973
  * back in Ordewell's hands: while it is on offer or owed, nothing overtakes it.
32627
32974
  */
32628
32975
  deliverNext() {
32629
- if (this.exited || this.sending || this.turn && !this.turn.ended) return;
32976
+ if (this.exited || this.stopping || this.sending || this.turn && !this.turn.ended) return;
32630
32977
  const head = this.queue[0];
32631
32978
  if (!head || this.queue.some((message) => message.stage !== "queued")) return;
32632
32979
  this.queue.shift();
@@ -33075,7 +33422,10 @@ export {
33075
33422
  collectProviderCredentials,
33076
33423
  commandAllowedBy,
33077
33424
  configuredProviders,
33425
+ contextMeterText,
33426
+ contextMeterTitle,
33078
33427
  continuability,
33428
+ costText,
33079
33429
  countedSessionCost,
33080
33430
  createAiService,
33081
33431
  createEmptyPlan,
@@ -33118,7 +33468,9 @@ export {
33118
33468
  flattenTasksWithParents,
33119
33469
  followTaskLog,
33120
33470
  followTurn,
33471
+ formatCost,
33121
33472
  formatSearchOutput,
33473
+ formatTokenCount,
33122
33474
  fromTranscript,
33123
33475
  getLatestSession,
33124
33476
  getProviderMeta,
@@ -33170,6 +33522,7 @@ export {
33170
33522
  migratePlanState,
33171
33523
  mintDaemonToken,
33172
33524
  mintSessionId,
33525
+ modelCostTitle,
33173
33526
  namedRepos,
33174
33527
  newTaskFields,
33175
33528
  normalizeAgentArgs,
@@ -33255,6 +33608,7 @@ export {
33255
33608
  stopTurn,
33256
33609
  stripAnsi,
33257
33610
  stripModelPrefix,
33611
+ subagentCostTitle,
33258
33612
  subtreeIds,
33259
33613
  summarizeToolCall,
33260
33614
  supervisorSummary,
@@ -33273,6 +33627,7 @@ export {
33273
33627
  taskStartedNotice,
33274
33628
  taskStatusKind,
33275
33629
  taskUsageSummary,
33630
+ tasksTotalText,
33276
33631
  textHasTaskOps,
33277
33632
  textHasTaskQuery,
33278
33633
  titledRefs,
@@ -33290,6 +33645,7 @@ export {
33290
33645
  unsendAll,
33291
33646
  unsendLatest,
33292
33647
  usageLine,
33648
+ usageText,
33293
33649
  waitsForApproval,
33294
33650
  walkPlan,
33295
33651
  wellKnownBinDirs,