@ordewell/core 0.10.8 → 0.10.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/{IFileSystem-C4_mpfze.d.ts → IFileSystem-1PqRCjhu.d.mts} +23 -3
  2. package/dist/{IFileSystem-DUgeWWQN.d.mts → IFileSystem-lTRL_G9Z.d.ts} +23 -3
  3. package/dist/{ModeResolver-4qNjxcDs.d.mts → ModeResolver-6TQXVsde.d.mts} +1 -1
  4. package/dist/{ModeResolver-DGK2rWHE.d.ts → ModeResolver-CoJAJD-f.d.ts} +1 -1
  5. package/dist/{Task-C0o4i3Nn.d.mts → Task-CfBM0wkV.d.mts} +100 -24
  6. package/dist/{Task-C0o4i3Nn.d.ts → Task-CfBM0wkV.d.ts} +100 -24
  7. package/dist/{chunk-H7QX32BH.mjs → chunk-24EL4Z2W.mjs} +2 -2
  8. package/dist/{chunk-DMG4ISUG.mjs → chunk-ESKK5I4F.mjs} +145 -21
  9. package/dist/chunk-ESKK5I4F.mjs.map +1 -0
  10. package/dist/{chunk-LTHKFBKM.mjs → chunk-KWS3VEER.mjs} +3 -3
  11. package/dist/{chunk-EEE2GBLG.mjs → chunk-PFH7WFIB.mjs} +1 -1
  12. package/dist/chunk-PFH7WFIB.mjs.map +1 -0
  13. package/dist/index.d.mts +145 -20
  14. package/dist/index.d.ts +145 -20
  15. package/dist/index.js +668 -112
  16. package/dist/index.js.map +1 -1
  17. package/dist/index.mjs +537 -96
  18. package/dist/index.mjs.map +1 -1
  19. package/dist/parsing.d.mts +2 -2
  20. package/dist/parsing.d.ts +2 -2
  21. package/dist/parsing.js.map +1 -1
  22. package/dist/parsing.mjs +3 -3
  23. package/dist/plan-utils.d.mts +4 -4
  24. package/dist/plan-utils.d.ts +4 -4
  25. package/dist/plan-utils.js +144 -18
  26. package/dist/plan-utils.js.map +1 -1
  27. package/dist/plan-utils.mjs +22 -4
  28. package/dist/{runSettingsText-BgOmLxdw.d.mts → runSettingsText-C1i9wBG1.d.mts} +61 -21
  29. package/dist/{runSettingsText-BgXvajj2.d.ts → runSettingsText-CWNrmF5U.d.ts} +61 -21
  30. package/dist/testing.d.mts +7 -2
  31. package/dist/testing.d.ts +7 -2
  32. package/dist/testing.js +11 -0
  33. package/dist/testing.js.map +1 -1
  34. package/dist/testing.mjs +12 -1
  35. package/dist/testing.mjs.map +1 -1
  36. package/package.json +1 -1
  37. package/dist/chunk-DMG4ISUG.mjs.map +0 -1
  38. package/dist/chunk-EEE2GBLG.mjs.map +0 -1
  39. /package/dist/{chunk-H7QX32BH.mjs.map → chunk-24EL4Z2W.mjs.map} +0 -0
  40. /package/dist/{chunk-LTHKFBKM.mjs.map → chunk-KWS3VEER.mjs.map} +0 -0
package/dist/index.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-DMG4ISUG.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);
@@ -9072,6 +9081,8 @@ var RunnerProcess = class {
9072
9081
  markEnded;
9073
9082
  child = null;
9074
9083
  exitStatus = null;
9084
+ /** The process itself reported its end, before its pipes drained; see {@link hasExited}. */
9085
+ exitReported = false;
9075
9086
  stderrTail = "";
9076
9087
  spawnEnv = {};
9077
9088
  /** Stdout readers' end-of-stream flushes, for an end the pipe itself never reports. */
@@ -9122,6 +9133,7 @@ var RunnerProcess = class {
9122
9133
  });
9123
9134
  child.on("close", (code, signal) => this.end({ code, signal }));
