@leanandmean/coding-agent 0.74.1-scramjet.67 → 0.74.1-scramjet.69

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.
@@ -17,14 +17,13 @@ import { basename, dirname, resolve } from "node:path";
17
17
  // SCRAMJET-DIVERGENCE: provider-safe harness tool-call id generation for the persisted-settlement
18
18
  // acknowledgement key (#341).
19
19
  import { generateHarnessToolCallId } from "@leanandmean/agent";
20
- import { clampThinkingLevel, cleanupSessionResources, flattenSystemPrompt, getSupportedThinkingLevels, isContextOverflow, modelsAreEqual, resetApiProviders, } from "@leanandmean/ai";
21
- import { validateResponsesProviderFailure } from "@leanandmean/ai/openai-responses";
20
+ import { blocksFailureRecovery, clampThinkingLevel, cleanupSessionResources, flattenSystemPrompt, getSupportedThinkingLevels, inspectFailureEvidence, isContextOverflow, modelsAreEqual, resetApiProviders, } from "@leanandmean/ai";
22
21
  import { theme } from "../modes/interactive/theme/theme.js";
23
22
  import { stripFrontmatter } from "../utils/frontmatter.js";
24
23
  import { sleep } from "../utils/sleep.js";
25
24
  import { formatNoApiKeyFoundMessage, formatNoModelSelectedMessage } from "./auth-guidance.js";
26
25
  import { executeBashWithOperations } from "./bash-executor.js";
27
- import { calculateContextTokens, collectEntriesForBranchSummary, compact, estimateContextTokens, generateBranchSummary, prepareCompaction, shouldCompact, } from "./compaction/index.js";
26
+ import { calculateContextTokens, collectEntriesForBranchSummary, compact, estimateContextTokens, estimateTokens, generateBranchSummary, prepareCompaction, shouldCompact, } from "./compaction/index.js";
28
27
  import { DEFAULT_THINKING_LEVEL } from "./defaults.js";
29
28
  import { exportSessionToHtml } from "./export-html/index.js";
30
29
  import { createToolHtmlRenderer } from "./export-html/tool-renderer.js";
