reasonix 0.23.0 → 0.23.1

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/dist/index.d.ts CHANGED
@@ -631,6 +631,7 @@ declare class CacheFirstLoop {
631
631
  private _escalateThisTurn;
632
632
  private _turnFailureCount;
633
633
  private _turnFailureTypes;
634
+ private _turnSelfCorrected;
634
635
  constructor(opts: CacheFirstLoopOptions);
635
636
  /** Shrink huge edit_file/write_file args post-dispatch — tool result already explains. */
636
637
  private compactToolCallArgsAfterResponse;
@@ -642,6 +643,8 @@ declare class CacheFirstLoop {
642
643
  charsSaved: number;
643
644
  };
644
645
  appendAndPersist(message: ChatMessage): void;
646
+ /** Swap the just-appended assistant entry — used by self-correction to restore the original tool_calls without dropping reasoning_content. */
647
+ private replaceTailAssistantMessage;
645
648
  /** "New chat" — drops messages but keeps session + immutable prefix (cache-first invariant). */
646
649
  clearLog(): {
647
650
  dropped: number;
package/dist/index.js CHANGED
@@ -1573,7 +1573,7 @@ var StormBreaker = class {
1573
1573
  if (count >= this.threshold - 1) {
1574
1574
  return {
1575
1575
  suppress: true,
1576
- reason: `call-storm suppressed: ${name} called with identical args ${count + 1} times within window=${this.windowSize}`
1576
+ reason: `${name} called with identical args ${count + 1} times \u2014 repeat-loop guard tripped`
1577
1577
  };
1578
1578
  }
1579
1579
  this.recent.push({ name, args, readOnly });
@@ -1858,6 +1858,7 @@ var CacheFirstLoop = class {
1858
1858
  _escalateThisTurn = false;
1859
1859
  _turnFailureCount = 0;
1860
1860
  _turnFailureTypes = {};
1861
+ _turnSelfCorrected = false;
1861
1862
  constructor(opts) {
1862
1863
  this.client = opts.client;
1863
1864
  this.prefix = opts.prefix;
@@ -1902,7 +1903,12 @@ var CacheFirstLoop = class {
1902
1903
  }
1903
1904
  return def.readOnly !== true;
1904
1905
  };
1905
- this.repair = new ToolCallRepair({ allowedToolNames: allowedNames, isMutating });
1906
+ this.repair = new ToolCallRepair({
1907
+ allowedToolNames: allowedNames,
1908
+ isMutating,
1909
+ stormThreshold: parsePositiveIntEnv(process.env.REASONIX_STORM_THRESHOLD),
1910
+ stormWindow: parsePositiveIntEnv(process.env.REASONIX_STORM_WINDOW)
1911
+ });
1906
1912
  this.sessionName = opts.session ?? null;
1907
1913
  if (this.sessionName) {
1908
1914
  const prior = loadSessionMessages(this.sessionName);
@@ -1984,6 +1990,21 @@ var CacheFirstLoop = class {
1984
1990
  }
1985
1991
  }
1986
1992
  }
1993
+ /** Swap the just-appended assistant entry — used by self-correction to restore the original tool_calls without dropping reasoning_content. */
1994
+ replaceTailAssistantMessage(message) {
1995
+ const entries = this.log.entries;
1996
+ const tail = entries[entries.length - 1];
1997
+ if (!tail || tail.role !== "assistant") return;
1998
+ const kept = entries.slice(0, -1);
1999
+ kept.push(message);
2000
+ this.log.compactInPlace(kept);
2001
+ if (this.sessionName) {
2002
+ try {
2003
+ rewriteSession(this.sessionName, kept);
2004
+ } catch {
2005
+ }
2006
+ }
2007
+ }
1987
2008
  /** "New chat" — drops messages but keeps session + immutable prefix (cache-first invariant). */
1988
2009
  clearLog() {
1989
2010
  const dropped = this.log.length;
@@ -2084,7 +2105,7 @@ var CacheFirstLoop = class {
2084
2105
  if (repair) {
2085
2106
  if (repair.scavenged > 0) bump("scavenged", repair.scavenged);
2086
2107
  if (repair.truncationsFixed > 0) bump("truncated", repair.truncationsFixed);
2087
- if (repair.stormsBroken > 0) bump("storm-broken", repair.stormsBroken);
2108
+ if (repair.stormsBroken > 0) bump("repeat-loop", repair.stormsBroken);
2088
2109
  }
2089
2110
  if (bumped && !this._escalateThisTurn && this.autoEscalate && this._turnFailureCount >= FAILURE_ESCALATION_THRESHOLD) {
2090
2111
  this._escalateThisTurn = true;
@@ -2154,6 +2175,7 @@ var CacheFirstLoop = class {
2154
2175
  this.repair.resetStorm();
2155
2176
  this._turnFailureCount = 0;
2156
2177
  this._turnFailureTypes = {};
2178
+ this._turnSelfCorrected = false;
2157
2179
  this._escalateThisTurn = false;
2158
2180
  let armedConsumed = false;
2159
2181
  if (this._proArmedForNextTurn) {
@@ -2505,10 +2527,35 @@ var CacheFirstLoop = class {
2505
2527
  content: `\u21E7 auto-escalating to ${ESCALATION_MODEL} for the rest of this turn \u2014 flash hit ${this.formatFailureBreakdown()}. Next turn falls back to ${this.model} unless /pro is armed.`
2506
2528
  };
2507
2529
  }
2530
+ const allSuppressed = report.stormsBroken > 0 && repairedCalls.length === 0 && toolCalls.length > 0;
2531
+ if (allSuppressed && !this._turnSelfCorrected) {
2532
+ this._turnSelfCorrected = true;
2533
+ this.replaceTailAssistantMessage(
2534
+ this.assistantMessage(
2535
+ assistantContent,
2536
+ toolCalls,
2537
+ this.modelForCurrentCall(),
2538
+ reasoningContent
2539
+ )
2540
+ );
2541
+ for (const call of toolCalls) {
2542
+ this.appendAndPersist({
2543
+ role: "tool",
2544
+ tool_call_id: call.id ?? "",
2545
+ name: call.function?.name ?? "",
2546
+ content: "[repeat-loop guard] this call was suppressed because it was identical to a previous call in this turn. Earlier results for it are above \u2014 try a meaningfully different approach, or stop and answer if you have enough."
2547
+ });
2548
+ }
2549
+ yield {
2550
+ turn: this._turn,
2551
+ role: "warning",
2552
+ content: "Caught a repeated tool call \u2014 let the model see the issue and retry with a different approach."
2553
+ };
2554
+ continue;
2555
+ }
2508
2556
  if (report.stormsBroken > 0) {
2509
2557
  const noteTail = report.notes.length ? ` \u2014 ${report.notes[report.notes.length - 1]}` : "";
2510
- const allSuppressed = repairedCalls.length === 0 && toolCalls.length > 0;
2511
- const phrase = allSuppressed ? `stopped the model from calling the same tool with identical args repeatedly (all ${toolCalls.length} call(s) this turn were already in the recent-repeat window). Likely a stuck retry \u2014 reword your instruction, rule out the underlying blocker, or try /retry after fixing it` : `suppressed ${report.stormsBroken} repeat tool call(s) that had fired 3+ times with identical args in a sliding window`;
2558
+ const phrase = allSuppressed ? "Stopped a stuck retry loop \u2014 the model kept calling the same tool with identical args after a self-correction nudge. Try /retry, rephrase, or rule out the underlying blocker." : `Suppressed ${report.stormsBroken} repeated tool call(s) \u2014 same name + args fired 3+ times.`;
2512
2559
  yield {
2513
2560
  turn: this._turn,
2514
2561
  role: "warning",
@@ -2516,7 +2563,6 @@ var CacheFirstLoop = class {
2516
2563
  };
2517
2564
  }
2518
2565
  if (repairedCalls.length === 0) {
2519
- const allSuppressed = report.stormsBroken > 0 && toolCalls.length > 0;
2520
2566
  if (allSuppressed) {
2521
2567
  yield* this.forceSummaryAfterIterLimit({ reason: "stuck" });
2522
2568
  return;
@@ -2741,6 +2787,11 @@ function stripHallucinatedToolMarkup(s) {
2741
2787
  out = out.replace(/<|DSML|[\s\S]*$/g, "");
2742
2788
  return out.trim();
2743
2789
  }
2790
+ function parsePositiveIntEnv(raw) {
2791
+ if (!raw) return void 0;
2792
+ const n = Number.parseInt(raw, 10);
2793
+ return Number.isFinite(n) && n > 0 ? n : void 0;
2794
+ }
2744
2795
  function safeParseToolArgs(raw) {
2745
2796
  try {
2746
2797
  return JSON.parse(raw);