@link-assistant/hive-mind 1.76.1 → 1.76.2

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,42 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.76.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 5d8d6c1: fix(cost): accumulate Anthropic cost across limit-reset resumes (#1886)
8
+
9
+ The session cost summary could report a large negative "Difference" (e.g.
10
+ `$-11.422796 (-31.66%)`) between the public pricing estimate and the Anthropic
11
+ figure. Root cause: the public estimate is computed from the session JSONL,
12
+ which accumulates the **entire** session across every limit-reset resume, while
13
+ the Anthropic `total_cost_usd` from the stream-json `result` event is scoped to a
14
+ **single** Claude process (only the resumed run). Comparing a full-session
15
+ estimate against a single-process figure produced a misleading gap even though
16
+ both numbers were individually correct.
17
+
18
+ The per-token math (`calculateModelCost`) was audited and is correct; this is a
19
+ scope mismatch, not a pricing error.
20
+
21
+ Fix:
22
+ - New `src/anthropic-cost-accumulator.lib.mjs` keeps a model-agnostic running
23
+ total of Anthropic's per-process `total_cost_usd` (it sums dollars, never
24
+ inspecting per-token prices, so it is correct for all models).
25
+ - `runClaude` seeds from and returns the cumulative total on every terminal path;
26
+ the cross-process limit-reset resume threads it via a new hidden
27
+ `--previous-anthropic-cost` option (`autoContinueWhenLimitResets`).
28
+ - A usage-limit hit ends as `is_error` with no `success` result event, so its
29
+ cost was previously discarded. The cost from a non-success terminal `result`
30
+ event is now kept as a fallback and folded into the accumulator, closing the
31
+ gap in the reported scenario.
32
+ - `displayCostComparison` / `displaySessionTokenUsage` print a verbose
33
+ accumulation breakdown ("cumulative across resume iterations: this run … +
34
+ carried forward … = …") so the figure is never mysterious again.
35
+
36
+ A deep case study (timeline, proven root causes, exact reproduced numbers, online
37
+ prior art incl. `anthropics/claude-code#13088`) is compiled under
38
+ `docs/case-studies/issue-1886/`.
39
+
3
40
  ## 1.76.1
4
41
 
5
42
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.76.1",
3
+ "version": "1.76.2",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Issue #1886: Cumulative Anthropic cost across resume iterations.
5
+ *
6
+ * Background
7
+ * ----------
8
+ * The "Token Usage Summary" compares two numbers:
9
+ * - "Public pricing estimate" — computed locally from the session JSONL by
10
+ * `calculateSessionTokens`. The JSONL accumulates the *entire* logical
11
+ * session: when a run hits a usage limit and is resumed (either in-process
12
+ * via the auto-merge loop, or cross-process via
13
+ * `autoContinueWhenLimitResets` spawning a fresh `solve` with `--resume`),
14
+ * Claude Code appends to the *same* `<session-id>.jsonl`, so every run's
15
+ * tokens are present.
16
+ * - "Calculated by Anthropic" — taken from the `result` event's
17
+ * `total_cost_usd`. That figure is scoped to a *single* Claude process: it
18
+ * only covers the tokens that process produced, NOT the tokens inherited
19
+ * from a previous run that was interrupted by a limit reset.
20
+ *
21
+ * The result is a scope mismatch, not a pricing bug. In issue #1886 the public
22
+ * estimate ($36.085016, full session) was compared against Anthropic's
23
+ * per-process figure ($24.662220, the resumed run only), yielding a misleading
24
+ * "-31.66%" difference even though both numbers are individually correct.
25
+ *
26
+ * The fix
27
+ * -------
28
+ * Accumulate Anthropic's reported cost across resume iterations so the figure
29
+ * shown next to the full-session public estimate covers the same scope. This
30
+ * module is the single source of truth for that running total:
31
+ *
32
+ * - Each `solve` process seeds the accumulator once from
33
+ * `--previous-anthropic-cost` (0 for the first run; the carried-forward
34
+ * total for an auto-resumed run).
35
+ * - Every finished Claude process adds its own `total_cost_usd` via
36
+ * `addAnthropicRunCost`, which also covers the in-process auto-merge loop
37
+ * (each iteration is a separate Claude process in the same node process).
38
+ * - The display and the cross-process spawn both read the cumulative total,
39
+ * so "Calculated by Anthropic" tracks the full session.
40
+ *
41
+ * The accumulation is model-agnostic: it sums dollar figures and never inspects
42
+ * per-token prices, so it is correct for Fable 5, Opus, Sonnet, Haiku, and any
43
+ * future model. See docs/case-studies/issue-1886/ for the full analysis.
44
+ */
45
+
46
+ // Module-level singleton: the cumulative Anthropic cost for the current logical
47
+ // session (this node process plus everything seeded from prior processes).
48
+ let cumulativeAnthropicCostUSD = 0;
49
+ // Seeding must happen exactly once per node process. The auto-merge loop calls
50
+ // runClaude (and therefore the seed helper) repeatedly within a single process;
51
+ // re-seeding from the same CLI flag each time would wipe out accumulation.
52
+ let seeded = false;
53
+
54
+ /**
55
+ * Coerce an arbitrary value to a non-negative finite USD amount.
56
+ * @param {*} value
57
+ * @returns {number} the sanitized amount, or 0 when not a positive finite number
58
+ */
59
+ const toCostAmount = value => {
60
+ const n = typeof value === 'number' ? value : Number(value);
61
+ return Number.isFinite(n) && n > 0 ? n : 0;
62
+ };
63
+
64
+ /**
65
+ * Seed the accumulator from the carried-forward previous-run cost, exactly once
66
+ * per node process. Subsequent calls are no-ops so the in-process auto-merge
67
+ * loop does not reset the running total.
68
+ * @param {number|string|null|undefined} previousAnthropicCostUSD
69
+ * @returns {number} the cumulative total after seeding
70
+ */
71
+ export const seedCumulativeAnthropicCost = previousAnthropicCostUSD => {
72
+ if (seeded) return cumulativeAnthropicCostUSD;
73
+ cumulativeAnthropicCostUSD = toCostAmount(previousAnthropicCostUSD);
74
+ seeded = true;
75
+ return cumulativeAnthropicCostUSD;
76
+ };
77
+
78
+ /**
79
+ * Add a single Claude process's reported cost to the running total.
80
+ * Non-positive / non-finite inputs (e.g. a null cost when a run was interrupted
81
+ * by a limit before emitting a success result) contribute nothing.
82
+ * @param {number|string|null|undefined} runCostUSD
83
+ * @returns {number} the cumulative total after adding
84
+ */
85
+ export const addAnthropicRunCost = runCostUSD => {
86
+ cumulativeAnthropicCostUSD += toCostAmount(runCostUSD);
87
+ return cumulativeAnthropicCostUSD;
88
+ };
89
+
90
+ /**
91
+ * @returns {number} the cumulative Anthropic cost for the current logical session
92
+ */
93
+ export const getCumulativeAnthropicCost = () => cumulativeAnthropicCostUSD;
94
+
95
+ /**
96
+ * @returns {boolean} true once a positive cost has been seeded or accumulated
97
+ */
98
+ export const hasCumulativeAnthropicCost = () => cumulativeAnthropicCostUSD > 0;
99
+
100
+ /**
101
+ * Reset the accumulator. Intended for tests — production code seeds once and
102
+ * accumulates for the lifetime of the process.
103
+ */
104
+ export const resetCumulativeAnthropicCost = () => {
105
+ cumulativeAnthropicCostUSD = 0;
106
+ seeded = false;
107
+ };
@@ -138,19 +138,38 @@ export const displayModelUsage = async (usage, log) => {
138
138
  /**
139
139
  * Display cost comparison between public pricing and Anthropic's official cost
140
140
  * Issue #1557: Show simplified format when costs match, remove USD suffix
141
- * @param {number|null} publicCost - Public pricing estimate
142
- * @param {number|null} anthropicCost - Anthropic's official cost
141
+ * Issue #1886: `anthropicCost` is the cumulative Anthropic cost across every
142
+ * resume iteration (the session JSONL and therefore `publicCost` — spans
143
+ * the full session, so the Anthropic figure must too). The optional
144
+ * `previousAnthropicCost` is the portion carried in from earlier runs; when
145
+ * non-zero we show a verbose breakdown so the accumulation is auditable.
146
+ * @param {number|null} publicCost - Public pricing estimate (full session)
147
+ * @param {number|null} anthropicCost - Anthropic's cumulative official cost (full session)
143
148
  * @param {Function} log - Logging function
149
+ * @param {Object} [options]
150
+ * @param {number} [options.previousAnthropicCost=0] - cost carried in from earlier resume iterations
144
151
  */
145
- export const displayCostComparison = async (publicCost, anthropicCost, log) => {
152
+ export const displayCostComparison = async (publicCost, anthropicCost, log, options = {}) => {
153
+ const previousAnthropicCost = options.previousAnthropicCost || 0;
146
154
  const hasPublic = publicCost !== null && publicCost !== undefined;
147
155
  const hasAnthropic = anthropicCost !== null && anthropicCost !== undefined;
148
156
  const publicDec = hasPublic ? new Decimal(publicCost) : null;
149
157
  const anthropicDec = hasAnthropic ? new Decimal(anthropicCost) : null;
158
+ // Issue #1886: when the Anthropic figure was accumulated across resumes,
159
+ // expose the breakdown in verbose mode so "this run + carried forward = total"
160
+ // is auditable from the saved log.
161
+ const logAccumulationBreakdown = async () => {
162
+ if (previousAnthropicCost > 0 && anthropicDec) {
163
+ const thisRun = anthropicDec.minus(new Decimal(previousAnthropicCost));
164
+ await log(` ↳ Anthropic cost is cumulative across resume iterations (issue #1886):`, { verbose: true });
165
+ await log(` this run: $${thisRun.toFixed(6)} + carried forward: $${new Decimal(previousAnthropicCost).toFixed(6)} = $${anthropicDec.toFixed(6)}`, { verbose: true });
166
+ }
167
+ };
150
168
  // Issue #1703: also collapse to the short form when the rounded difference is below display precision,
151
169
  // so reports like "Difference: $-0.000000 (-0.00%)" no longer waste two extra lines.
152
170
  if (publicDec && anthropicDec && anthropicDec.minus(publicDec).abs().toFixed(6) === '0.000000') {
153
171
  await log(`\n 💰 Cost: $${anthropicDec.toFixed(6)}`);
172
+ await logAccumulationBreakdown();
154
173
  return;
155
174
  }
156
175
  await log('\n 💰 Cost estimation:');
@@ -163,6 +182,7 @@ export const displayCostComparison = async (publicCost, anthropicCost, log) => {
163
182
  } else {
164
183
  await log(' Difference: unknown');
165
184
  }
185
+ await logAccumulationBreakdown();
166
186
  };
167
187
 
168
188
  /**
@@ -316,11 +336,12 @@ export const displayBudgetStats = async (usage, tokenUsage, log) => {
316
336
  * @param {string} params.sessionId - Claude session id (skips when falsy)
317
337
  * @param {string} params.tempDir - Working directory containing the session JSONL (skips when falsy)
318
338
  * @param {Object|null} params.resultModelUsage - Authoritative per-model usage from the result JSON event
319
- * @param {number} params.anthropicTotalCostUSD - Anthropic's official total cost (for the comparison line)
339
+ * @param {number} params.anthropicTotalCostUSD - Anthropic's cumulative official cost across resume iterations (issue #1886)
340
+ * @param {number} [params.previousAnthropicCostUSD=0] - portion of anthropicTotalCostUSD carried in from earlier resume iterations (issue #1886)
320
341
  * @param {Object} params.argv - Parsed CLI args (reads argv.tokensBudgetStats)
321
342
  * @param {Function} params.log - Logger
322
343
  */
323
- export const displaySessionTokenUsage = async ({ sessionId, tempDir, resultModelUsage, anthropicTotalCostUSD, argv, log }) => {
344
+ export const displaySessionTokenUsage = async ({ sessionId, tempDir, resultModelUsage, anthropicTotalCostUSD, previousAnthropicCostUSD = 0, argv, log }) => {
324
345
  if (!sessionId || !tempDir) return;
325
346
  try {
326
347
  const tokenUsage = await calculateSessionTokens(sessionId, tempDir, resultModelUsage);
@@ -355,7 +376,9 @@ export const displaySessionTokenUsage = async ({ sessionId, tempDir, resultModel
355
376
  await log('\n 📈 Total across all models:');
356
377
  }
357
378
  // Show cost comparison (for both single and multiple models)
358
- await displayCostComparison(tokenUsage.totalCostUSD, anthropicTotalCostUSD, log);
379
+ // Issue #1886: anthropicTotalCostUSD is cumulative across resume iterations
380
+ // so it shares the full-session scope of the JSONL-based public estimate.
381
+ await displayCostComparison(tokenUsage.totalCostUSD, anthropicTotalCostUSD, log, { previousAnthropicCost: previousAnthropicCostUSD });
359
382
  // Show total tokens for single model only
360
383
  if (modelIds.length === 1) {
361
384
  await log(` Total tokens: ${formatNumber(tokenUsage.totalTokens)}`);
@@ -17,6 +17,7 @@ import { sanitizeObjectStrings } from './unicode-sanitization.lib.mjs';
17
17
  import Decimal from 'decimal.js-light';
18
18
  import { createEmptySubSessionUsage, accumulateModelUsage, mergeResultModelUsage, createSubAgentCallEntry, accumulateSubAgentUsage, getRawRequestInputTokens, displaySessionTokenUsage } from './claude.budget-stats.lib.mjs';
19
19
  import { buildClaudeResumeCommand, buildClaudeAutonomousResumeCommand } from './claude.command-builder.lib.mjs';
20
+ import { seedCumulativeAnthropicCost, addAnthropicRunCost } from './anthropic-cost-accumulator.lib.mjs'; // Issue #1886
20
21
  import { buildSolveResumeCommand } from './solve.resume-command.lib.mjs'; // Issue #942
21
22
  import { SESSION_FORCE_KILLED_MARKER, postTrackedComment } from './tool-comments.lib.mjs'; // Issue #1625
22
23
  import { handleClaudeRuntimeSwitch } from './claude.runtime-switch.lib.mjs'; // see issue #1141
@@ -662,6 +663,9 @@ export const executeClaudeCommand = async params => {
662
663
  let stderrErrors = [];
663
664
  let resultSuccessReceived = false;
664
665
  let anthropicTotalCostUSD = null;
666
+ // Issue #1886: a usage-limit hit ends as is_error (no success result). Keep
667
+ // the latest cost from ANY result event as a fallback for the failure path.
668
+ let anthropicCostFromAnyResult = null;
665
669
  let errorDuringExecution = false;
666
670
  let resultSummary = null;
667
671
  let resultModelUsage = null;
@@ -942,7 +946,9 @@ export const executeClaudeCommand = async params => {
942
946
  anthropicTotalCostUSD = data.total_cost_usd;
943
947
  await log(`💰 Anthropic official cost captured from success result: $${anthropicTotalCostUSD.toFixed(6)}`, { verbose: true });
944
948
  } else if (data.total_cost_usd !== undefined && data.total_cost_usd !== null) {
945
- await log(`💰 Anthropic cost from ${data.subtype || 'unknown'} result ignored: $${data.total_cost_usd.toFixed(6)}`, { verbose: true });
949
+ // Issue #1886: non-success terminal (e.g. usage-limit hit) still reports this process's cost keep as accumulation fallback.
950
+ anthropicCostFromAnyResult = data.total_cost_usd;
951
+ await log(`💰 Anthropic cost from ${data.subtype || 'unknown'} result kept as fallback for accumulation: $${data.total_cost_usd.toFixed(6)}`, { verbose: true });
946
952
  }
947
953
  // Issue #1263: Extract result summary (AI's summary of work done) for --attach-solution-summary
948
954
  if (data.subtype === 'success' && data.result && typeof data.result === 'string') {
@@ -1106,6 +1112,10 @@ export const executeClaudeCommand = async params => {
1106
1112
  await log(JSON.stringify(data, null, 2));
1107
1113
  if (data.type === 'result' && data.subtype === 'success' && data.total_cost_usd != null) {
1108
1114
  anthropicTotalCostUSD = data.total_cost_usd;
1115
+ } else if (data.type === 'result' && data.total_cost_usd != null) {
1116
+ // Issue #1886: keep a non-success terminal result's cost as a fallback
1117
+ // for accumulation (see the streaming branch above).
1118
+ anthropicCostFromAnyResult = data.total_cost_usd;
1109
1119
  }
1110
1120
  // Issue #1472: Forward remaining buffer event to interactive handler (was previously missed)
1111
1121
  if (interactiveHandler) {
@@ -1187,6 +1197,9 @@ export const executeClaudeCommand = async params => {
1187
1197
  await log(`\n\n❌ API explicitly marked error as not retryable (x-should-retry: false) and session made no progress (num_turns=${resultNumTurns}) after ${retryCount} attempt(s)`, { level: 'error' });
1188
1198
  await log(` This error is not recoverable. Failing fast to avoid a stuck retry loop (Issue #1437).`, { level: 'error' });
1189
1199
  await log(` Check https://status.anthropic.com/ for API status.`, { level: 'error' });
1200
+ // Issue #1886: fold captured cost so a cross-process resume's carried-forward cost is not dropped here.
1201
+ seedCumulativeAnthropicCost(argv.previousAnthropicCost);
1202
+ const cumulativeAnthropicCostUSDOnStuckRetry = addAnthropicRunCost(anthropicTotalCostUSD ?? anthropicCostFromAnyResult);
1190
1203
  return {
1191
1204
  success: false,
1192
1205
  sessionId,
@@ -1196,7 +1209,7 @@ export const executeClaudeCommand = async params => {
1196
1209
  messageCount,
1197
1210
  toolUseCount,
1198
1211
  is503Error,
1199
- anthropicTotalCostUSD,
1212
+ anthropicTotalCostUSD: cumulativeAnthropicCostUSDOnStuckRetry, // Issue #1104/#1886
1200
1213
  resultSummary,
1201
1214
  // Issue #1845: surface the actual error so callers can show it to users
1202
1215
  errorInfo: { message: lastMessage || 'API explicitly marked error as not retryable', exitCode },
@@ -1233,6 +1246,9 @@ export const executeClaudeCommand = async params => {
1233
1246
  return await executeWithRetry();
1234
1247
  } else {
1235
1248
  await log(`\n\n❌ Transient API error persisted after ${maxRetries} retries\n Please try again later or check https://status.anthropic.com/`, { level: 'error' });
1249
+ // Issue #1886: fold captured cost so the carried-forward cost survives this retries-exhausted path.
1250
+ seedCumulativeAnthropicCost(argv.previousAnthropicCost);
1251
+ const cumulativeAnthropicCostUSDOnRetriesExhausted = addAnthropicRunCost(anthropicTotalCostUSD ?? anthropicCostFromAnyResult);
1236
1252
  return {
1237
1253
  success: false,
1238
1254
  sessionId,
@@ -1242,7 +1258,7 @@ export const executeClaudeCommand = async params => {
1242
1258
  messageCount,
1243
1259
  toolUseCount,
1244
1260
  is503Error, // preserve for callers that check this
1245
- anthropicTotalCostUSD, // Issue #1104: Include cost even on failure
1261
+ anthropicTotalCostUSD: cumulativeAnthropicCostUSDOnRetriesExhausted, // Issue #1104/#1886: Include cumulative cost even on failure
1246
1262
  resultSummary, // Issue #1263: Include result summary
1247
1263
  // Issue #1845: surface the actual error so callers can show it to users
1248
1264
  errorInfo: { message: lastMessage || `Transient API error persisted after ${maxRetries} retries`, exitCode },
@@ -1294,6 +1310,12 @@ export const executeClaudeCommand = async params => {
1294
1310
  await log(` Memory: ${resourcesAfter.memory.split('\n')[1]}`, { verbose: true });
1295
1311
  await log(` Load: ${resourcesAfter.load}`, { verbose: true });
1296
1312
  await showResumeCommand(sessionId, tempDir, claudePath, argv.model, log, argv);
1313
+ // Issue #1886: on failure (usually a usage-limit hit → auto-resume) fold
1314
+ // the captured cost into the cumulative total so autoContinueWhenLimitResets
1315
+ // carries it forward. A limit hit ends as is_error → fall back to the
1316
+ // non-success result cost.
1317
+ seedCumulativeAnthropicCost(argv.previousAnthropicCost);
1318
+ const cumulativeAnthropicCostUSDOnFailure = addAnthropicRunCost(anthropicTotalCostUSD ?? anthropicCostFromAnyResult);
1297
1319
  return {
1298
1320
  success: false,
1299
1321
  sessionId,
@@ -1303,7 +1325,7 @@ export const executeClaudeCommand = async params => {
1303
1325
  messageCount,
1304
1326
  toolUseCount,
1305
1327
  errorDuringExecution,
1306
- anthropicTotalCostUSD, // Issue #1104: Include cost even on failure
1328
+ anthropicTotalCostUSD: cumulativeAnthropicCostUSDOnFailure, // Issue #1104/#1886: cumulative cost even on failure
1307
1329
  resultSummary, // Issue #1263: Include result summary
1308
1330
  // Issue #1845: surface the core error (e.g. "API Error: Output blocked by content
1309
1331
  // filtering policy") so users see what actually went wrong, not just a generic message.
@@ -1322,7 +1344,13 @@ export const executeClaudeCommand = async params => {
1322
1344
  await log(`📊 Total messages: ${messageCount}, Tool uses: ${toolUseCount}`);
1323
1345
  // Calculate and display total token usage from session JSONL file.
1324
1346
  // Extracted to claude.budget-stats.lib.mjs to keep this file under the line limit (Issue #1834).
1325
- await displaySessionTokenUsage({ sessionId, tempDir, resultModelUsage, anthropicTotalCostUSD, argv, log });
1347
+ // Issue #1886: the JSONL spans every resume iteration but each result
1348
+ // event's total_cost_usd covers only this process; seed the carried-forward
1349
+ // cost + add this process's so the cumulative total shares the JSONL scope.
1350
+ seedCumulativeAnthropicCost(argv.previousAnthropicCost);
1351
+ const cumulativeAnthropicCostUSD = addAnthropicRunCost(anthropicTotalCostUSD);
1352
+ const previousAnthropicCostUSD = cumulativeAnthropicCostUSD - (anthropicTotalCostUSD || 0);
1353
+ await displaySessionTokenUsage({ sessionId, tempDir, resultModelUsage, anthropicTotalCostUSD: cumulativeAnthropicCostUSD, previousAnthropicCostUSD, argv, log });
1326
1354
  await showResumeCommand(sessionId, tempDir, claudePath, argv.model, log, argv);
1327
1355
  return {
1328
1356
  success: true,
@@ -1332,7 +1360,7 @@ export const executeClaudeCommand = async params => {
1332
1360
  limitTimezone,
1333
1361
  messageCount,
1334
1362
  toolUseCount,
1335
- anthropicTotalCostUSD, // Pass Anthropic's official total cost
1363
+ anthropicTotalCostUSD: cumulativeAnthropicCostUSD, // Issue #1104/#1886: cumulative Anthropic cost across resume iterations
1336
1364
  errorDuringExecution, // Issue #1088: Track if error_during_execution subtype occurred
1337
1365
  resultSummary, // Issue #1263: Include result summary for --attach-solution-summary
1338
1366
  resultModelUsage, // Issue #1454
@@ -1378,6 +1406,9 @@ export const executeClaudeCommand = async params => {
1378
1406
  }
1379
1407
  }
1380
1408
  await log(`\n\n❌ Error executing Claude command: ${error.message}`, { level: 'error' });
1409
+ // Issue #1886: fold captured cost so the carried-forward cost survives this exception path too.
1410
+ seedCumulativeAnthropicCost(argv.previousAnthropicCost);
1411
+ const cumulativeAnthropicCostUSDOnException = addAnthropicRunCost(anthropicTotalCostUSD ?? anthropicCostFromAnyResult);
1381
1412
  return {
1382
1413
  success: false,
1383
1414
  sessionId,
@@ -1386,7 +1417,7 @@ export const executeClaudeCommand = async params => {
1386
1417
  limitTimezone: null,
1387
1418
  messageCount,
1388
1419
  toolUseCount,
1389
- anthropicTotalCostUSD, // Issue #1104: Include cost even on failure
1420
+ anthropicTotalCostUSD: cumulativeAnthropicCostUSDOnException, // Issue #1104/#1886: Include cumulative cost even on failure
1390
1421
  resultSummary, // Issue #1263: Include result summary
1391
1422
  // Issue #1845: surface the actual exception message so callers can show it to users
1392
1423
  errorInfo: { message: error.message || error.toString() },
@@ -54,6 +54,9 @@ import { formatAutoIterationLimit, hasReachedAutoIterationLimit, normalizeAutoIt
54
54
  // Issue #1574: Interruptible sleep so CTRL+C is never blocked by a lingering timer
55
55
  const { interruptibleSleep } = await import('./interruptible-sleep.lib.mjs');
56
56
 
57
+ // Issue #1886: cumulative Anthropic cost carried across cross-process resumes
58
+ const { getCumulativeAnthropicCost } = await import('./anthropic-cost-accumulator.lib.mjs');
59
+
57
60
  const { calculateWaitTime } = validation;
58
61
 
59
62
  /**
@@ -168,6 +171,20 @@ export const autoContinueWhenLimitResets = async (issueUrl, sessionId, argv, sho
168
171
  resumeArgs.push('--auto-resume-iteration', String(nextAutoResumeIteration));
169
172
  resumeArgs.push('--auto-resume-max-iterations', String(maxAutoResumeIterations));
170
173
 
174
+ // Issue #1886: carry the cumulative Anthropic cost into the resumed process.
175
+ // The resumed run reads the same session JSONL (full session, all runs), so
176
+ // its public-pricing estimate spans every run; without this the resumed
177
+ // run's "Calculated by Anthropic" figure would cover only its own process
178
+ // and disagree with the full-session public estimate (the -31.66% gap in
179
+ // issue #1886). getCumulativeAnthropicCost() already includes this run's
180
+ // cost folded in at the runClaude return, plus anything carried from prior
181
+ // iterations via --previous-anthropic-cost.
182
+ const carriedAnthropicCost = getCumulativeAnthropicCost();
183
+ if (carriedAnthropicCost > 0) {
184
+ resumeArgs.push('--previous-anthropic-cost', String(carriedAnthropicCost));
185
+ await log(`💰 Carrying forward cumulative Anthropic cost: $${carriedAnthropicCost.toFixed(6)} (issue #1886)`, { verbose: true });
186
+ }
187
+
171
188
  // Pass session type for proper comment differentiation
172
189
  // See: https://github.com/link-assistant/hive-mind/issues/1152
173
190
  const sessionType = isRestart ? 'auto-restart' : 'auto-resume';
@@ -218,6 +218,19 @@ export const SOLVE_OPTION_DEFINITIONS = {
218
218
  default: 0,
219
219
  hidden: true,
220
220
  },
221
+ // Issue #1886: carried-forward Anthropic cost from previous resume iterations.
222
+ // The session JSONL accumulates the full session across limit-reset resumes,
223
+ // but Anthropic's result-event total_cost_usd is scoped to a single process.
224
+ // Threading the previous total here lets the resumed run display the
225
+ // full-session Anthropic cost alongside the full-session public estimate,
226
+ // instead of a misleading per-run figure. Internal/hidden: set automatically
227
+ // by autoContinueWhenLimitResets when spawning the resumed solve process.
228
+ 'previous-anthropic-cost': {
229
+ type: 'number',
230
+ description: 'Internal: cumulative Anthropic total_cost_usd carried forward from previous resume iterations (issue #1886)',
231
+ default: 0,
232
+ hidden: true,
233
+ },
221
234
  'auto-merge': {
222
235
  type: 'boolean',
223
236
  description: 'Automatically merge the pull request when the working session is finished and all CI/CD statuses pass and PR is mergeable. Implies --auto-restart-until-mergeable.',