@@ -87,6 +86,9 @@ function rejectDeferred(deferred, error) {
87
86
  function settleDeferred(deferred, error) {
88
87
  return error ? rejectDeferred(deferred, error) : resolveDeferred(deferred, undefined);
89
88
  }
89
+ function validRecordedCost(value) {
90
+ return typeof value === "number" && Number.isFinite(value) && value >= 0;
91
+ }
90
92
  export class AgentSession {
91
93
  agent;
92
94
  sessionManager;
@@ -97,6 +99,10 @@ export class AgentSession {
97
99
  _unsubscribeAgent;
98
100
  _eventListeners = [];
99
101
  _agentEventQueue = Promise.resolve();
102
+ // SCRAMJET-DIVERGENCE: execution ownership survives final-event queue latency until append succeeds (#598).
103
+ _toolCostExecutions = new Map();
104
+ _pendingToolCosts = new Set();
105
+ _historicalCostDefinitions = new Map();
100
106
  _outputThroughputTracker = new OutputThroughputTracker();
101
107
  _outputThroughputHistory;
102
108
  _outputThroughputGeneration;
@@ -115,6 +121,7 @@ export class AgentSession {
115
121
  _inputSessionEntries = new WeakMap();
116
122
  // Compaction state
117
123
  _compactionAbortController = undefined;
124
+ _manualCompactionDrain;
118
125
  _autoCompactionAbortController = undefined;
119
126
  _overflowRecoveryAttempted = false;
120
127
  // Branch summarization state
@@ -299,10 +306,13 @@ export class AgentSession {
299
306
  }
300
307
  /** Internal handler for agent events - shared by subscribe and reconnect */
301
308
  _handleAgentEvent = (event) => {
309
+ if (this._disposed)
310
+ return;
302
311
  this._captureOutputThroughput(event);
312
+ const toolCost = this._captureToolCost(event);
303
313
  const runSettlement = this._captureAgentRunSettlement(event);
304
314
  const harnessAck = this._harnessPersistenceAckForEvent(event);
305
- const processing = this._agentEventQueue.then(() => this._processAgentEventTracked(event, runSettlement, harnessAck), () => this._processAgentEventTracked(event, runSettlement, harnessAck));
315
+ const processing = this._agentEventQueue.then(() => this._processAgentEventTracked(event, runSettlement, harnessAck, toolCost), () => this._processAgentEventTracked(event, runSettlement, harnessAck, toolCost));
306
316
  this._agentEventQueue = processing;
307
317
  if (harnessAck)
308
318
  harnessAck.persistenceTail = processing;
@@ -313,6 +323,42 @@ export class AgentSession {
313
323
  if (event.type === "turn_end")
314
324
  return this._agentEventQueue;
315
325
  };
326
+ // SCRAMJET-DIVERGENCE: scalar reports are captured before mutable events enter async hooks (#598).
327
+ _captureToolCost(event) {
328
+ if (event.type === "tool_execution_start") {
329
+ const execution = {};
330
+ this._toolCostExecutions.set(event.toolCallId, execution);
331
+ this._pendingToolCosts.add(execution);
332
+ return;
333
+ }
334
+ let toolCallId;
335
+ let reported;
336
+ const final = event.type === "message_end" && event.message.role === "toolResult";
337
+ if (event.type === "tool_execution_update" || event.type === "tool_execution_end") {
338
+ toolCallId = event.toolCallId;
339
+ reported = event.type === "tool_execution_update" ? event.partialResult?.cost : event.result?.cost;
340
+ }
341
+ else if (event.type === "message_end" && event.message.role === "toolResult") {
342
+ toolCallId = event.message.toolCallId;
343
+ reported = event.message.cost;
344
+ }
345
+ else {
346
+ return;
347
+ }
348
+ let execution = this._toolCostExecutions.get(toolCallId);
349
+ if (!execution && validRecordedCost(reported)) {
350
+ execution = {};
351
+ this._toolCostExecutions.set(toolCallId, execution);
352
+ this._pendingToolCosts.add(execution);
353
+ }
354
+ if (execution && validRecordedCost(reported))
355
+ execution.cost = reported;
356
+ if (final) {
357
+ this._toolCostExecutions.delete(toolCallId);
358
+ return execution;
359
+ }
360
+ return;
361
+ }
316
362
  // SCRAMJET-DIVERGENCE: Capture provider events synchronously and reset stale throughput lifecycles (#476).
317
363
  _captureOutputThroughput(event) {
318
364
  if (event.type === "message_start" && event.message.role === "assistant" && event.message.origin === "provider") {
@@ -391,16 +437,18 @@ export class AgentSession {
391
437
  }
392
438
  // SCRAMJET-DIVERGENCE: harness-tool persisted settlement (#341). Synchronous ownership keeps queued
393
439
  // invocation events attributed after early failures; public settlement awaits execution and the final persistence tail.
394
- async _processAgentEventTracked(event, run, harnessAck) {
440
+ async _processAgentEventTracked(event, run, harnessAck, toolCost) {
395
441
  try {
396
- await this._processAgentEvent(event, run, harnessAck !== undefined);
442
+ await this._processAgentEvent(event, run, harnessAck !== undefined, toolCost);
397
443
  }
398
444
  catch (err) {
399
445
  const error = err instanceof Error ? err : new Error(String(err));
446
+ if (this._manualCompactionDrain)
447
+ this._manualCompactionDrain.firstError ??= error;
400
448
  if (harnessAck) {
401
449
  harnessAck.firstPersistenceError ??= error;
402
450
  }
403
- else if (run) {
451
+ else if (run && !this._manualCompactionDrain) {
404
452
  if (event.type === "message_end" && event.message.role === "assistant") {
405
453
  run.assistantPersistenceError = error;
406
454
  }
@@ -437,7 +485,19 @@ export class AgentSession {
437
485
  }
438
486
  return toolCallId === undefined ? undefined : this._harnessPersistenceAcks.get(toolCallId);
439
487
  }
440
- async _processAgentEvent(event, run, fromHarnessInvocation = false) {
488
+ async _processAgentEvent(event, run, fromHarnessInvocation = false, toolCost) {
489
+ if (this._disposed)
490
+ return;
491
+ // SCRAMJET-DIVERGENCE: queued lifecycle events must respect manual compaction ownership (#598).
492
+ if (this._manualCompactionDrain && event.type !== "message_end" && event.type !== "agent_end")
493
+ return;
494
+ if (this._manualCompactionDrain && event.type === "agent_end") {
495
+ if (this._retry)
496
+ this._finishCancelledRetry("cancelled_during_continuation");
497
+ if (run)
498
+ this._completeAgentRun(run, this._manualCompactionDrain.firstError);
499
+ return;
500
+ }
441
501
  // When a user message starts, check if it's from either queue and remove it BEFORE emitting
442
502
  // This ensures the UI sees the updated queue state
443
503
  if (event.type === "message_start" && event.message.role === "user") {
@@ -462,6 +522,18 @@ export class AgentSession {
462
522
  }
463
523
  // Emit to extensions first
464
524
  await this._emitExtensionEvent(event);
525
+ if (event.type === "message_end" && event.message.role === "toolResult" && toolCost?.cost !== undefined) {
526
+ event.message.cost = toolCost.cost;
527
+ }
528
+ // An awaited hook may have acquired manual ownership while this event was in flight.
529
+ if (this._manualCompactionDrain && event.type !== "message_end") {
530
+ if (event.type === "agent_end" && run) {
531
+ if (this._retry)
532
+ this._finishCancelledRetry("cancelled_during_continuation");
533
+ this._completeAgentRun(run, this._manualCompactionDrain.firstError);
534
+ }
535
+ return;
536
+ }
465
537
  // Notify all listeners
466
538
  this._emit(event);
467
539
  if (this._disposed)
@@ -482,6 +554,8 @@ export class AgentSession {
482
554
  ? JSON.parse(JSON.stringify(event.message))
483
555
  : event.message;
484
556
  const messageEntryId = this.sessionManager.appendMessage(persistedMessage);
557
+ if (toolCost)
558
+ this._pendingToolCosts.delete(toolCost);
485
559
  if (persistedMessage.role === "assistant" && !fromHarnessInvocation && run) {
486
560
  run.persistedAssistantSnapshot = persistedMessage;
487
561
  }
@@ -514,7 +588,8 @@ export class AgentSession {
514
588
  if (event.message.role === "assistant" &&
515
589
  event.message.stopReason !== "error" &&
516
590
  event.message.stopReason !== "aborted" &&
517
- !fromHarnessInvocation) {
591
+ !fromHarnessInvocation &&
592
+ !this._manualCompactionDrain) {
518
593
  this._overflowRecoveryAttempted = false;
519
594
  if (this._retry)
520
595
  this._retry = { ...this._retry, attempt: 0 };
@@ -523,6 +598,12 @@ export class AgentSession {
523
598
  if (event.type === "agent_end") {
524
599
  if (this._disposed || !run)
525
600
  return;
601
+ if (this._manualCompactionDrain) {
602
+ if (this._retry)
603
+ this._finishCancelledRetry("cancelled_during_continuation");
604
+ this._completeAgentRun(run, this._manualCompactionDrain.firstError);
605
+ return;
606
+ }
526
607
  if (run.assistantPersistenceError) {
527
608
  const error = run.assistantPersistenceError;
528
609
  run.assistantPersistenceError = undefined;
@@ -741,8 +822,8 @@ export class AgentSession {
741
822
  * Preserves all existing listeners.
742
823
  */
743
824
  _reconnectToAgent() {
744
- if (this._unsubscribeAgent)
745
- return; // Already connected
825
+ if (this._disposed || this._unsubscribeAgent)
826
+ return; // Already connected or disposed
746
827
  this._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent);
747
828
  }
748
829
  /**
@@ -755,6 +836,8 @@ export class AgentSession {
755
836
  if (this._disposed)
756
837
  return;
757
838
  this._disposed = true;
839
+ this._toolCostExecutions.clear();
840
+ this._pendingToolCosts.clear();
758
841
  this._resetOutputThroughput();
759
842
  const retryDisposeError = new Error("AgentSession disposed before retry settlement completed.");
760
843
  const unsettledRuns = [...this._unsettledAgentRuns];
@@ -966,6 +1049,10 @@ export class AgentSession {
966
1049
  if (this._disposed) {
967
1050
  throw new Error(`Cannot invoke harness tool "${name}": the session has been disposed.`);
968
1051
  }
1052
+ // SCRAMJET-DIVERGENCE: reject new session work before allocating invocation ownership (#598).
1053
+ if (this._compactionAbortController) {
1054
+ throw new Error(`Cannot invoke harness tool "${name}": manual compaction is in progress. Retry after it settles.`);
1055
+ }
969
1056
  const tool = this._toolRegistry.get(name);
970
1057
  if (!tool) {
971
1058
  throw new Error(`Cannot invoke harness tool "${name}": no tool with that name is registered.`);
@@ -1231,7 +1318,7 @@ export class AgentSession {
1231
1318
  // Check if we need to compact before sending (catches aborted responses)
1232
1319
  const lastAssistant = this._findLastAssistantMessage();
1233
1320
  if (lastAssistant) {
1234
- await this._checkCompaction(lastAssistant, false);
1321
+ await this._checkCompaction(lastAssistant, "new_prompt");
1235
1322
  }
1236
1323
  // Build messages array (custom message if any, then user message)
1237
1324
  messages = [];
@@ -1759,12 +1846,32 @@ export class AgentSession {
1759
1846
  * @param customInstructions Optional instructions for the compaction summary
1760
1847
  */
1761
1848
  async compact(customInstructions) {
1762
- this._resetOutputThroughput();
1763
- this._disconnectFromAgent();
1764
- await this.abort();
1765
- this._compactionAbortController = new AbortController();
1766
- this._emit({ type: "compaction_start", reason: "manual" });
1849
+ // SCRAMJET-DIVERGENCE: acquire one owner before awaits and drain terminal persistence (#598).
1850
+ if (this._disposed)
1851
+ throw new Error("Cannot compact a disposed session.");
1852
+ if (this.isCompacting)
1853
+ throw new Error("Compaction or branch summarization is already in progress. Retry after it settles.");
1854
+ if (this.agent.hasActiveTransientHarnessTools) {
1855
+ throw new Error("A harness tool is still executing outside an Agent run. Retry compaction after its persisted settlement.");
1856
+ }
1857
+ const controller = new AbortController();
1858
+ this._compactionAbortController = controller;
1859
+ const drain = {};
1860
+ this._manualCompactionDrain = drain;
1861
+ const runs = [...this._unsettledAgentRuns];
1767
1862
  try {
1863
+ this._resetOutputThroughput();
1864
+ this._disconnectFromAgent();
1865
+ this._unsubscribeAgent = this.agent.subscribe((event) => this._handleAgentEvent(event));
1866
+ this._emit({ type: "compaction_start", reason: "manual" });
1867
+ await this.abort();
1868
+ await this._agentEventQueue;
1869
+ if (drain.firstError)
1870
+ throw drain.firstError;
1871
+ if (this._disposed)
1872
+ throw new Error("Session disposed during compaction.");
1873
+ if (controller.signal.aborted)
1874
+ throw new Error("Compaction cancelled");
1768
1875
  if (!this.model) {
1769
1876
  throw new Error(formatNoModelSelectedMessage());
1770
1877
  }
@@ -1788,7 +1895,7 @@ export class AgentSession {
1788
1895
  preparation,
1789
1896
  branchEntries: pathEntries,
1790
1897
  customInstructions,
1791
- signal: this._compactionAbortController.signal,
1898
+ signal: controller.signal,
1792
1899
  }));
1793
1900
  if (result?.cancel) {
1794
1901
  throw new Error("Compaction cancelled");
@@ -1811,13 +1918,15 @@ export class AgentSession {
1811
1918
  }
1812
1919
  else {
1813
1920
  // Generate compaction result
1814
- const result = await compact(preparation, this.model, apiKey, headers, customInstructions, this._compactionAbortController.signal, this.thinkingLevel);
1921
+ const result = await compact(preparation, this.model, apiKey, headers, customInstructions, controller.signal, this.thinkingLevel);
1815
1922
  summary = result.summary;
1816
1923
  firstKeptEntryId = result.firstKeptEntryId;
1817
1924
  tokensBefore = result.tokensBefore;
1818
1925
  details = result.details;
1819
1926
  }
1820
- if (this._compactionAbortController.signal.aborted) {
1927
+ if (this._disposed)
1928
+ throw new Error("Session disposed during compaction.");
1929
+ if (controller.signal.aborted) {
1821
1930
  throw new Error("Compaction cancelled");
1822
1931
  }
1823
1932
  this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromExtension);
@@ -1849,8 +1958,13 @@ export class AgentSession {
1849
1958
  return compactionResult;
1850
1959
  }
1851
1960
  catch (error) {
1852
- const message = error instanceof Error ? error.message : String(error);
1853
- const aborted = message === "Compaction cancelled" || (error instanceof Error && error.name === "AbortError");
1961
+ const failure = drain.firstError ?? error;
1962
+ const settlementError = failure instanceof Error ? failure : new Error(String(failure));
1963
+ this._releaseRetryForFailedRun(settlementError);
1964
+ for (const run of runs)
1965
+ this._completeAgentRun(run, settlementError);
1966
+ const message = settlementError.message;
1967
+ const aborted = message === "Compaction cancelled" || settlementError.name === "AbortError";
1854
1968
  this._emit({
1855
1969
  type: "compaction_end",
1856
1970
  reason: "manual",
@@ -1859,9 +1973,11 @@ export class AgentSession {
1859
1973
  willRetry: false,
1860
1974
  errorMessage: aborted ? undefined : `Compaction failed: ${message}`,
1861
1975
  });
1862
- throw error;
1976
+ throw failure;
1863
1977
  }
1864
1978
  finally {
1979
+ this._disconnectFromAgent();
1980
+ this._manualCompactionDrain = undefined;
1865
1981
  this._compactionAbortController = undefined;
1866
1982
  this._reconnectToAgent();
1867
1983
  }
@@ -1888,15 +2004,43 @@ export class AgentSession {
1888
2004
  * 2. Threshold: Context over threshold, compact, NO auto-retry (user continues manually)
1889
2005
  *
1890
2006
  * @param assistantMessage The assistant message to check
1891
- * @param skipAbortedCheck If false, include aborted messages (for pre-prompt check). Default: true
1892
2007
  */
1893
- async _checkCompaction(assistantMessage, skipAbortedCheck = true) {
2008
+ async _checkCompaction(assistantMessage, trigger = "settlement") {
2009
+ if (this._manualCompactionDrain)
2010
+ return;
1894
2011
  const settings = this.settingsManager.getCompactionSettings();
1895
2012
  if (!settings.enabled)
1896
2013
  return;
1897
- // Skip if message was aborted (user cancelled) - unless skipAbortedCheck is false
1898
- if (skipAbortedCheck && assistantMessage.stopReason === "aborted")
2014
+ // SCRAMJET-DIVERGENCE: New prompts maintain context independently of failed-turn recovery.
2015
+ if (trigger === "settlement" && blocksFailureRecovery(assistantMessage))
1899
2016
  return;
2017
+ if (trigger === "new_prompt") {
2018
+ const boundary = getLatestCompactionEntry(this.sessionManager.getBranch());
2019
+ const messages = this.agent.state.messages;
2020
+ const reverseIndex = [...messages]
2021
+ .reverse()
2022
+ .findIndex((message) => message.role === "assistant" &&
2023
+ message.origin !== "harness" &&
2024
+ message.stopReason !== "error" &&
2025
+ message.stopReason !== "aborted" &&
2026
+ message.provider === this.model?.provider &&
2027
+ message.model === this.model?.id &&
2028
+ Number.isFinite(calculateContextTokens(message.usage)) &&
2029
+ calculateContextTokens(message.usage) >= 0 &&
2030
+ (!boundary || message.timestamp > new Date(boundary.timestamp).getTime()) &&
2031
+ [message.usage.input, message.usage.output, message.usage.cacheRead, message.usage.cacheWrite].every((n) => Number.isFinite(n) && n >= 0));
2032
+ if (reverseIndex < 0)
2033
+ return;
2034
+ const sourceIndex = messages.length - reverseIndex - 1;
2035
+ const source = messages[sourceIndex];
2036
+ const tokens = calculateContextTokens(source.usage) +
2037
+ messages.slice(sourceIndex + 1).reduce((total, message) => total + estimateTokens(message), 0);
2038
+ const window = this.model?.contextWindow ?? 0;
2039
+ if (shouldCompact(tokens, window, settings) ||
2040
+ (window > settings.reserveTokens && tokens > (this.model?.maxInputTokens ?? Infinity)))
2041
+ await this._runAutoCompaction("threshold", false);
2042
+ return;
2043
+ }
1900
2044
  // SCRAMJET-DIVERGENCE: Total context and independent input limits are separate constraints.
1901
2045
  const contextWindow = this.model?.contextWindow ?? 0;
1902
2046
  // Skip overflow check if the message came from a different model.
@@ -1969,9 +2113,10 @@ export class AgentSession {
1969
2113
  */
1970
2114
  async _runAutoCompaction(reason, willRetry) {
1971
2115
  const settings = this.settingsManager.getCompactionSettings();
1972
- this._emit({ type: "compaction_start", reason });
2116
+ // SCRAMJET-DIVERGENCE: reentrant subscribers must observe compaction ownership (#598).
1973
2117
  this._autoCompactionAbortController = new AbortController();
1974
2118
  try {
2119
+ this._emit({ type: "compaction_start", reason });
1975
2120
  if (!this.model) {
1976
2121
  this._emit({
1977
2122
  type: "compaction_end",
@@ -2316,7 +2461,13 @@ export class AgentSession {
2316
2461
  definition,
2317
2462
  sourceInfo: createSyntheticSourceInfo(`<sdk:${definition.name}>`, { source: "sdk" }),
2318
2463
  })),
2319
- ].filter((tool) => isAllowedTool(tool.definition.name));
2464
+ ];
2465
+ // SCRAMJET-DIVERGENCE: historical interpretation follows definition collision winners, not execution allowlists (#598).
2466
+ this._historicalCostDefinitions = new Map(this._baseToolDefinitions);
2467
+ for (const tool of allCustomTools) {
2468
+ this._historicalCostDefinitions.set(tool.definition.name, tool.definition);
2469
+ }
2470
+ const allowedCustomTools = allCustomTools.filter((tool) => isAllowedTool(tool.definition.name));
2320
2471
  const definitionRegistry = new Map(Array.from(this._baseToolDefinitions.entries())
2321
2472
  .filter(([name]) => isAllowedTool(name))
2322
2473
  .map(([name, definition]) => [
@@ -2326,7 +2477,7 @@ export class AgentSession {
2326
2477
  sourceInfo: createSyntheticSourceInfo(`<builtin:${name}>`, { source: "builtin" }),
2327
2478
  },
2328
2479
  ]));
2329
- for (const tool of allCustomTools) {
2480
+ for (const tool of allowedCustomTools) {
2330
2481
  definitionRegistry.set(tool.definition.name, {
2331
2482
  definition: tool.definition,
2332
2483
  sourceInfo: tool.sourceInfo,
@@ -2346,7 +2497,7 @@ export class AgentSession {
2346
2497
  })
2347
2498
  .filter((entry) => entry !== undefined));
2348
2499
  const runner = this._extensionRunner;
2349
- const wrappedExtensionTools = wrapRegisteredTools(allCustomTools, runner);
2500
+ const wrappedExtensionTools = wrapRegisteredTools(allowedCustomTools, runner);
2350
2501
  const wrappedBuiltInTools = wrapRegisteredTools(Array.from(this._baseToolDefinitions.values())
2351
2502
  .filter((definition) => isAllowedTool(definition.name))
2352
2503
  .map((definition) => ({
@@ -2451,20 +2602,30 @@ export class AgentSession {
2451
2602
  // =========================================================================
2452
2603
  // SCRAMJET-DIVERGENCE: persisted post-extension snapshots authoritatively classify and settle retries (#553).
2453
2604
  _classifyRetry(message) {
2605
+ if ((message.origin === "harness" || message.stopReason === "aborted") && blocksFailureRecovery(message))
2606
+ return { kind: "do_not_retry", reason: "non_provider_failure", evidence: "none" };
2454
2607
  const contextWindow = this.model?.contextWindow ?? 0;
2455
2608
  if (isContextOverflow(message, Math.min(contextWindow, this.model?.maxInputTokens ?? Infinity))) {
2456
2609
  return { kind: "context_overflow" };
2457
2610
  }
2458
- const structured = validateResponsesProviderFailure(message.diagnostics);
2611
+ const structured = inspectFailureEvidence(message.diagnostics);
2459
2612
  if (structured.status === "valid") {
2460
- if (structured.retryDisposition === "transient")
2461
- return { kind: "retry", evidence: "provider_failure" };
2613
+ if (structured.source !== "provider")
2614
+ return { kind: "do_not_retry", reason: "non_provider_failure", evidence: "none" };
2615
+ if (structured.suppression)
2616
+ return { kind: "do_not_retry", reason: structured.suppression, evidence: structured.family };
2617
+ if (structured.transient || ["missing_terminal_event", "missing_body"].includes(structured.category))
2618
+ return { kind: "retry", evidence: structured.family };
2462
2619
  return {
2463
2620
  kind: "do_not_retry",
2464
- reason: structured.retryDisposition === "non_transient" ? "structured_non_transient" : "structured_unknown",
2465
- evidence: "provider_failure",
2621
+ reason: ["unknown", "provider_error", "malformed_event"].includes(structured.category)
2622
+ ? "structured_unknown"
2623
+ : "structured_non_transient",
2624
+ evidence: structured.family,
2466
2625
  };
2467
2626
  }
2627
+ if (structured.status === "conflicting")
2628
+ return { kind: "do_not_retry", reason: "invalid_failure_evidence", evidence: "none" };
2468
2629
  if (structured.status === "malformed") {
2469
2630
  return { kind: "do_not_retry", reason: "malformed_provider_diagnostic", evidence: "none" };
2470
2631
  }
@@ -2498,7 +2659,7 @@ export class AgentSession {
2498
2659
  this._appendAutoRetryRecord(record);
2499
2660
  }
2500
2661
  catch (error) {
2501
- if (retry) {
2662
+ if (retry && !this._manualCompactionDrain) {
2502
2663
  this._emit({
2503
2664
  type: "auto_retry_end",
2504
2665
  success: false,
@@ -2508,7 +2669,7 @@ export class AgentSession {
2508
2669
  }
2509
2670
  throw this._reportRetryFailure(error);
2510
2671
  }
2511
- if (retry && end)
2672
+ if (retry && end && !this._manualCompactionDrain)
2512
2673
  this._emit(end);
2513
2674
  }
2514
2675
  _finishRetryWithoutSuccess(record, message) {
@@ -2561,9 +2722,8 @@ export class AgentSession {
2561
2722
  async _handleRetryableError(message, evidence, run) {
2562
2723
  const settings = this.settingsManager.getRetrySettings();
2563
2724
  const attempt = (this._retry?.attempt ?? 0) + 1;
2564
- this._runRetryCount++;
2565
- const cumulativeCap = settings.maxRetries * 2;
2566
- if (attempt > settings.maxRetries) {
2725
+ const cumulativeCap = Math.min(Number.MAX_SAFE_INTEGER, settings.maxRetries * 2);
2726
+ if ((this._retry?.attempt ?? 0) >= settings.maxRetries) {
2567
2727
  const attemptsCompleted = attempt - 1;
2568
2728
  this._finishRetry({
2569
2729
  schemaVersion: 1,
@@ -2575,15 +2735,15 @@ export class AgentSession {
2575
2735
  }, { type: "auto_retry_end", success: false, attempt: attemptsCompleted, finalError: message.errorMessage });
2576
2736
  return false;
2577
2737
  }
2578
- if (this._runRetryCount > cumulativeCap) {
2579
- const attemptsCompleted = this._runRetryCount - 1;
2738
+ if (this._runRetryCount >= cumulativeCap) {
2739
+ const attemptsCompleted = this._runRetryCount;
2580
2740
  this._finishRetry({
2581
2741
  schemaVersion: 1,
2582
2742
  outcome: "exhausted",
2583
2743
  reason: "cumulative_limit",
2584
2744
  attemptsCompleted: this._bounded(attemptsCompleted),
2585
2745
  maxAttempts: this._bounded(settings.maxRetries),
2586
- cumulativeErrors: this._bounded(this._runRetryCount),
2746
+ cumulativeErrors: this._bounded(this._runRetryCount + 1),
2587
2747
  }, {
2588
2748
  type: "auto_retry_end",
2589
2749
  success: false,
@@ -2592,7 +2752,10 @@ export class AgentSession {
2592
2752
  });
2593
2753
  return false;
2594
2754
  }
2595
- const delayMs = settings.baseDelayMs * 2 ** (attempt - 1);
2755
+ this._runRetryCount++;
2756
+ const delayMs = settings.baseDelayMs === 0
2757
+ ? 0
2758
+ : Math.min(2_147_483_647, settings.baseDelayMs * 2 ** Math.min(31, attempt - 1));
2596
2759
  const previous = this._retry;
2597
2760
  this._retry = undefined;
2598
2761
  try {
@@ -2645,6 +2808,8 @@ export class AgentSession {
2645
2808
  if (controller.signal.aborted || this._disposed) {
2646
2809
  return this._finishCancelledRetry("cancelled_during_backoff", backoff);
2647
2810
  }
2811
+ if (this._manualCompactionDrain)
2812
+ return this._finishCancelledRetry("cancelled_during_backoff", backoff);
2648
2813
  // Hand the chain to the continuation before `continue()`; the run that eventually emits
2649
2814
  // `agent_start` adopts it (see `_captureAgentRunSettlement`), however long Agent defers that start.
2650
2815
  const continuing = { phase: "continuing", attempt, chain: run.chain };
@@ -3051,6 +3216,43 @@ export class AgentSession {
3051
3216
  }
3052
3217
  return result;
3053
3218
  }
3219
+ // SCRAMJET-DIVERGENCE: whole-journal parent/tool costs plus reports awaiting successful append (#598).
3220
+ getRecordedSessionCost() {
3221
+ let total = 0;
3222
+ for (const entry of this.sessionManager.getEntries()) {
3223
+ if (entry.type !== "message")
3224
+ continue;
3225
+ const message = entry.message;
3226
+ if (message.role === "assistant") {
3227
+ if (validRecordedCost(message.usage?.cost?.total))
3228
+ total += message.usage.cost.total;
3229
+ }
3230
+ else if (message.role === "toolResult") {
3231
+ if (validRecordedCost(message.cost)) {
3232
+ total += message.cost;
3233
+ continue;
3234
+ }
3235
+ const definition = this._historicalCostDefinitions.get(message.toolName);
3236
+ if (!definition?.getHistoricalCost)
3237
+ continue;
3238
+ try {
3239
+ const cost = definition.getHistoricalCost(message.details);
3240
+ if (validRecordedCost(cost))
3241
+ total += cost;
3242
+ }
3243
+ catch (error) {
3244
+ this._extensionRunner.emitError({
3245
+ extensionPath: `tool:${message.toolName}`,
3246
+ event: "historical_cost",
3247
+ error: `Failed to interpret historical tool cost: ${error instanceof Error ? error.message : String(error)}`,
3248
+ });
3249
+ }
3250
+ }
3251
+ }
3252
+ for (const execution of this._pendingToolCosts)
3253
+ total += execution.cost ?? 0;
3254
+ return total;
3255
+ }
3054
3256
  /**
3055
3257
  * Get session statistics.
3056
3258
  */