@leanandmean/coding-agent 0.74.1-scramjet.68 → 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";
@@ -1319,7 +1318,7 @@ export class AgentSession {
1319
1318
  // Check if we need to compact before sending (catches aborted responses)
1320
1319
  const lastAssistant = this._findLastAssistantMessage();
1321
1320
  if (lastAssistant) {
1322
- await this._checkCompaction(lastAssistant, false);
1321
+ await this._checkCompaction(lastAssistant, "new_prompt");
1323
1322
  }
1324
1323
  // Build messages array (custom message if any, then user message)
1325
1324
  messages = [];
@@ -2005,17 +2004,43 @@ export class AgentSession {
2005
2004
  * 2. Threshold: Context over threshold, compact, NO auto-retry (user continues manually)
2006
2005
  *
2007
2006
  * @param assistantMessage The assistant message to check
2008
- * @param skipAbortedCheck If false, include aborted messages (for pre-prompt check). Default: true
2009
2007
  */
2010
- async _checkCompaction(assistantMessage, skipAbortedCheck = true) {
2008
+ async _checkCompaction(assistantMessage, trigger = "settlement") {
2011
2009
  if (this._manualCompactionDrain)
2012
2010
  return;
2013
2011
  const settings = this.settingsManager.getCompactionSettings();
2014
2012
  if (!settings.enabled)
2015
2013
  return;
2016
- // Skip if message was aborted (user cancelled) - unless skipAbortedCheck is false
2017
- if (skipAbortedCheck && assistantMessage.stopReason === "aborted")
2014
+ // SCRAMJET-DIVERGENCE: New prompts maintain context independently of failed-turn recovery.
2015
+ if (trigger === "settlement" && blocksFailureRecovery(assistantMessage))
2018
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
+ }
2019
2044
  // SCRAMJET-DIVERGENCE: Total context and independent input limits are separate constraints.
2020
2045
  const contextWindow = this.model?.contextWindow ?? 0;
2021
2046
  // Skip overflow check if the message came from a different model.
@@ -2577,20 +2602,30 @@ export class AgentSession {
2577
2602
  // =========================================================================
2578
2603
  // SCRAMJET-DIVERGENCE: persisted post-extension snapshots authoritatively classify and settle retries (#553).
2579
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" };
2580
2607
  const contextWindow = this.model?.contextWindow ?? 0;
2581
2608
  if (isContextOverflow(message, Math.min(contextWindow, this.model?.maxInputTokens ?? Infinity))) {
2582
2609
  return { kind: "context_overflow" };
2583
2610
  }
2584
- const structured = validateResponsesProviderFailure(message.diagnostics);
2611
+ const structured = inspectFailureEvidence(message.diagnostics);
2585
2612
  if (structured.status === "valid") {
2586
- if (structured.retryDisposition === "transient")
2587
- 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 };
2588
2619
  return {
2589
2620
  kind: "do_not_retry",
2590
- reason: structured.retryDisposition === "non_transient" ? "structured_non_transient" : "structured_unknown",
2591
- evidence: "provider_failure",
2621
+ reason: ["unknown", "provider_error", "malformed_event"].includes(structured.category)
2622
+ ? "structured_unknown"
2623
+ : "structured_non_transient",
2624
+ evidence: structured.family,
2592
2625
  };
2593
2626
  }
2627
+ if (structured.status === "conflicting")
2628
+ return { kind: "do_not_retry", reason: "invalid_failure_evidence", evidence: "none" };
2594
2629
  if (structured.status === "malformed") {
2595
2630
  return { kind: "do_not_retry", reason: "malformed_provider_diagnostic", evidence: "none" };
2596
2631
  }
@@ -2687,9 +2722,8 @@ export class AgentSession {
2687
2722
  async _handleRetryableError(message, evidence, run) {
2688
2723
  const settings = this.settingsManager.getRetrySettings();
2689
2724
  const attempt = (this._retry?.attempt ?? 0) + 1;
2690
- this._runRetryCount++;
2691
- const cumulativeCap = settings.maxRetries * 2;
2692
- if (attempt > settings.maxRetries) {
2725
+ const cumulativeCap = Math.min(Number.MAX_SAFE_INTEGER, settings.maxRetries * 2);
2726
+ if ((this._retry?.attempt ?? 0) >= settings.maxRetries) {
2693
2727
  const attemptsCompleted = attempt - 1;
2694
2728
  this._finishRetry({
2695
2729
  schemaVersion: 1,
@@ -2701,15 +2735,15 @@ export class AgentSession {
2701
2735
  }, { type: "auto_retry_end", success: false, attempt: attemptsCompleted, finalError: message.errorMessage });
2702
2736
  return false;
2703
2737
  }
2704
- if (this._runRetryCount > cumulativeCap) {
2705
- const attemptsCompleted = this._runRetryCount - 1;
2738
+ if (this._runRetryCount >= cumulativeCap) {
2739
+ const attemptsCompleted = this._runRetryCount;
2706
2740
  this._finishRetry({
2707
2741
  schemaVersion: 1,
2708
2742
  outcome: "exhausted",
2709
2743
  reason: "cumulative_limit",
2710
2744
  attemptsCompleted: this._bounded(attemptsCompleted),
2711
2745
  maxAttempts: this._bounded(settings.maxRetries),
2712
- cumulativeErrors: this._bounded(this._runRetryCount),
2746
+ cumulativeErrors: this._bounded(this._runRetryCount + 1),
2713
2747
  }, {
2714
2748
  type: "auto_retry_end",
2715
2749
  success: false,
@@ -2718,7 +2752,10 @@ export class AgentSession {
2718
2752
  });
2719
2753
  return false;
2720
2754
  }
2721
- 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));
2722
2759
  const previous = this._retry;
2723
2760
  this._retry = undefined;
2724
2761
  try {