9124
9135
  child.once("exit", (code, signal) => {
9136
+ this.exitReported = true;
9125
9137
  const grace = setTimeout(() => {
9126
9138
  for (const flush of [...this.stdoutEnds]) flush();
9127
9139
  this.end({ code, signal });
@@ -9176,6 +9188,15 @@ ${err.message}`);
9176
9188
  get exit() {
9177
9189
  return this.exitStatus;
9178
9190
  }
9191
+ /**
9192
+ * Whether the process has ended, for any reason. Unlike {@link exit} this is
9193
+ * true from its own 'exit', not only once 'close' has drained the pipes: a
9194
+ * runner that has exited can take no further turn even while a descendant
9195
+ * holds its stdout or stderr.
9196
+ */
9197
+ get hasExited() {
9198
+ return this.exitStatus !== null || this.exitReported;
9199
+ }
9179
9200
  /** The exit code a listener is told: -1 for a signal or a spawn failure. */
9180
9201
  get exitCode() {
9181
9202
  return this.exitStatus?.code ?? -1;
@@ -9374,6 +9395,9 @@ var StdioAgentAdapter = class {
9374
9395
  nativeSessionId() {
9375
9396
  return this.sessionId;
9376
9397
  }
9398
+ get exited() {
9399
+ return this.runner.hasExited;
9400
+ }
9377
9401
  onProcessExit(listener) {
9378
9402
  const fire = () => listener(this.runner.exitCode, this.exitMessage());
9379
9403
  if (this.runner.exit) queueMicrotask(fire);
@@ -9874,6 +9898,14 @@ function canUseToolAnswer({ input, suggestions }, decision) {
9874
9898
  }
9875
9899
  var MODEL_COMMAND = "model";
9876
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
+ }
9877
9909
  function usageRecord(usage, model, subagentId) {
9878
9910
  const record = {
9879
9911
  source: "claude-code",
@@ -9882,6 +9914,7 @@ function usageRecord(usage, model, subagentId) {
9882
9914
  if (model) record.model = model;
9883
9915
  if (usage.output_tokens !== void 0) record.outputTokens = usage.output_tokens;
9884
9916
  if (subagentId) record.subagentId = subagentId;
9917
+ else if (record.inputTokens !== void 0) record.contextTokens = record.inputTokens + (record.outputTokens ?? 0);
9885
9918
  return record;
9886
9919
  }
9887
9920
  function subagentFinalCall(result) {
@@ -9931,16 +9964,27 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
9931
9964
  replyRuns = new ReplyRuns();
9932
9965
  /** The model answering the planner's current message, and the message's id, as its `message_start` named them. */
9933
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;
9934
9973
  streamingMessage;
9935
9974
  /** The message the last `assistant` line was part of, and how many of its blocks the lines have carried. */
9936
9975
  lineMessage = null;
9937
9976
  /**
9938
- * The session's `total_cost_usd` as last counted. A resume starts from what
9939
- * was counted of that session before, and is undefined without it: the CLI
9940
- * 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.
9941
9983
  */
9942
9984
  reportedCostUsd = 0;
9943
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 };
9944
9988
  /**
9945
9989
  * Subagents started and not yet finished, keyed by the `Agent` call's id.
9946
9990
  * Kept across turns: a backgrounded one reports after its turn has ended.
@@ -10027,6 +10071,14 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
10027
10071
  if (!sessionId || this.reportedCostUsd === void 0) return null;
10028
10072
  return { sessionId, usd: this.reportedCostUsd, modelUsd: Object.fromEntries(this.reportedModelCostUsd) };
10029
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
+ }
10030
10082
  /** A count kept for another session says nothing of this one's. */
10031
10083
  countFrom(sessionId, counted) {
10032
10084
  const known = counted?.sessionId === sessionId ? counted : void 0;
@@ -10100,11 +10152,10 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
10100
10152
  * the alias resolves to now; that id is pinned before the first turn.
10101
10153
  */
10102
10154
  async handshake(opts) {
10103
- if (opts.kind !== "task") return;
10104
10155
  const initialized = (await this.control({ subtype: "initialize" }, MODEL_PIN_TIMEOUT_MS))?.response;
10105
10156
  this.listedCommands = initialized?.commands ?? [];
10106
10157
  this.listedModels = initialized?.models ?? [];
10107
- if (opts.flags.permissionMode !== "plan" || !opts.model) return;
10158
+ if (opts.kind !== "task" || opts.flags.permissionMode !== "plan" || !opts.model) return;
10108
10159
  const resolved = initialized?.models?.find((m) => m.value === opts.model)?.resolvedModel;
10109
10160
  if (resolved && resolved !== opts.model) await this.control({ subtype: "set_model", model: resolved }, MODEL_PIN_TIMEOUT_MS);
10110
10161
  }
@@ -10145,6 +10196,15 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
10145
10196
  this.interruptRequested = true;
10146
10197
  return (await this.control({ subtype: "interrupt" }, timeoutMs))?.subtype === "success";
10147
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
+ }
10148
10208
  /**
10149
10209
  * A `user` line written mid-turn is Claude Code's own queue (ADR-0023): the
10150
10210
  * CLI attaches it to the next tool result, or runs it as a turn of its own
@@ -10232,6 +10292,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
10232
10292
  this.resultHeld = false;
10233
10293
  this.turnClosed = false;
10234
10294
  this.clearFollowOnTimer();
10295
+ this.plannerModel = void 0;
10235
10296
  }
10236
10297
  /**
10237
10298
  * The echo of a steered message: the model has it. After a closed turn it
@@ -10286,6 +10347,8 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
10286
10347
  this.permissions.withdraw(msg.request_id, emit);
10287
10348
  return;
10288
10349
  case "control_response": {
10350
+ const account = msg.response?.response?.account;
10351
+ if (account) this.reportBilling(account.apiKeySource, account.subscriptionType, emit);
10289
10352
  const requestId = msg.response?.request_id;
10290
10353
  if (requestId) this.pendingControl.get(requestId)?.(msg.response ?? null);
10291
10354
  return;
@@ -10329,12 +10392,24 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
10329
10392
  emit({ type: "tool_result", id, name: "", output: diff || output, success: block.is_error !== true, subagentId });
10330
10393
  }
10331
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
+ }
10332
10404
  case "system":
10405
+ if (subagentId && (msg.subtype === "init" || msg.subtype === "compact_boundary")) return;
10333
10406
  if (msg.subtype === "init") {
10334
10407
  this.clearFollowOnTimer();
10335
10408
  if (this.refuseOtherMode(msg, emit)) return;
10409
+ if (msg.model && msg.model !== "<synthetic>") this.sessionModel = msg.model;
10336
10410
  this.readLoaded(msg, emit);
10337
10411
  this.followModel(msg.model, emit);
10412
+ this.reportBilling(msg.apiKeySource, void 0, emit);
10338
10413
  }
10339
10414
  if (msg.subtype === "commands_changed" && Array.isArray(msg.commands)) this.listedCommands = msg.commands;
10340
10415
  if (msg.subtype === "compact_boundary") {
@@ -10345,6 +10420,8 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
10345
10420
  ...typeof meta?.pre_tokens === "number" ? { preTokens: meta.pre_tokens } : {},
10346
10421
  ...typeof meta?.post_tokens === "number" ? { postTokens: meta.post_tokens } : {}
10347
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 } : {} } });
10348
10425
  }
10349
10426
  if (msg.subtype === "background_tasks_changed" && Array.isArray(msg.tasks)) {
10350
10427
  this.backgroundTaskCount = msg.tasks.length;
@@ -10356,6 +10433,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
10356
10433
  }
10357
10434
  return;
10358
10435
  case "result":
10436
+ if (subagentId) return;
10359
10437
  this.reportSessionCost(msg, emit);
10360
10438
  for (const command of this.modelCommands) command.turnsLeft = Math.max(0, command.turnsLeft - 1);
10361
10439
  if (this.interruptRequested && (msg.is_error || msg.subtype !== "success")) {
@@ -10520,16 +10598,69 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
10520
10598
  */
10521
10599
  handleStreamEvent(event, emit) {
10522
10600
  if (event.type === "message_start") {
10523
- this.plannerModel = event.message?.model;
10601
+ this.plannerModel = this.modelId(event.message?.model);
10524
10602
  this.streamingMessage = event.message?.id;
10525
10603
  this.followModel(this.plannerModel, emit);
10526
- } else if (event.type === "message_delta" && event.usage) emit({ type: "usage", record: usageRecord(event.usage, this.plannerModel) });
10527
- 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) {
10528
10611
  this.replyRuns.delta(`${this.streamingMessage ?? ""}:${event.index ?? 0}`, event.delta.text, emit);
10529
10612
  } else if (event.type === "content_block_delta" && event.delta?.type === "thinking_delta" && event.delta.thinking) {
10530
10613
  emit({ type: "thinking_delta", text: event.delta.thinking });
10531
10614
  }
10532
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
+ }
10533
10664
  /**
10534
10665
  * What only the result line knows: the cost, which covers every call the
10535
10666
  * session made — subagents included, since none of their lines carries one —
@@ -10538,24 +10669,54 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
10538
10669
  * the session before; with no count kept, the first turn only sets the
10539
10670
  * baseline: its total includes turns counted before, and a turn's own share
10540
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.
10541
10685
  */
10542
10686
  reportSessionCost(msg, emit) {
10687
+ this.reportUnstreamedUsage(msg, emit);
10543
10688
  const total = msg.total_cost_usd;
10544
10689
  const canReport = this.reportedCostUsd !== void 0;
10545
- 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
+ };
10546
10698
  if (modelCosts.length > 0) {
10547
- for (const [model, usage] of modelCosts) {
10548
- const previous = this.reportedModelCostUsd.get(model) ?? 0;
10549
- const amount = usage.costUSD - previous;
10550
- if (canReport && amount >= 0) emit({ type: "usage", record: { source: "claude-code", model, aggregate: true, reportedCost: { amount, currency: "USD" } } });
10551
- 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
+ }
10552
10706
  }
10553
- } else if (typeof total === "number" && canReport && total >= this.reportedCostUsd) {
10554
- 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 });
10555
10710
  }
10556
- if (typeof total === "number") this.reportedCostUsd = total;
10557
- const contextWindow = this.plannerModel ? msg.modelUsage?.[this.plannerModel]?.contextWindow : void 0;
10558
- if (contextWindow !== void 0) emit({ type: "usage", record: { source: "claude-code", model: this.plannerModel, contextWindow } });
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;
10716
+ }
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 } });
10559
10720
  }
10560
10721
  };
10561
10722
 
@@ -11190,9 +11351,13 @@ function mergeCodexRateLimits(known, params) {
11190
11351
  const id = snapshot.limitId ?? "";
11191
11352
  const before = known.get(id);
11192
11353
  const next = new Map(known);
11193
- 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 });
11194
11355
  return next;
11195
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
+ }
11196
11361
  function isCodexUsageLimit(error) {
11197
11362
  const info = error?.codexErrorInfo;
11198
11363
  if (info === void 0 || info === null || info === "other") return void 0;
@@ -11751,8 +11916,21 @@ ${this.exitMessage()}`);
11751
11916
  case "thread/tokenUsage/updated":
11752
11917
  this.emitUsage(msg.params, emit);
11753
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
+ }
11754
11928
  case "account/rateLimits/updated":
11755
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
+ }
11756
11934
  return;
11757
11935
  case "turn/started": {
11758
11936
  if (this.subagentOf(msg.params?.threadId)) return;
@@ -11881,6 +12059,13 @@ ${this.exitMessage()}`);
11881
12059
  * `last.outputTokens` already includes `reasoningOutputTokens` — the thread
11882
12060
  * total is `inputTokens + outputTokens`, not a sum of three — so reasoning is
11883
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`).
11884
12069
  */
11885
12070
  emitUsage(params, emit) {
11886
12071
  const last = params?.tokenUsage?.last;
@@ -11908,6 +12093,8 @@ ${this.exitMessage()}`);
11908
12093
  } else {
11909
12094
  const window = params?.tokenUsage?.modelContextWindow;
11910
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);
11911
12098
  }
11912
12099
  emit({ type: "usage", record });
11913
12100
  }
@@ -12069,7 +12256,9 @@ ${this.exitMessage()}`);
12069
12256
  }
12070
12257
  switch (item.type) {
12071
12258
  case "contextCompaction":
12072
- 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 } });
12073
12262
  return;
12074
12263
  // Codex announces the item started and completed; whichever lands first delivers.
12075
12264
  case "userMessage":
@@ -12365,8 +12554,9 @@ function usageRecord2(tokens, call) {
12365
12554
  if ((prompt.inputTokens ?? 0) + outputTokens === 0) return null;
12366
12555
  const record = { source: "opencode", ...prompt, outputTokens };
12367
12556
  if (call.model) record.model = call.model;
12368
- 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" };
12369
12558
  if (call.subagentId) record.subagentId = call.subagentId;
12559
+ else record.contextTokens = (prompt.inputTokens ?? 0) + outputTokens;
12370
12560
  return record;
12371
12561
  }
12372
12562
  function permissionReply(decision, key) {
@@ -12929,6 +13119,7 @@ var OpenCodeV2 = class {
12929
13119
  }
12930
13120
  if (frame.type === "session.compaction.ended") {
12931
13121
  onEvent({ type: "compaction", trigger: data.reason === "manual" ? "manual" : "auto" });
13122
+ onEvent({ type: "usage", record: { source: "opencode", compacted: true } });
12932
13123
  return;
12933
13124
  }
12934
13125
  if (frame.type === "session.execution.started") {
@@ -13187,7 +13378,7 @@ var OpenCodeAdapter = class {
13187
13378
  return this.runner.ended;
13188
13379
  }
13189
13380
  get exited() {
13190
- return this.runner.exit !== null;
13381
+ return this.runner.hasExited;
13191
13382
  }
13192
13383
  async start(opts) {
13193
13384
  try {
@@ -13794,6 +13985,7 @@ var OpenCodeAdapter = class {
13794
13985
  if (session && session !== this.sessionId && !turn.children.follows(session)) return;
13795
13986
  if (frame.type === "session.compacted" && session === this.sessionId) {
13796
13987
  onEvent({ type: "compaction", trigger: this.manualCompaction ? "manual" : "auto" });
13988
+ onEvent({ type: "usage", record: { source: "opencode", compacted: true } });
13797
13989
  return;
13798
13990
  }
13799
13991
  if (session === this.sessionId && this.taskTurn) this.followTaskTurn(frame, this.taskTurn, onEvent);
@@ -17348,6 +17540,7 @@ var PlannerConversation = class {
17348
17540
  const request = summaryRequest(this.currentPlanLines());
17349
17541
  const onProgress = (p) => {
17350
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 } });
17351
17544
  };
17352
17545
  let turn;
17353
17546
  if (this.canContinueLive()) {
@@ -19891,10 +20084,21 @@ var GitWorktreeIsolation = class {
19891
20084
  const entry = record.repos[repo.path];
19892
20085
  if (!entry?.changed) continue;
19893
20086
  const merged = await this.tryGit(repo.root, ["merge-base", "--is-ancestor", entry.landedTip ?? repo.integrationBranch, "HEAD"]);
19894
- if (!merged.ok) return false;
20087
+ if (!merged.ok && !await this.integrationGone(repo)) return false;
19895
20088
  }
19896
20089
  return true;
19897
20090
  }
20091
+ /**
20092
+ * The repo's integration branch is gone — deleted by hand, or by a squash
20093
+ * merge's clean-up — in a repo git can read. Merge all merges that branch by
20094
+ * name, so no merge can bring the work in again, and it is not offered as
20095
+ * work to merge. A repo git cannot read at all (moved, or git failing)
20096
+ * proves nothing either way and counts as not merged.
20097
+ */
20098
+ async integrationGone(repo) {
20099
+ if (!(await this.tryGit(repo.root, ["rev-parse", "--verify", "-q", "HEAD"])).ok) return false;
20100
+ return !(await this.tryGit(repo.root, ["rev-parse", "--verify", "-q", `refs/heads/${repo.integrationBranch}`])).ok;
20101
+ }
19898
20102
  async snapshotTree(workspaceRoot, exclude) {
19899
20103
  const scan = await this.scanGroup(workspaceRoot);
19900
20104
  if ("refused" in scan) return null;
@@ -20953,6 +21157,9 @@ function nonPromptingFs(fs18) {
20953
21157
  }
20954
21158
  };
20955
21159
  }
21160
+ function withoutContext({ contextTokens: _context, ...record }) {
21161
+ return record;
21162
+ }
20956
21163
  async function runLoop(prompt, deps) {
20957
21164
  if (deps.signal?.aborted) return "[research agent aborted before starting]";
20958
21165
  const fs18 = nonPromptingFs(deps.fs);
@@ -20960,7 +21167,8 @@ async function runLoop(prompt, deps) {
20960
21167
  (delta) => deps.onProgress?.({ type: "thinking", text: delta, subagentId: deps.subagentId }),
20961
21168
  // The record says who made the call: unstamped, the ledger would read the
20962
21169
  // subagent's prompt as the planner's own and measure its context by it.
20963
- (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 })
20964
21172
  );
20965
21173
  let turn = await chat.sendMessage(prompt, deps.signal);
20966
21174
  for (let step = 0; turn.hasToolCalls && step < SUBAGENT_LIMITS.maxSteps; step++) {
@@ -21470,7 +21678,8 @@ function usageRecordFrom(usage, model) {
21470
21678
  model,
21471
21679
  ...inputTokens !== void 0 ? { inputTokens } : {},
21472
21680
  ...reportedOutput.length > 0 ? { outputTokens: reportedOutput.reduce((a, b) => a + b, 0) } : {},
21473
- ...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) } : {}
21474
21683
  };
21475
21684
  const anyReported = record.inputTokens !== void 0 || record.outputTokens !== void 0 || record.cachedInputTokens !== void 0;
21476
21685
  return anyReported ? record : void 0;
@@ -21873,7 +22082,9 @@ var OpenAiResearchChat = class {
21873
22082
  /**
21874
22083
  * The one record for this API call, built only from what the provider
21875
22084
  * reported. OpenRouter's `cost` is in USD (its credits are dollar-pegged);
21876
- * 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.
21877
22088
  */
21878
22089
  usageRecord(reported) {
21879
22090
  if (!reported) return void 0;
@@ -21892,9 +22103,10 @@ var OpenAiResearchChat = class {
21892
22103
  hasMeasure = true;
21893
22104
  }
21894
22105
  if (reported.cost !== void 0) {
21895
- record.reportedCost = { amount: reported.cost, currency: "USD" };
22106
+ record.reportedCost = { amount: reported.cost, currency: "USD", ...record.source === "openrouter" ? { kind: "billed" } : {} };
21896
22107
  hasMeasure = true;
21897
22108
  }
22109
+ if (record.inputTokens !== void 0) record.contextTokens = record.inputTokens + (record.outputTokens ?? 0);
21898
22110
  return hasMeasure ? record : void 0;
21899
22111
  }
21900
22112
  };
@@ -23042,6 +23254,8 @@ var IsolationRunController = class {
23042
23254
  listener;
23043
23255
  liveTasks;
23044
23256
  run = null;
23257
+ /** An adoption stays scoped even while cleanup temporarily leaves no run record. */
23258
+ adoptions = 0;
23045
23259
  /** Copied paths already reported for the current run: every task gets the same copies. */
23046
23260
  reportedCopies = /* @__PURE__ */ new Set();
23047
23261
  reportedSetupFailures = /* @__PURE__ */ new Set();
@@ -23117,18 +23331,27 @@ var IsolationRunController = class {
23117
23331
  * Take over a plan's persisted isolation, or none for a plan that has not
23118
23332
  * isolated yet. Whatever a crashed process left behind for the run — a
23119
23333
  * worktree still marked active, a directory no record owns — is pruned,
23120
- * while kept, failed and conflicted worktrees stay for the user.
23334
+ * while kept, failed and conflicted worktrees stay for the user. Then git is
23335
+ * asked which landed work the user has merged since the record was saved:
23336
+ * a settled run whose landed work is all in HEAD has nothing left to hand
23337
+ * over, so it is cleared up as Merge all would have, unless it still holds
23338
+ * work that never landed (ADR-0013).
23121
23339
  *
23122
- * Deliberately not reported as a change: adopting is not a change to
23123
- * persist, and a host that adopts without persisting (VS Code's restore)
23124
- * would otherwise write a new session file on every reload.
23340
+ * Deliberately not reported to the listener: adopting is not a change of the
23341
+ * session's own, and a host that adopts without persisting (VS Code's
23342
+ * restore) would otherwise write a new session file on every reload. What
23343
+ * git changed is returned instead, for the caller to save where it can and
23344
+ * announce. A plan loaded meanwhile supersedes this adoption, which then
23345
+ * touches nothing and resolves `unchanged`.
23125
23346
  */
23126
23347
  async adopt(state) {
23348
+ const adoption = ++this.adoptions;
23127
23349
  this.run = state?.run ?? null;
23128
23350
  this.resolvers = { ...state?.resolvers ?? {} };
23129
- if (!this.run) return;
23351
+ const run = this.run;
23352
+ if (!run) return "unchanged";
23130
23353
  try {
23131
- const pruned = await this.isolation.pruneOrphans(this.run);
23354
+ const pruned = await this.isolation.pruneOrphans(run);
23132
23355
  for (const task of pruned.kept) {
23133
23356
  this.tell("warn", `Task "${task.title}" was still holding unlanded work when this plan was re-opened, so its worktree and branch were kept. Retry it to land the work, or review it by hand.`);
23134
23357
  }
@@ -23136,6 +23359,24 @@ var IsolationRunController = class {
23136
23359
  } catch (err) {
23137
23360
  this.tell("warn", `Could not prune leftover worktrees: ${err instanceof Error ? err.message : String(err)}`);
23138
23361
  }
23362
+ if (this.run !== run) return "unchanged";
23363
+ const found = await this.findLanded(run);
23364
+ if (this.run !== run) return "unchanged";
23365
+ const outcome = found ? "changed" : "unchanged";
23366
+ if (this.isOpen || !this.allMerged(run) || this.holdouts(run).length > 0) return outcome;
23367
+ const resolvers = this.resolvers;
23368
+ this.run = null;
23369
+ this.resolvers = {};
23370
+ const discarded = await this.discardMerged(run);
23371
+ if (this.adoptions !== adoption) return "unchanged";
23372
+ if (!discarded) {
23373
+ if (this.run === null) {
23374
+ this.run = run;
23375
+ this.resolvers = resolvers;
23376
+ }
23377
+ return outcome;
23378
+ }
23379
+ return this.isOpen ? "changed" : "cleared";
23139
23380
  }
23140
23381
  /** Open a run if none is, leaving whether it isolates to its first change task. */
23141
23382
  open() {
@@ -23292,8 +23533,7 @@ var IsolationRunController = class {
23292
23533
  const run = this.run;
23293
23534
  if (mode !== "isolated" || !run) return;
23294
23535
  await this.refreshInHead();
23295
- const records = Object.values(run.tasks);
23296
- if (records.some((r) => r.status === "merged") && handoffOf(run).landed.length === 0) {
23536
+ if (this.allMerged(run)) {
23297
23537
  this.tell("info", "Everything this run landed is merged into your branch already.");
23298
23538
  await this.clearMerged(run);
23299
23539
  return;
@@ -23320,11 +23560,21 @@ var IsolationRunController = class {
23320
23560
  */
23321
23561
  async refreshInHead() {
23322
23562
  const run = this.run;
23323
- if (!run || !Object.values(run.tasks).some((r) => r.status === "merged" && !r.inHead)) return false;
23563
+ if (!run) return false;
23564
+ const found = await this.findLanded(run);
23565
+ if (found) this.listener.changed();
23566
+ return found;
23567
+ }
23568
+ /** Mark the run's landed work git finds in HEAD; true when some was found. */
23569
+ async findLanded(run) {
23570
+ if (!Object.values(run.tasks).some((r) => r.status === "merged" && !r.inHead)) return false;
23324
23571
  const found = await this.isolation.findInHead(run).catch(() => []);
23325
- if (found.length > 0) this.listener.changed();
23326
23572
  return found.length > 0;
23327
23573
  }
23574
+ /** The run landed something and every landing is in the user's HEAD. */
23575
+ allMerged(run) {
23576
+ return Object.values(run.tasks).some((r) => r.status === "merged") && handoffOf(run).landed.length === 0;
23577
+ }
23328
23578
  /**
23329
23579
  * The tracked state of the workspace as an attempt whose tree is checked
23330
23580
  * starts (see `attemptKind.checksTree`), for {@link filesChangedSince}
@@ -23446,7 +23696,7 @@ var IsolationRunController = class {
23446
23696
  */
23447
23697
  async clearMerged(run) {
23448
23698
  const live = this.liveRecords(run);
23449
- const unlanded = Object.values(run.tasks).filter((r) => r.status !== "merged" && !live.includes(r));
23699
+ const unlanded = this.holdouts(run).filter((r) => !live.includes(r));
23450
23700
  if (live.length > 0 || unlanded.length > 0) {
23451
23701
  const why = [
23452
23702
  ...live.length > 0 ? [`${tasksNamed(live)} ${live.length === 1 ? "is" : "are"} still running or waiting on you`] : [],
@@ -23456,13 +23706,22 @@ var IsolationRunController = class {
23456
23706
  this.tell("info", `The run is not cleared up: ${why.join(", and ")}, so ${one ? "its worktree stays" : "their worktrees stay"}, and so do the run's branches.`);
23457
23707
  return;
23458
23708
  }
23709
+ if (await this.discardMerged(run)) this.forget();
23710
+ }
23711
+ /** The records that keep a settled run from being cleared up: live, or holding work that never landed. */
23712
+ holdouts(run) {
23713
+ const live = this.liveTasks();
23714
+ return Object.values(run.tasks).filter((r) => live.has(r.taskId) || r.status !== "merged");
23715
+ }
23716
+ /** The run's worktrees and merged branches go; false, said, when git would not let them. */
23717
+ async discardMerged(run) {
23459
23718
  try {
23460
23719
  this.report(await this.isolation.discard(run, { integration: "delete-merged" }));
23720
+ return true;
23461
23721
  } catch (err) {
23462
23722
  this.tell("warn", `Merged, but could not clean up the run's worktrees and branches: ${err instanceof Error ? err.message : String(err)}`);
23463
- return;
23723
+ return false;
23464
23724
  }
23465
- this.forget();
23466
23725
  }
23467
23726
  /** Say what a removal kept back instead of deleting: where the work went, or which worktree stayed. */
23468
23727
  report(removal) {
@@ -24018,6 +24277,10 @@ var TaskOrchestrator = class _TaskOrchestrator {
24018
24277
  store;
24019
24278
  output;
24020
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();
24021
24284
  verifier = new VerdictEngine();
24022
24285
  /** Changed only by start, stop, a plan load, a usage-limit pause, a move off the limit, and {@link settleRun}. */
24023
24286
  runState = "idle";
@@ -24566,8 +24829,9 @@ var TaskOrchestrator = class _TaskOrchestrator {
24566
24829
  return blockingFailures(this.store.planTasks).get(taskId) ?? [];
24567
24830
  }
24568
24831
  loadPlan(tasks, planRunners = [...DEFAULT_RUNNERS]) {
24569
- this.store.load(tasks, planRunners);
24570
24832
  const repairs = this.endAllAttempts("load").filter((a) => a.kind.kind === "repair" && a.worktree);
24833
+ this.planScope += 1;
24834
+ this.store.load(tasks, planRunners);
24571
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}"`);
24572
24836
  this.runs.interrupt({ keepOpen: this.isScheduling });
24573
24837
  if (this.runState !== "scheduling") this.runState = "idle";
@@ -24653,6 +24917,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
24653
24917
  if (reason === "user") this.stopGeneration++;
24654
24918
  this.runState = "idle";
24655
24919
  this.verifier.reset();
24920
+ for (const attempt of this.attempts.values()) this.stopRunner(attempt);
24656
24921
  this.runner.stopAll();
24657
24922
  const stopped = [];
24658
24923
  for (const a of this.endAllAttempts("stop")) {
@@ -24766,6 +25031,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
24766
25031
  if (!task || !attempt) return;
24767
25032
  attempt.decided = true;
24768
25033
  const stoppedMidTurn = endedByExit(verdict) ? saidInFlight(attempt) : null;
25034
+ this.stopRunner(attempt);
24769
25035
  console.error(`[TaskOrchestrator] Task ${taskRefOf(this.store.planTasks, task.id) ?? `#${task.order}`} "${task.title}" verdict=${verdict.outcome}`);
24770
25036
  console.error(`[TaskOrchestrator] Runner: ${task.assignedRunner}, Model: ${task.assignedModel?.modelId ?? "default"}`);
24771
25037
  console.error(`[TaskOrchestrator] Prompt preview: ${(task.prompt ?? "").slice(0, 200)}`);
@@ -25677,7 +25943,9 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
25677
25943
  autoResumed: opts.autoResumed === true,
25678
25944
  turnError: null,
25679
25945
  turnOutputFrom: 0,
25680
- reminded: false
25946
+ reminded: false,
25947
+ runnerStopped: false,
25948
+ planScope: this.planScope
25681
25949
  };
25682
25950
  this.spawnCounts.set(task.id, attempt.attempt);
25683
25951
  this.attempts.set(task.id, attempt);
@@ -25714,6 +25982,7 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
25714
25982
  const manifest = this.registry?.get(attempt.runner)?.manifest;
25715
25983
  const unavailable = manifest && unavailableTaskModeMessage(manifest, task.taskMode);
25716
25984
  if (unavailable) throw new Error(unavailable);
25985
+ if (kind.kind === "continuation") await this.windDowns.get(task.id);
25717
25986
  const { cwd, worktree, previousWork } = await attemptCwd(kind, task, this.runs);
25718
25987
  attempt.cwd = cwd;
25719
25988
  attempt.worktree = worktree;
@@ -25769,10 +26038,11 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
25769
26038
  }
25770
26039
  if (takesSkills(kind)) this.store.setTaskAttemptSkills(task.id, [...attempt.skills]);
25771
26040
  session.onEvent((event) => {
25772
- 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;
25773
26042
  this.store.recordAttemptUsage(task.id, attempt.attempt, event.record, (/* @__PURE__ */ new Date()).toISOString());
25774
26043
  this.emit("onUsageChanged");
25775
26044
  });
26045
+ session.onExit(() => this.woundDown(attempt, session));
25776
26046
  this.output.attach(task.id, session);
25777
26047
  this.verifier.watch(task, session);
25778
26048
  session.onEvent((event) => {
@@ -25941,10 +26211,35 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
25941
26211
  if (stopsRunner(reason)) {
25942
26212
  const task = this.store.get(taskId);
25943
26213
  if (task) this.verifier.clear(task);
25944
- if (session) this.runner.stop(session.id);
26214
+ if (attempt) this.stopRunner(attempt);
25945
26215
  }
25946
26216
  return attempt;
25947
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
+ }
25948
26243
  /** Kept on the task once the attempt ends, so a continue can resume it after a reload (ADR-0018, K1). */
25949
26244
  saveNativeSession(taskId, nativeSessionId, runner) {
25950
26245
  if (nativeSessionId) this.store.setTaskRunnerSessionId(taskId, nativeSessionId, runner);
@@ -27117,17 +27412,37 @@ var CliAgentAiService = class {
27117
27412
  * `startConversation` reseeds from Ordewell's transcript, which is the same
27118
27413
  * degradation `restoreChat` already performs on every surface.
27119
27414
  *
27415
+ * A process can end while idle without Ordewell stopping it — the OS, the
27416
+ * CLI itself, a sleeping machine, a signal sent to its group — and every
27417
+ * path of ours that kills one also drops its adapter. So an adapter still
27418
+ * held whose process has ended died on its own, and is restarted here
27419
+ * rather than answering the next message with its exit code.
27420
+ *
27120
27421
  * Also restarted when the policy's decision on the runner's own search has
27121
27422
  * changed since the process was spawned: the running thread would otherwise
27122
27423
  * keep searching unasked after Full turned Guarded. Codex 0.162 applies
27123
27424
  * `thread/resume`'s `config.web_search` either way (checked live).
27124
27425
  */
27125
27426
  async ensureAdapter(session) {
27126
- if (session.adapter && session.nativeSearch === this.nativeSearch(session)) return session.adapter;
27427
+ const live = session.adapter;
27428
+ if (live && !live.exited && session.nativeSearch === this.nativeSearch(session)) return live;
27127
27429
  session.stop();
27128
27430
  const start = session.start;
27129
27431
  if (!start) throw new Error("No active planner conversation.");
27130
- return this.startAdapter({ ...start.startOptions, resumeSessionId: session.nativeSessionId ?? void 0, resumeCost: session.sessionCost ?? void 0 }, start.tools, session);
27432
+ const resumeSessionId = session.nativeSessionId ?? void 0;
27433
+ try {
27434
+ const adapter = await this.startAdapter({ ...start.startOptions, resumeSessionId, resumeCost: session.sessionCost ?? void 0 }, start.tools, session);
27435
+ const resumed = adapter.nativeSessionId();
27436
+ if (resumeSessionId && resumed && resumed !== resumeSessionId) {
27437
+ throw new Error("The runner started a different session instead.");
27438
+ }
27439
+ return adapter;
27440
+ } catch (err) {
27441
+ if (!resumeSessionId || session.discarded.aborted) throw err;
27442
+ if (this.session === session) this.session = null;
27443
+ session.discard();
27444
+ throw new Error(`The ${this.runner} planner's session could not be resumed (${err instanceof Error ? err.message : String(err)}). Send your message again: the planner picks the conversation up from its transcript.`);
27445
+ }
27131
27446
  }
27132
27447
  /**
27133
27448
  * What the planner gets of the repository's config, its runner being
@@ -28659,19 +28974,50 @@ var TaskLogRecorder = class {
28659
28974
  };
28660
28975
 
28661
28976
  // src/services/ContextWindowFill.ts
28662
- var ContextWindowFill = class {
28663
- constructor(catalog, last = {}) {
28977
+ var SessionWindows = class {
28978
+ constructor(catalog = () => void 0) {
28664
28979
  this.catalog = catalog;
28665
- if (last.contextWindow !== void 0) this.known.set(modelKey(last.source, last.model), last.contextWindow);
28666
28980
  }
28667
28981
  catalog;
28668
- /** 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. */
28669
29007
  known = /* @__PURE__ */ new Map();
28670
29008
  fill(record) {
28671
29009
  if (record.subagentId || record.aggregate) return record;
28672
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
+ }
28673
29018
  const reported = record.contextWindow !== void 0 && record.contextWindow > 0 ? record.contextWindow : void 0;
28674
- 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);
28675
29021
  if (window !== void 0) this.known.set(key, window);
28676
29022
  return window === void 0 || window === record.contextWindow ? record : { ...record, contextWindow: window };
28677
29023
  }
@@ -28679,9 +29025,9 @@ var ContextWindowFill = class {
28679
29025
  function modelKey(source, model) {
28680
29026
  return JSON.stringify([source ?? null, model ?? null]);
28681
29027
  }
28682
- function withContextWindows(runner, catalog) {
29028
+ function withContextWindows(runner, windows) {
28683
29029
  return {
28684
- 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)),
28685
29031
  stop: (sessionId) => runner.stop(sessionId),
28686
29032
  stopAll: () => runner.stopAll(),
28687
29033
  get activeCount() {
@@ -28712,12 +29058,12 @@ function positive(n) {
28712
29058
 
28713
29059
  // src/services/PlannerUsage.ts
28714
29060
  var PlannerUsageLedger = class {
28715
- /** `catalogWindow` serves the window of a planner model whose backend reports none. */
28716
- constructor(catalogWindow = () => void 0) {
28717
- this.catalogWindow = catalogWindow;
28718
- 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);
28719
29065
  }
28720
- catalogWindow;
29066
+ sessionWindows;
28721
29067
  usage = { totals: {} };
28722
29068
  windows;
28723
29069
  /** Fold one model call's record into the totals; returns the running ledger. */
@@ -28728,16 +29074,16 @@ var PlannerUsageLedger = class {
28728
29074
  /** Adopt a persisted ledger (a reopened session) or start from zero. */
28729
29075
  restore(usage) {
28730
29076
  this.usage = usage ?? { totals: {} };
28731
- this.windows = new ContextWindowFill(this.catalogWindow, this.usage);
29077
+ this.windows = new ContextWindowFill(this.sessionWindows, this.usage);
28732
29078
  }
28733
29079
  /** Start a fresh session's ledger from zero. */
28734
29080
  clear() {
28735
29081
  this.usage = { totals: {} };
28736
- this.windows = new ContextWindowFill(this.catalogWindow);
29082
+ this.windows = new ContextWindowFill(this.sessionWindows);
28737
29083
  }
28738
- /** Whether anything has been recorded — a plan with no usage says nothing. */
29084
+ /** Whether the ledger has measures or session billing facts to broadcast. */
28739
29085
  get hasUsage() {
28740
- return isMeasured(this.usage.totals);
29086
+ return isMeasured(this.usage.totals) || this.usage.sessionBilling !== void 0;
28741
29087
  }
28742
29088
  /** The value persisted onto the plan state. */
28743
29089
  snapshot() {
@@ -30186,7 +30532,7 @@ function createSession(deps) {
30186
30532
  const backend = new PlannerBackend(deps.aiService ? { pinned: deps.aiService, busy } : { provider: () => deps.config.aiProvider, create: () => createAiService(deps.config, { workspaceRoot: deps.workspaceRoot, mcpServer }), busy });
30187
30533
  const aiService = () => backend.current();
30188
30534
  const store = new PlanStore();
30189
- const catalogWindow = (source, model) => deps.modelResolver.contextWindowFor?.(source, model);
30535
+ const windows = new SessionWindows((source, model) => deps.modelResolver.contextWindowFor?.(source, model));
30190
30536
  const taskLogs = new TaskLogRecorder({
30191
30537
  broadcast: deps.broadcast,
30192
30538
  // Read through the Session, whose id and workspace are the ones it saves
@@ -30219,7 +30565,7 @@ function createSession(deps) {
30219
30565
  const orchestrator = TaskOrchestrator.compose({
30220
30566
  config: deps.config,
30221
30567
  notifications: deps.notifications,
30222
- runner: taskLogs.wrap(runnerApprovals.wrap(withContextWindows(deps.runner, catalogWindow))),
30568
+ runner: taskLogs.wrap(runnerApprovals.wrap(withContextWindows(deps.runner, windows))),
30223
30569
  store,
30224
30570
  output: deps.taskOutput,
30225
30571
  isolation: deps.isolation,
@@ -30231,7 +30577,7 @@ function createSession(deps) {
30231
30577
  runSettings: () => session.planState?.runSettings,
30232
30578
  isApproved: (taskId) => session.isApproved(taskId)
30233
30579
  });
30234
- const usage = new PlannerUsageLedger(catalogWindow);
30580
+ const usage = new PlannerUsageLedger(windows);
30235
30581
  const events = new SessionEventRelay({
30236
30582
  broadcast: deps.broadcast,
30237
30583
  onNotice: deps.onNotice,
@@ -30301,6 +30647,7 @@ function createSession(deps) {
30301
30647
  runs: orchestrator.runs,
30302
30648
  events,
30303
30649
  usage,
30650
+ windows,
30304
30651
  approvals,
30305
30652
  approvalPolicy,
30306
30653
  taskLogs,
@@ -30328,6 +30675,7 @@ function configSecrets(config) {
30328
30675
  var Session = class {
30329
30676
  backend;
30330
30677
  usage;
30678
+ windows;
30331
30679
  events;
30332
30680
  planner;
30333
30681
  orchestrator;
@@ -30433,6 +30781,7 @@ var Session = class {
30433
30781
  this.runs = parts.runs;
30434
30782
  this.events = parts.events;
30435
30783
  this.usage = parts.usage;
30784
+ this.windows = parts.windows;
30436
30785
  this.approvals = parts.approvals;
30437
30786
  this.approvalPolicy = parts.approvalPolicy;
30438
30787
  this.taskLogs = parts.taskLogs;
@@ -30911,6 +31260,7 @@ var Session = class {
30911
31260
  this.syncPlanTasks();
30912
31261
  this.plan.isolation = this.runs.planIsolation ?? void 0;
30913
31262
  this.plan.plannerUsage = this.usage.snapshot();
31263
+ this.plan.contextWindows = this.windows.snapshot();
30914
31264
  this.plan.supervision = this.supervision.snapshot();
30915
31265
  this.plan.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
30916
31266
  this.save(this.plan, this.goal, this.workspace, this.currentSessionId);
@@ -30940,6 +31290,7 @@ var Session = class {
30940
31290
  this.owingEffects.clear();
30941
31291
  this.conversation.reset();
30942
31292
  this.usage.clear();
31293
+ this.windows.clear();
30943
31294
  this.approvals.clear(isPlannerApproval);
30944
31295
  if (!keepGrants) this.approvalPolicy.reset();
30945
31296
  this.store.clearLog();
@@ -31280,7 +31631,18 @@ var Session = class {
31280
31631
  if (!this.plan) throw new ConversationEditError("No planning conversation to condense");
31281
31632
  const opening = this.plannerTurnMayOpen(signal);
31282
31633
  if (opening) await opening;
31283
- 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;
31284
31646
  }
31285
31647
  rewindTargets() {
31286
31648
  return this.conversation.rewindTargets();
@@ -31590,9 +31952,9 @@ var Session = class {
31590
31952
  runSnapshot() {
31591
31953
  if (!this.plan) return EMPTY_RUN;
31592
31954
  const planned = reduceRun(EMPTY_RUN, { type: "plan_snapshot", plan: surfacePlan({ ...this.plan, tasks: this.store.snapshot() }) });
31593
- const view = reduceRun(planned, this.events.statusMessage());
31594
- const held2 = this.sentHeld ? reduceRun(view, { type: "plan_edit_held", held: this.sentHeld }) : view;
31595
- return this.sentHandoff ? reduceRun(held2, { type: "isolation_handoff", ...this.sentHandoff }) : held2;
31955
+ const handed = this.sentHandoff ? reduceRun(planned, { type: "isolation_handoff", ...this.sentHandoff }) : planned;
31956
+ const view = reduceRun(handed, this.events.statusMessage());
31957
+ return this.sentHeld ? reduceRun(view, { type: "plan_edit_held", held: this.sentHeld }) : view;
31596
31958
  }
31597
31959
  /** The ancestors a task waits on at its merge gate (ADR-0020); empty when it waits for no Merge all. */
31598
31960
  mergeGate(taskId) {
@@ -31768,7 +32130,10 @@ var Session = class {
31768
32130
  if (!plan.approvedTasks && plan.approved) {
31769
32131
  plan.approvedTasks = flattenTasks(plan.tasks).map((t) => t.id);
31770
32132
  }
31771
- if (adopting) this.usage.restore(plan.plannerUsage);
32133
+ if (adopting) {
32134
+ this.usage.restore(plan.plannerUsage);
32135
+ this.windows.restore(plan.contextWindows);
32136
+ }
31772
32137
  if (opts?.sessionId) this.currentSessionId = opts.sessionId;
31773
32138
  if (adopting) {
31774
32139
  for (const task of flattenTasks(plan.tasks)) delete task.resumeAt;
@@ -31782,12 +32147,31 @@ var Session = class {
31782
32147
  }
31783
32148
  carryOverQueuedEdits(plan);
31784
32149
  if (adopting) {
31785
- void this.runs.adopt(plan.isolation ?? null);
32150
+ void this.adoptRun(plan, opts?.persist !== false || opts?.sessionId !== void 0);
31786
32151
  this.sentHandoff = this.owedHandoff();
31787
32152
  }
31788
32153
  if (opts?.persist !== false) this.persist();
31789
32154
  if (this.usage.hasUsage) this.broadcast(this.usage.message());
31790
32155
  }
32156
+ /**
32157
+ * Take the loaded plan's run over, then correct what the surfaces were told
32158
+ * by what git found: the saved record cannot know what the user merged
32159
+ * since, and its handoff would offer Merge all for work already in their
32160
+ * branch. The status update says what landed work is still unmerged, which
32161
+ * a handoff folds to (see `reduceRun`), and `run_closed` that the run was
32162
+ * cleared up. Saved only where the load itself may save (`persists`), so a
32163
+ * restore with no session file to write keeps the correction in memory for
32164
+ * its next save. A plan loaded meanwhile is not this one's to correct.
32165
+ * Starts nothing (ADR-0044).
32166
+ */
32167
+ async adoptRun(plan, persists) {
32168
+ const outcome = await this.runs.adopt(plan.isolation ?? null);
32169
+ if (this.closed || this.plan !== plan || outcome === "unchanged") return;
32170
+ plan.isolation = this.runs.planIsolation ?? void 0;
32171
+ if (persists) this.persist();
32172
+ this.events.status(plan);
32173
+ if (outcome === "cleared") this.broadcast({ type: "run_closed" });
32174
+ }
31791
32175
  destroy() {
31792
32176
  this.closed = true;
31793
32177
  this.dropPlanScope();
@@ -31836,17 +32220,12 @@ var AbstractRunner = class {
31836
32220
  get activeCount() {
31837
32221
  return this.sessions.size;
31838
32222
  }
32223
+ /** A session winding down stays registered until it exits, so a second stop gets the same wind-down. */
31839
32224
  stop(sessionId) {
31840
- const session = this.sessions.get(sessionId);
31841
- if (!session) return;
31842
- session.kill();
31843
- this.sessions.delete(sessionId);
32225
+ return this.sessions.get(sessionId)?.stop() ?? Promise.resolve();
31844
32226
  }
31845
32227
  stopAll() {
31846
- for (const [, session] of this.sessions) {
31847
- session.kill();
31848
- }
31849
- this.sessions.clear();
32228
+ for (const session of this.sessions.values()) void session.stop();
31850
32229
  }
31851
32230
  registerSession(id, session) {
31852
32231
  this.sessions.set(id, session);
@@ -32004,6 +32383,8 @@ var StructuredSession = class extends AbstractRunnerSession {
32004
32383
  queue = [];
32005
32384
  messageCount = 0;
32006
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;
32007
32388
  lastSessionId = null;
32008
32389
  lastSessionCost = null;
32009
32390
  structuredEmitter = new EventEmitter2();
@@ -32029,7 +32410,7 @@ var StructuredSession = class extends AbstractRunnerSession {
32029
32410
  await this.startAttached(this.launch.startOptions);
32030
32411
  this.state = "working";
32031
32412
  setImmediate(() => {
32032
- if (!this.exited) this.deliver(prompt);
32413
+ if (!this.exited && !this.stopping) this.deliver(prompt);
32033
32414
  });
32034
32415
  }
32035
32416
  getOutput() {
@@ -32109,6 +32490,53 @@ var StructuredSession = class extends AbstractRunnerSession {
32109
32490
  this.withdrawPermissions();
32110
32491
  this.baseHandleExit(-1);
32111
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
+ }
32112
32540
  turnState() {
32113
32541
  return this.state;
32114
32542
  }
@@ -32134,7 +32562,7 @@ var StructuredSession = class extends AbstractRunnerSession {
32134
32562
  this.messageCount += 1;
32135
32563
  const id = `msg-${this.messageCount}`;
32136
32564
  const sender2 = opening.from ? { from: opening.from } : {};
32137
- 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 });
32138
32566
  else if (this.state === "idle" && this.adapterStarted && !this.sending) this.deliver(text, opening);
32139
32567
  else {
32140
32568
  this.state = "working";
@@ -32189,7 +32617,7 @@ var StructuredSession = class extends AbstractRunnerSession {
32189
32617
  });
32190
32618
  }
32191
32619
  runningTurn() {
32192
- 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;
32193
32621
  }
32194
32622
  /** No runner can recall a message once offered to it, so only a queued one comes back. */
32195
32623
  removeQueued(id) {
@@ -32250,7 +32678,7 @@ var StructuredSession = class extends AbstractRunnerSession {
32250
32678
  if (this.interrupting) return this.interrupting;
32251
32679
  const turn = this.turn;
32252
32680
  const adapter = this.adapter;
32253
- if (!turn || turn.ended || !adapter) return Promise.resolve();
32681
+ if (!turn || turn.ended || !adapter || this.stopping) return Promise.resolve();
32254
32682
  this.interrupting = this.interruptTurn(turn, adapter).finally(() => {
32255
32683
  this.interrupting = null;
32256
32684
  this.offerNext();
@@ -32264,7 +32692,7 @@ var StructuredSession = class extends AbstractRunnerSession {
32264
32692
  const settled = await settleWithin(ended.then(() => true), { timeoutMs: grace, onTimeout: () => false });
32265
32693
  if (settled) return;
32266
32694
  }
32267
- if (turn.ended || this.exited) return;
32695
+ if (turn.ended || this.exited || this.stopping) return;
32268
32696
  await this.restartInterrupted(turn);
32269
32697
  }
32270
32698
  /**
@@ -32368,7 +32796,7 @@ var StructuredSession = class extends AbstractRunnerSession {
32368
32796
  offerNext() {
32369
32797
  const adapter = this.adapter;
32370
32798
  const turn = this.turn;
32371
- 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;
32372
32800
  if (this.queue.some((m) => m.stage === "steering")) return;
32373
32801
  const next = this.queue.find((m) => m.stage === "queued");
32374
32802
  if (!next || next.command) return;
@@ -32443,7 +32871,7 @@ var StructuredSession = class extends AbstractRunnerSession {
32443
32871
  }
32444
32872
  return;
32445
32873
  }
32446
- 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)) {
32447
32875
  turn = this.openTurn("");
32448
32876
  this.offerNext();
32449
32877
  }
@@ -32514,7 +32942,7 @@ var StructuredSession = class extends AbstractRunnerSession {
32514
32942
  * back in Ordewell's hands: while it is on offer or owed, nothing overtakes it.
32515
32943
  */
32516
32944
  deliverNext() {
32517
- 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;
32518
32946
  const head = this.queue[0];
32519
32947
  if (!head || this.queue.some((message) => message.stage !== "queued")) return;
32520
32948
  this.queue.shift();
@@ -32683,11 +33111,13 @@ var STATE_FILE = "state.json";
32683
33111
  function getStatePath(baseDir) {
32684
33112
  return path33.join(getStateDir(baseDir), STATE_FILE);
32685
33113
  }
32686
- function saveState(plan, baseDir) {
33114
+ var SESSION_ID_KEY = "sessionId";
33115
+ function saveState(plan, baseDir, sessionId) {
32687
33116
  const statePath = getStatePath(baseDir ?? process.cwd());
32688
33117
  ensureDir(path33.dirname(statePath));
32689
33118
  plan.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
32690
- fs17.writeFileSync(statePath, JSON.stringify(plan, null, 2));
33119
+ const file = sessionId ? { ...plan, [SESSION_ID_KEY]: sessionId } : plan;
33120
+ fs17.writeFileSync(statePath, JSON.stringify(file, null, 2));
32691
33121
  }
32692
33122
  function loadState(baseDir, logger = defaultLogger) {
32693
33123
  const dir = baseDir ?? process.cwd();
@@ -32700,10 +33130,12 @@ function loadState(baseDir, logger = defaultLogger) {
32700
33130
  logger.warn("stateStore", `rejecting state from ${statePath}: old format (scalar "runner" instead of "runners" array). Please re-plan.`);
32701
33131
  return null;
32702
33132
  }
33133
+ const sessionId = parsed[SESSION_ID_KEY];
33134
+ delete parsed[SESSION_ID_KEY];
32703
33135
  const plan = parsed;
32704
33136
  migratePlanStateIsolation(plan);
32705
33137
  migrateLoadedTasks(plan.tasks);
32706
- return plan;
33138
+ return typeof sessionId === "string" && sessionId ? { plan, sessionId } : { plan };
32707
33139
  } catch (err) {
32708
33140
  logger.warn("stateStore", `failed to load state from ${statePath}; falling back to empty state`, err);
32709
33141
  return null;
@@ -32959,7 +33391,10 @@ export {
32959
33391
  collectProviderCredentials,
32960
33392
  commandAllowedBy,
32961
33393
  configuredProviders,
33394
+ contextMeterText,
33395
+ contextMeterTitle,
32962
33396
  continuability,
33397
+ costText,
32963
33398
  countedSessionCost,
32964
33399
  createAiService,
32965
33400
  createEmptyPlan,
@@ -33002,7 +33437,9 @@ export {
33002
33437
  flattenTasksWithParents,
33003
33438
  followTaskLog,
33004
33439
  followTurn,
33440
+ formatCost,
33005
33441
  formatSearchOutput,
33442
+ formatTokenCount,
33006
33443
  fromTranscript,
33007
33444
  getLatestSession,
33008
33445
  getProviderMeta,
@@ -33054,6 +33491,7 @@ export {
33054
33491
  migratePlanState,
33055
33492
  mintDaemonToken,
33056
33493
  mintSessionId,
33494
+ modelCostTitle,
33057
33495
  namedRepos,
33058
33496
  newTaskFields,
33059
33497
  normalizeAgentArgs,
@@ -33139,6 +33577,7 @@ export {
33139
33577
  stopTurn,
33140
33578
  stripAnsi,
33141
33579
  stripModelPrefix,
33580
+ subagentCostTitle,
33142
33581
  subtreeIds,
33143
33582
  summarizeToolCall,
33144
33583
  supervisorSummary,
@@ -33157,6 +33596,7 @@ export {
33157
33596
  taskStartedNotice,
33158
33597
  taskStatusKind,
33159
33598
  taskUsageSummary,
33599
+ tasksTotalText,
33160
33600
  textHasTaskOps,
33161
33601
  textHasTaskQuery,
33162
33602
  titledRefs,
@@ -33174,6 +33614,7 @@ export {
33174
33614
  unsendAll,
33175
33615
  unsendLatest,
33176
33616
  usageLine,
33617
+ usageText,
33177
33618
  waitsForApproval,
33178
33619
  walkPlan,
33179
33620
  wellKnownBinDirs,