reasonix 0.13.5 → 0.15.0

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.js CHANGED
@@ -139,12 +139,7 @@ var DeepSeekClient = class {
139
139
  }
140
140
  return payload;
141
141
  }
142
- /**
143
- * Fetch the current DeepSeek account balance. Separate endpoint
144
- * from chat completions, no billing impact. Returns null on any
145
- * network/auth failure so callers can gate the balance display
146
- * without a hard error — the rest of the session works regardless.
147
- */
142
+ /** Returns null on failure so callers can degrade — session must keep working without balance UI. */
148
143
  async getBalance(opts = {}) {
149
144
  try {
150
145
  const resp = await this._fetch(`${this.baseUrl}/user/balance`, {
@@ -160,13 +155,7 @@ var DeepSeekClient = class {
160
155
  return null;
161
156
  }
162
157
  }
163
- /**
164
- * Fetch the model catalog DeepSeek currently exposes. Today this is
165
- * `deepseek-chat` (V3) and `deepseek-reasoner` (R1), but querying is
166
- * the only way to learn about new ones without a Reasonix release.
167
- * Returns null on any network/auth failure so callers can degrade
168
- * gracefully — e.g. `/models` falls back to the hardcoded hint.
169
- */
158
+ /** Returns null on failure — callers fall back to a hardcoded model hint. */
170
159
  async listModels(opts = {}) {
171
160
  try {
172
161
  const resp = await this._fetch(`${this.baseUrl}/models`, {
@@ -915,19 +904,7 @@ function setByPath(target, path, value) {
915
904
  var ToolRegistry = class {
916
905
  _tools = /* @__PURE__ */ new Map();
917
906
  _autoFlatten;
918
- /**
919
- * When true, `dispatch` refuses any tool whose `readOnly` flag isn't
920
- * set (and whose `readOnlyCheck` doesn't pass on the specific args).
921
- * Drives `reasonix code`'s Plan Mode — the model can still explore
922
- * via read tools but its writes and non-allowlisted shell calls are
923
- * bounced until the user approves a submitted plan.
924
- */
925
907
  _planMode = false;
926
- /**
927
- * Optional hook run after arg parsing but before tool.fn. Lets the TUI
928
- * reroute specific tool calls (e.g. edit_file in review mode) without
929
- * modifying the tool definitions themselves.
930
- */
931
908
  _interceptor = null;
932
909
  constructor(opts = {}) {
933
910
  this._autoFlatten = opts.autoFlatten !== false;
@@ -940,11 +917,7 @@ var ToolRegistry = class {
940
917
  get planMode() {
941
918
  return this._planMode;
942
919
  }
943
- /**
944
- * Install or clear the dispatch interceptor. At most one interceptor
945
- * is active at a time — calling twice replaces the previous. Pass
946
- * `null` to remove.
947
- */
920
+ /** At most one interceptor active; calling twice replaces. */
948
921
  setToolInterceptor(fn) {
949
922
  this._interceptor = fn;
950
923
  }
@@ -1180,33 +1153,10 @@ function blockToString(block) {
1180
1153
  import { createHash } from "crypto";
1181
1154
  var ImmutablePrefix = class {
1182
1155
  system;
1183
- /**
1184
- * Backing array for `toolSpecs`. Originally `Object.freeze`d at
1185
- * construction (hence the class name) — but `addTool` now lets the
1186
- * dashboard register `semantic_search` after a mid-session
1187
- * `reasonix index` build without forcing the user to restart. Each
1188
- * add is documented to cost one cache-miss turn (the cached prefix
1189
- * on DeepSeek's side is keyed by the full tool list); subsequent
1190
- * turns re-cache against the new shape.
1191
- */
1156
+ /** Each `addTool` costs one cache-miss turn — DeepSeek's prefix cache is keyed by full tool list. */
1192
1157
  _toolSpecs;
1193
1158
  fewShots;
1194
- /**
1195
- * Cached SHA-256 of the prefix payload. Computed lazily on first
1196
- * `fingerprint` access, invalidated only by mutations that go
1197
- * through `addTool` (the one legitimate post-construction mutation
1198
- * path). The TUI reads `fingerprint` on every render — without the
1199
- * cache, that means a fresh `JSON.stringify` + sha256 over the
1200
- * full prefix (system prompt + tools list + few-shots, typically
1201
- * 5-10KB) on every keystroke.
1202
- *
1203
- * The lazy-init also acts as a cheap drift guard: if some future
1204
- * code path mutates `_toolSpecs` directly without going through
1205
- * `addTool`, `fingerprint` will return the stale cached value
1206
- * while the actual prefix sent to DeepSeek diverges — the cache
1207
- * miss would be the first symptom. {@link verifyFingerprint}
1208
- * lets dev / test code assert the cache matches reality.
1209
- */
1159
+ /** Invalidated only via `addTool`; bypassing it leaves cache stale → fingerprint diverges from sent prefix. */
1210
1160
  _fingerprintCache = null;
1211
1161
  constructor(opts) {
1212
1162
  this.system = opts.system;
@@ -1222,12 +1172,6 @@ var ImmutablePrefix = class {
1222
1172
  tools() {
1223
1173
  return this._toolSpecs.map((t) => structuredClone(t));
1224
1174
  }
1225
- /**
1226
- * Add a tool spec to the prefix. Returns `true` if added, `false`
1227
- * if a tool with the same name was already present (callers can
1228
- * decide whether to ignore or surface the no-op). The model picks
1229
- * up the new tool on the next turn after the cache busts once.
1230
- */
1231
1175
  addTool(spec) {
1232
1176
  const name = spec.function?.name;
1233
1177
  if (!name) return false;
@@ -1241,14 +1185,7 @@ var ImmutablePrefix = class {
1241
1185
  this._fingerprintCache = this.computeFingerprint();
1242
1186
  return this._fingerprintCache;
1243
1187
  }
1244
- /**
1245
- * Recompute the fingerprint from scratch and assert it matches the
1246
- * cached value. Returns the freshly-computed hash on success; throws
1247
- * with a diff if the cache drifted, which always indicates a bug —
1248
- * either a non-`addTool` mutation path was added, or `addTool`
1249
- * forgot to invalidate the cache. Dev / test only; the live loop
1250
- * doesn't call this on the hot path.
1251
- */
1188
+ /** Dev/test only — throws on cache drift, which always means a non-`addTool` mutation slipped in. */
1252
1189
  verifyFingerprint() {
1253
1190
  const fresh = this.computeFingerprint();
1254
1191
  if (this._fingerprintCache !== null && this._fingerprintCache !== fresh) {
@@ -1279,13 +1216,7 @@ var AppendOnlyLog = class {
1279
1216
  extend(messages) {
1280
1217
  for (const m of messages) this.append(m);
1281
1218
  }
1282
- /**
1283
- * Bulk-replace entries. Intentionally named to be hard to reach for —
1284
- * this is the one mutation path that breaks the log's append-only
1285
- * spirit, reserved for compaction flows (`/compact`) and recovery
1286
- * where the caller has consciously decided to drop old history. Any
1287
- * other use is almost certainly wrong; append() is what you want.
1288
- */
1219
+ /** The one append-only-breaking path — reserved for `/compact` + recovery. Use `append()` otherwise. */
1289
1220
  compactInPlace(replacement) {
1290
1221
  this._entries = [...replacement];
1291
1222
  }
@@ -1559,13 +1490,7 @@ var ToolCallRepair = class {
1559
1490
  this.opts = opts;
1560
1491
  this.storm = new StormBreaker(opts.stormWindow ?? 6, opts.stormThreshold ?? 3, opts.isMutating);
1561
1492
  }
1562
- /**
1563
- * Drop the StormBreaker's sliding window of recent (name, args)
1564
- * signatures. Called at the start of every user turn — a fresh user
1565
- * message is a new intent, so carrying old repetition state into it
1566
- * would turn a valid "try again with different input" flow into a
1567
- * false-positive block.
1568
- */
1493
+ /** Called at start of every user turn — fresh intent shouldn't inherit old repetition state. */
1569
1494
  resetStorm() {
1570
1495
  this.storm.reset();
1571
1496
  }
@@ -1846,85 +1771,23 @@ var CacheFirstLoop = class {
1846
1771
  harvestOptions;
1847
1772
  branchEnabled;
1848
1773
  branchOptions;
1849
- /** See ReconfigurableOptions — mutable so `/effort` can flip mid-session. */
1850
1774
  reasoningEffort;
1851
- /**
1852
- * Auto-escalation toggle. `true` lets the loop self-promote to pro
1853
- * mid-turn (NEEDS_PRO marker / failure threshold); `false` keeps it
1854
- * pinned to `model`. Mutable so the dashboard's preset switcher can
1855
- * flip it live alongside `model`.
1856
- */
1857
1775
  autoEscalate = true;
1858
- /**
1859
- * Soft USD budget — see {@link CacheFirstLoopOptions.budgetUsd}.
1860
- * Mutable so `/budget` slash can set / change / clear it mid-session.
1861
- * `null` (the default) disables all budget checks.
1862
- */
1863
1776
  budgetUsd;
1864
- /**
1865
- * Set the first time a turn crosses 80% of the budget so the warning
1866
- * doesn't repeat every turn afterwards. Cleared by `setBudget` (any
1867
- * change re-arms the warning, including raising the cap above the
1868
- * current spend).
1869
- */
1777
+ /** One-shot 80% warning latch — cleared by setBudget so a bump re-arms at the new boundary. */
1870
1778
  _budgetWarned = false;
1871
1779
  sessionName;
1872
- /**
1873
- * Hook list, mutable so `/hooks reload` can swap it without
1874
- * reconstructing the loop. Default empty — the filter cost on a
1875
- * tool call is one array length check.
1876
- */
1877
1780
  hooks;
1878
- /**
1879
- * `cwd` reported to hook stdin. Mutable so `/cwd` can switch the
1880
- * working directory mid-session — the App keeps it in sync with
1881
- * the same currentRootDir that drives tool re-registration.
1882
- */
1883
1781
  hookCwd;
1884
1782
  /** Number of messages that were pre-loaded from the session file. */
1885
1783
  resumedMessageCount;
1886
1784
  _turn = 0;
1887
1785
  _streamPreference;
1888
- /**
1889
- * AbortController per active turn. Threaded through the DeepSeek
1890
- * HTTP calls AND every tool dispatch so Esc actually cancels the
1891
- * in-flight network/subprocess work — not "we'll get to it after
1892
- * the current call finishes." Re-created at the start of each
1893
- * `step()` (the prior turn's signal has already fired).
1894
- */
1786
+ /** Threaded through HTTP + every tool dispatch so Esc cancels in-flight work, not after. */
1895
1787
  _turnAbort = new AbortController();
1896
- /**
1897
- * "Next turn should run on pro, regardless of this.model." Set by the
1898
- * `/pro` slash command; consumed at the next turn's start (flipping
1899
- * `_escalateThisTurn` on and self-clearing) so it's a fire-and-forget
1900
- * single-turn upgrade. Survives across multiple slash inputs so
1901
- * typing `/pro` and then hesitating a while before submitting a real
1902
- * message still applies.
1903
- */
1904
1788
  _proArmedForNextTurn = false;
1905
- /**
1906
- * Active for the current turn only — true means every model call
1907
- * this turn uses pro instead of `this.model`. Turned on by EITHER
1908
- * the pro-armed consumption OR the mid-turn auto-escalation
1909
- * threshold (see `_turnFailureCount`). Cleared at turn end.
1910
- */
1911
1789
  _escalateThisTurn = false;
1912
- /**
1913
- * Visible-failure count for the current turn. Incremented by tool
1914
- * dispatch paths when a result matches a known "flash is struggling"
1915
- * shape (SEARCH-not-found errors, scavenge / truncation / storm
1916
- * repair fires). Once it hits {@link FAILURE_ESCALATION_THRESHOLD},
1917
- * the remainder of the turn's model calls auto-upgrade to pro so
1918
- * the user doesn't watch flash retry the same edit 5 times.
1919
- */
1920
1790
  _turnFailureCount = 0;
1921
- /**
1922
- * Per-type breakdown of failure signals counted toward the turn's
1923
- * auto-escalation threshold. Surfaced in the warning when the
1924
- * threshold trips so the user sees what kind of trouble flash
1925
- * actually hit ("3× search-mismatch, 2× truncated") rather than
1926
- * just a bare count. Reset alongside _turnFailureCount.
1927
- */
1928
1791
  _turnFailureTypes = {};
1929
1792
  constructor(opts) {
1930
1793
  this.client = opts.client;
@@ -1995,55 +1858,7 @@ var CacheFirstLoop = class {
1995
1858
  this.resumedMessageCount = 0;
1996
1859
  }
1997
1860
  }
1998
- /**
1999
- * Shrink the log by re-truncating oversized tool results to a tighter
2000
- * token cap, and persist the result back to disk so the next launch
2001
- * doesn't re-inherit a fat session file. Returns a summary the TUI
2002
- * can display.
2003
- *
2004
- * The cap is in DeepSeek V3 tokens (not chars) — so CJK text gets
2005
- * capped at the same effective context footprint as English instead
2006
- * of slipping past a char cap at 2× the token cost. Default 4000
2007
- * tokens, matching the token-aware dispatch cap from 0.5.2.
2008
- *
2009
- * Only tool-role messages are touched (same rationale as
2010
- * {@link healLoadedMessages}). User and assistant messages carry
2011
- * authored intent we can't mechanically shrink without losing
2012
- * meaning.
2013
- */
2014
- /**
2015
- * Conservative args-only shrink fired after every tool response —
2016
- * strictly about ONE thing: stop oversized `edit_file` / `write_file`
2017
- * arguments from riding every future turn's prompt.
2018
- *
2019
- * Why this is worth doing AUTOMATICALLY (not just on /compact):
2020
- * Each tool-call arguments string sticks in the log verbatim. On a
2021
- * coding session with ~10 edits, that's 20-40K tokens of stale
2022
- * SEARCH/REPLACE text riding along on every turn. Even at a 98.9%
2023
- * cache hit rate the input cost still adds up linearly (cache-hit
2024
- * price × tokens × turns). Compacting IMMEDIATELY after the tool
2025
- * responds means the next turn's prompt is already smaller — the
2026
- * shrink is a one-time write that saves every future prompt.
2027
- *
2028
- * Threshold rationale: 800 tokens ≈ 3 KB. A typical 20-line edit's
2029
- * args land well under that; massive rewrites (whole-file content,
2030
- * 100+ line refactors) land above and get the compaction. Small
2031
- * edits stay byte-verbatim so nothing common-case changes.
2032
- *
2033
- * Safety: we ONLY shrink args whose tool has ALREADY responded.
2034
- * Structurally that's every call in `log.toMessages()` at this
2035
- * point — the current turn's assistant/tool pairing is by
2036
- * construction closed by the time we get here (append happens
2037
- * AFTER dispatch). The in-flight assistant message being built
2038
- * lives in scratch, not the log, so this pass can't touch it.
2039
- *
2040
- * Model impact: the model may occasionally want to reference the
2041
- * exact SEARCH text of a prior edit — it then reads the file
2042
- * directly (which shows current state) or looks at the preceding
2043
- * assistant text (which has its plan). Losing the stale args is a
2044
- * net win: one extra read_file vs. dragging N KB of stale text
2045
- * through every subsequent turn.
2046
- */
1861
+ /** Shrink huge edit_file/write_file args post-dispatch — tool result already explains. */
2047
1862
  compactToolCallArgsAfterResponse() {
2048
1863
  const before = this.log.toMessages();
2049
1864
  const { messages, healedCount } = shrinkOversizedToolCallArgsByTokens(
@@ -2059,25 +1874,7 @@ var CacheFirstLoop = class {
2059
1874
  }
2060
1875
  }
2061
1876
  }
2062
- /**
2063
- * Fired at the END of a turn (just before `done` is yielded). Shrinks
2064
- * every tool RESULT in the log that exceeds {@link TURN_END_RESULT_CAP_TOKENS}
2065
- * to a tight cap so the NEXT turn's prompt doesn't re-pay for big
2066
- * reads or searches done earlier. Unlike the reactive 40/80%
2067
- * thresholds which react to context pressure, this runs unconditionally
2068
- * — the win is preventive: each turn's big outputs get trimmed before
2069
- * they ride into the next prompt. Saves compounding cost on long
2070
- * sessions.
2071
- *
2072
- * Why compact the JUST-finished turn's results too (not just older
2073
- * turns)? The same-turn iters already consumed the raw content to
2074
- * make their decisions — the log is only carried forward for future
2075
- * prompts. And "let me re-read the file" is vastly cheaper than
2076
- * "carry this 12KB result in every future turn's prompt forever."
2077
- *
2078
- * Safe by construction: args-compact for THIS turn already ran
2079
- * inside `compactToolCallArgsAfterResponse`; this pass is orthogonal.
2080
- */
1877
+ /** Preventive end-of-turn shrink — trim big results before they ride into the next prompt. */
2081
1878
  autoCompactToolResultsOnTurnEnd() {
2082
1879
  const before = this.log.toMessages();
2083
1880
  const shrunk = shrinkOversizedToolResultsByTokens(before, TURN_END_RESULT_CAP_TOKENS);
@@ -2118,17 +1915,7 @@ var CacheFirstLoop = class {
2118
1915
  }
2119
1916
  }
2120
1917
  }
2121
- /**
2122
- * Start a fresh conversation WITHOUT exiting. Drops every message
2123
- * in the in-memory log AND rewrites the session file to empty so
2124
- * a resume won't re-hydrate the old turns. Unlike `/forget`, which
2125
- * deletes the session entirely, this keeps the session name and
2126
- * config intact — it's the "new chat" button.
2127
- *
2128
- * The immutable prefix (system prompt + tool specs) is preserved
2129
- * — that's the cache-first invariant, not part of the conversation.
2130
- * Returns the number of messages dropped so the UI can show it.
2131
- */
1918
+ /** "New chat" — drops messages but keeps session + immutable prefix (cache-first invariant). */
2132
1919
  clearLog() {
2133
1920
  const dropped = this.log.length;
2134
1921
  this.log.compactInPlace([]);
@@ -2141,12 +1928,6 @@ var CacheFirstLoop = class {
2141
1928
  this.scratch.reset();
2142
1929
  return { dropped };
2143
1930
  }
2144
- /**
2145
- * Reconfigure model/harvest/branch/stream mid-session. The loop's log,
2146
- * scratch, and stats are preserved — only the per-turn behavior changes.
2147
- * Used by the TUI's slash commands and by library callers who want to
2148
- * flip a knob between turns.
2149
- */
2150
1931
  configure(opts) {
2151
1932
  if (opts.model !== void 0) this.model = opts.model;
2152
1933
  if (opts.stream !== void 0) this._streamPreference = opts.stream;
@@ -2173,22 +1954,12 @@ var CacheFirstLoop = class {
2173
1954
  }
2174
1955
  this.stream = this.branchEnabled ? false : this._streamPreference;
2175
1956
  }
2176
- /**
2177
- * Set / change / clear the soft USD budget. `null` (or any non-
2178
- * positive number) disables the cap entirely. Re-arms the 80%
2179
- * warning so a user who bumps the cap mid-session sees a fresh
2180
- * threshold message at the new boundary.
2181
- */
1957
+ /** `null` disables the cap; any change re-arms the 80% warning. */
2182
1958
  setBudget(usd) {
2183
1959
  this.budgetUsd = typeof usd === "number" && usd > 0 ? usd : null;
2184
1960
  this._budgetWarned = false;
2185
1961
  }
2186
- /**
2187
- * Arm pro for the next turn (consumed at turn start). Called by
2188
- * `/pro`. Idempotent — repeated calls stay armed, `disarmPro()`
2189
- * clears. Separate from `/preset max` which persistently switches
2190
- * this.model; armed state is strictly single-turn.
2191
- */
1962
+ /** Single-turn upgrade consumed at next step() — distinct from `/preset max` (persistent). */
2192
1963
  armProForNextTurn() {
2193
1964
  this._proArmedForNextTurn = true;
2194
1965
  }
@@ -2204,25 +1975,10 @@ var CacheFirstLoop = class {
2204
1975
  get escalatedThisTurn() {
2205
1976
  return this._escalateThisTurn;
2206
1977
  }
2207
- /**
2208
- * Model the current model call should use. Defaults to `this.model`;
2209
- * upgrades to {@link ESCALATION_MODEL} when the turn is armed for
2210
- * pro (via `/pro`) or has hit the failure-escalation threshold.
2211
- * Same thinking + effort policy applies regardless — pro defaults
2212
- * to thinking=enabled and effort=max, which the current turn wanted
2213
- * anyway when flash was struggling.
2214
- */
2215
1978
  modelForCurrentCall() {
2216
1979
  return this._escalateThisTurn ? ESCALATION_MODEL : this.model;
2217
1980
  }
2218
- /**
2219
- * Parse the escalation marker out of the model's leading content.
2220
- * Returns `{ matched: true, reason? }` for both bare and reason-
2221
- * carrying forms. Only the FIRST line matters — the model is
2222
- * instructed to emit the marker as the first output token if at
2223
- * all. Matches anywhere else in the text are normal content
2224
- * references (e.g. the user asked about the marker itself).
2225
- */
1981
+ /** Anchored to lead — mid-text matches are normal content (user asking about the marker). */
2226
1982
  parseEscalationMarker(content) {
2227
1983
  const m = NEEDS_PRO_MARKER_RE.exec(content.trimStart());
2228
1984
  if (!m) return { matched: false };
@@ -2233,14 +1989,7 @@ var CacheFirstLoop = class {
2233
1989
  isEscalationRequest(content) {
2234
1990
  return this.parseEscalationMarker(content).matched;
2235
1991
  }
2236
- /**
2237
- * Could `buf` STILL plausibly become the full marker as more chunks
2238
- * arrive? Drives the streaming buffer's flush decision: while this
2239
- * is true we keep accumulating; once it's false (or the buffer
2240
- * exceeds the byte limit) we flush so the user isn't staring at a
2241
- * delayed display for arbitrary content that just happens to start
2242
- * with `<`.
2243
- */
1992
+ /** Drives streaming flush — while plausibly partial, keep accumulating; else flush. */
2244
1993
  looksLikePartialEscalationMarker(buf) {
2245
1994
  const t = buf.trimStart();
2246
1995
  if (t.length === 0) return true;
@@ -2252,16 +2001,7 @@ var CacheFirstLoop = class {
2252
2001
  if (rest[0] !== ">" && rest[0] !== ":") return false;
2253
2002
  return true;
2254
2003
  }
2255
- /**
2256
- * Check whether a tool result string looks like a "flash struggled"
2257
- * signal and, if so, increment the turn's failure counter. Escalates
2258
- * the REST of the current turn to pro once the threshold is hit.
2259
- * Idempotent after escalation — further failures don't re-escalate,
2260
- * but the turn is already on pro so it doesn't matter.
2261
- *
2262
- * Return: `true` when this call tipped the turn into escalation
2263
- * mode (so the loop can surface a one-time warning to the user).
2264
- */
2004
+ /** Returns true ONLY on the tipping call — caller surfaces a one-shot warning. */
2265
2005
  noteToolFailureSignal(resultJson, repair) {
2266
2006
  let bumped = false;
2267
2007
  const bump = (kind, by = 1) => {
@@ -2283,12 +2023,6 @@ var CacheFirstLoop = class {
2283
2023
  }
2284
2024
  return false;
2285
2025
  }
2286
- /**
2287
- * Render `_turnFailureTypes` as a comma-separated breakdown like
2288
- * "2× search-mismatch, 1× truncated" for the auto-escalation
2289
- * warning. Empty if no types have been recorded yet (defensive —
2290
- * the warning sites only call this after a bump).
2291
- */
2292
2026
  formatFailureBreakdown() {
2293
2027
  const parts = Object.entries(this._turnFailureTypes).filter(([, n]) => n > 0).map(([kind, n]) => `${n}\xD7 ${kind}`);
2294
2028
  return parts.length > 0 ? parts.join(", ") : `${this._turnFailureCount} repair/error signal(s)`;
@@ -2299,28 +2033,10 @@ var CacheFirstLoop = class {
2299
2033
  if (pendingUser !== null) msgs.push({ role: "user", content: pendingUser });
2300
2034
  return msgs;
2301
2035
  }
2302
- /**
2303
- * Signal the currently-running {@link step} to stop **now**. Cancels
2304
- * the in-flight network request (DeepSeek HTTP/SSE) AND any tool call
2305
- * currently dispatching (MCP `notifications/cancelled` + promise
2306
- * reject). The loop itself also sees `signal.aborted` at each
2307
- * iteration boundary and exits quickly instead of looping again.
2308
- * Called by the TUI on Esc.
2309
- */
2310
2036
  abort() {
2311
2037
  this._turnAbort.abort();
2312
2038
  }
2313
- /**
2314
- * Drop everything in the log after (and including) the most recent
2315
- * user message. Used by `/retry` so the caller can re-send that
2316
- * message with a fresh turn instead of layering another response on
2317
- * top of the prior exchange. Returns the content of the dropped user
2318
- * message, or `null` if there isn't one yet.
2319
- *
2320
- * Persists by rewriting the session file — otherwise the next
2321
- * launch would rehydrate the old exchange and `/retry` would seem
2322
- * to have done nothing.
2323
- */
2039
+ /** Drop the last user message + everything after; caller re-sends. Persists to session file. */
2324
2040
  retryLastUser() {
2325
2041
  const entries = this.log.entries;
2326
2042
  let lastUserIdx = -1;
@@ -2644,6 +2360,7 @@ var CacheFirstLoop = class {
2644
2360
  if (signal.aborted) {
2645
2361
  this.autoCompactToolResultsOnTurnEnd();
2646
2362
  yield { turn: this._turn, role: "done", content: "" };
2363
+ this._turnAbort = new AbortController();
2647
2364
  return;
2648
2365
  }
2649
2366
  yield {
@@ -2930,22 +2647,7 @@ ${summary}`;
2930
2647
  }
2931
2648
  return final;
2932
2649
  }
2933
- /**
2934
- * Build an assistant message for the log. The `producingModel` arg is
2935
- * the model that actually generated this turn (flash, pro, the
2936
- * forced-summary flash call, `this.model` for synthetics, etc.) —
2937
- * NOT `this.model`, because escalation + forced-summary can both
2938
- * route a single turn to a different model.
2939
- *
2940
- * The single invariant this encodes: if the producing model is
2941
- * thinking-mode, `reasoning_content` MUST be present on the
2942
- * persisted message — even as an empty string. DeepSeek's validator
2943
- * 400s the NEXT request if any historical thinking-mode assistant
2944
- * turn is missing it. We used to gate on `reasoning.length > 0`,
2945
- * which silently dropped the field whenever the stream emitted zero
2946
- * reasoning deltas or the API returned `reasoning_content: null` —
2947
- * both legitimate edge cases the 0.5.15/0.5.18 fixes missed.
2948
- */
2650
+ /** Thinking-mode producer ⇒ reasoning_content MUST be set (even ""), or next call 400s. */
2949
2651
  assistantMessage(content, toolCalls, producingModel, reasoningContent) {
2950
2652
  const msg = { role: "assistant", content };
2951
2653
  if (toolCalls.length > 0) msg.tool_calls = toolCalls;
@@ -2954,13 +2656,7 @@ ${summary}`;
2954
2656
  }
2955
2657
  return msg;
2956
2658
  }
2957
- /**
2958
- * Synthetic assistant message (abort notices, future system injections)
2959
- * — no real API round trip. Delegates to {@link assistantMessage} with
2960
- * `this.model` as the stand-in producer, so the same thinking-mode
2961
- * invariant applies: reasoner sessions get an empty-string
2962
- * `reasoning_content`; V3 sessions get nothing.
2963
- */
2659
+ /** Abort notices etc — uses this.model as stand-in producer for the thinking-mode stamp. */
2964
2660
  syntheticAssistantMessage(content) {
2965
2661
  return this.assistantMessage(content, [], this.model, "");
2966
2662
  }
@@ -3593,11 +3289,7 @@ var SkillStore = class {
3593
3289
  hasProjectScope() {
3594
3290
  return this.projectRoot !== void 0;
3595
3291
  }
3596
- /**
3597
- * Root directories scanned, in priority order. Project scope first
3598
- * so a per-repo skill overrides a global one with the same name —
3599
- * users expect the local copy to win when both exist.
3600
- */
3292
+ /** Project scope first so per-repo skill overrides a global with the same name. */
3601
3293
  roots() {
3602
3294
  const out = [];
3603
3295
  if (this.projectRoot) {
@@ -3609,11 +3301,7 @@ var SkillStore = class {
3609
3301
  out.push({ dir: join6(this.homeDir, ".reasonix", SKILLS_DIRNAME), scope: "global" });
3610
3302
  return out;
3611
3303
  }
3612
- /**
3613
- * List every skill visible to this store. On name collisions the
3614
- * higher-priority root (project over global over builtin) wins.
3615
- * Sorted by name for stable prefix hashing.
3616
- */
3304
+ /** Higher-priority root wins on collision (project > global > builtin); sorted for stable prefix hash. */
3617
3305
  list() {
3618
3306
  const byName = /* @__PURE__ */ new Map();
3619
3307
  for (const { dir, scope } of this.roots()) {
@@ -4000,10 +3688,6 @@ var MemoryStore = class {
4000
3688
  hasProjectScope() {
4001
3689
  return this.projectRoot !== void 0;
4002
3690
  }
4003
- /**
4004
- * Read the `MEMORY.md` index for a scope. Returns post-cap content
4005
- * (with a truncation marker if clipped), or `null` when absent / empty.
4006
- */
4007
3691
  loadIndex(scope) {
4008
3692
  if (scope === "project" && !this.projectRoot) return null;
4009
3693
  const file = join7(
@@ -4042,11 +3726,7 @@ var MemoryStore = class {
4042
3726
  createdAt: data.created ?? ""
4043
3727
  };
4044
3728
  }
4045
- /**
4046
- * List every memory in this store. Scans both scopes (skips project
4047
- * scope if unconfigured). Silently skips malformed files; the index
4048
- * must stay queryable even if one file is hand-edited into nonsense.
4049
- */
3729
+ /** Skips malformed files — index stays queryable even if one file is hand-edited into nonsense. */
4050
3730
  list() {
4051
3731
  const out = [];
4052
3732
  const scopes = this.projectRoot ? ["global", "project"] : ["global"];
@@ -4071,11 +3751,6 @@ var MemoryStore = class {
4071
3751
  }
4072
3752
  return out;
4073
3753
  }
4074
- /**
4075
- * Write a new memory (or overwrite existing). Creates the scope dir,
4076
- * writes the `.md` file, and regenerates `MEMORY.md`. Returns the
4077
- * absolute path written to.
4078
- */
4079
3754
  write(input) {
4080
3755
  if (input.scope === "project" && !this.projectRoot) {
4081
3756
  throw new Error("cannot write project-scoped memory: no projectRoot configured");
@@ -4111,12 +3786,7 @@ var MemoryStore = class {
4111
3786
  this.regenerateIndex(scope);
4112
3787
  return true;
4113
3788
  }
4114
- /**
4115
- * Rebuild `MEMORY.md` from the `.md` files currently in the scope dir.
4116
- * Called after every write/delete. Sorted by name for stable prefix
4117
- * hashing — two stores with the same set of files produce byte-identical
4118
- * MEMORY.md content, keeping the cache prefix reproducible.
4119
- */
3789
+ /** Sorted by name — same file set must produce byte-identical MEMORY.md for stable prefix hashing. */
4120
3790
  regenerateIndex(scope) {
4121
3791
  const dir = scopeDir({ homeDir: this.homeDir, scope, projectRoot: this.projectRoot });
4122
3792
  if (!existsSync7(dir)) return;
@@ -5052,12 +4722,6 @@ var PlanProposedError = class extends Error {
5052
4722
  this.steps = steps;
5053
4723
  this.summary = summary;
5054
4724
  }
5055
- /**
5056
- * Structured tool-result shape. Consumed by the TUI to extract the
5057
- * plan without regex-scraping the error message. Optional fields
5058
- * are omitted from the payload when absent so consumers don't see
5059
- * `undefined` keys in the JSON.
5060
- */
5061
4725
  toToolResult() {
5062
4726
  const payload = {
5063
4727
  error: `${this.name}: ${this.message}`,
@@ -5568,11 +5232,7 @@ var READY_SIGNALS = [
5568
5232
  var JobRegistry = class {
5569
5233
  jobs = /* @__PURE__ */ new Map();
5570
5234
  nextId = 1;
5571
- /**
5572
- * Spawn a background child. Resolves after `waitSec` OR on ready
5573
- * signal OR on early exit, whichever comes first. The child continues
5574
- * to run (and buffer output) regardless of which path fires.
5575
- */
5235
+ /** Resolves on (a) ready signal, (b) early exit, or (c) waitSec deadline — child keeps running regardless. */
5576
5236
  async start(command, opts) {
5577
5237
  const trimmed = command.trim();
5578
5238
  if (!trimmed) throw new Error("run_background: empty command");
@@ -5707,12 +5367,6 @@ ${job.output.slice(start)}`;
5707
5367
  exitCode: job.exitCode
5708
5368
  };
5709
5369
  }
5710
- /**
5711
- * Read a job's accumulated output. `since` lets a caller poll
5712
- * incrementally: pass the byte count returned from the last call to
5713
- * get only newly-written content. Returns both full output and a
5714
- * running snapshot so the caller can use whichever.
5715
- */
5716
5370
  read(id, opts = {}) {
5717
5371
  const job = this.jobs.get(id);
5718
5372
  if (!job) return null;
@@ -5736,11 +5390,7 @@ ${job.output.slice(start)}`;
5736
5390
  spawnError: job.spawnError
5737
5391
  };
5738
5392
  }
5739
- /**
5740
- * Send SIGTERM, wait `graceMs`, then SIGKILL if still alive. Returns
5741
- * the final job record (or null when the job id is unknown). Safe to
5742
- * call on an already-exited job — returns the record unchanged.
5743
- */
5393
+ /** SIGTERM, wait graceMs, then SIGKILL. Idempotent on already-exited jobs. */
5744
5394
  async stop(id, opts = {}) {
5745
5395
  const job = this.jobs.get(id);
5746
5396
  if (!job) return null;
@@ -5771,11 +5421,6 @@ ${job.output.slice(start)}`;
5771
5421
  list() {
5772
5422
  return [...this.jobs.values()].map(snapshot);
5773
5423
  }
5774
- /**
5775
- * Best-effort kill of every still-running job. Called on TUI shutdown
5776
- * so dev servers don't outlive the Reasonix process. Resolves after
5777
- * every child has closed or a hard deadline passes (3s total).
5778
- */
5779
5424
  async shutdown(deadlineMs = 5e3) {
5780
5425
  const start = Date.now();
5781
5426
  const runningJobs = [...this.jobs.values()].filter((j) => j.running && j.child);
@@ -7239,10 +6884,7 @@ var McpClient = class {
7239
6884
  get serverInstructions() {
7240
6885
  return this._instructions;
7241
6886
  }
7242
- /**
7243
- * Complete the initialize → initialized handshake. Must be called
7244
- * before any other method (otherwise compliant servers reject).
7245
- */
6887
+ /** Compliant servers reject other methods until this completes. */
7246
6888
  async initialize() {
7247
6889
  if (this.initialized) throw new Error("MCP client already initialized");
7248
6890
  this.startReaderIfNeeded();
@@ -7272,22 +6914,7 @@ var McpClient = class {
7272
6914
  this.assertInitialized();
7273
6915
  return this.request("tools/list", {});
7274
6916
  }
7275
- /**
7276
- * Invoke a tool by name. When `onProgress` is supplied, attaches a
7277
- * fresh progress token so the server can send incremental updates
7278
- * via `notifications/progress`; they're routed to the callback until
7279
- * the final response arrives (or the request times out, in which
7280
- * case the handler is simply dropped — no extra notification).
7281
- *
7282
- * When `signal` is supplied, aborting it:
7283
- * 1) fires `notifications/cancelled` to the server (MCP 2024-11-05
7284
- * way of saying "forget this request, I no longer care"), and
7285
- * 2) rejects the pending promise immediately with an AbortError,
7286
- * so the caller doesn't have to wait for the subprocess to
7287
- * finish its in-flight file write or network request.
7288
- * The server MAY still emit a late response; we drop it in dispatch
7289
- * since the request id is gone from `pending`.
7290
- */
6917
+ /** Abort sends `notifications/cancelled` and rejects immediately; late server responses are dropped. */
7291
6918
  async callTool(name, args, opts = {}) {
7292
6919
  this.assertInitialized();
7293
6920
  const params = { name, arguments: args ?? {} };
@@ -7303,13 +6930,7 @@ var McpClient = class {
7303
6930
  if (token !== void 0) this.progressHandlers.delete(token);
7304
6931
  }
7305
6932
  }
7306
- /**
7307
- * List resources the server exposes. Supports a pagination cursor;
7308
- * callers interested in the full set should loop on `nextCursor`.
7309
- * Servers that don't support resources respond with method-not-found
7310
- * (−32601) — we surface that as a thrown Error so callers can gate
7311
- * on the `serverCapabilities.resources` field first.
7312
- */
6933
+ /** Throws on method-not-found; callers should gate on `serverCapabilities.resources` first. */
7313
6934
  async listResources(cursor) {
7314
6935
  this.assertInitialized();
7315
6936
  return this.request("resources/list", {
@@ -7330,11 +6951,6 @@ var McpClient = class {
7330
6951
  ...cursor ? { cursor } : {}
7331
6952
  });
7332
6953
  }
7333
- /**
7334
- * Fetch a rendered prompt by name. `args` supplies values for any
7335
- * required template arguments; the server validates. Returns messages
7336
- * ready to prepend to the model's input.
7337
- */
7338
6954
  async getPrompt(name, args) {
7339
6955
  this.assertInitialized();
7340
6956
  return this.request("prompts/get", {
@@ -7351,7 +6967,6 @@ var McpClient = class {
7351
6967
  this.pending.clear();
7352
6968
  await this.transport.close();
7353
6969
  }
7354
- // ---------- internals ----------
7355
6970
  assertInitialized() {
7356
6971
  if (!this.initialized) throw new Error("MCP client not initialized \u2014 call initialize() first");
7357
6972
  }
@@ -7619,7 +7234,6 @@ var SseTransport = class {
7619
7234
  } catch {
7620
7235
  }
7621
7236
  }
7622
- // ---------- internals ----------
7623
7237
  async runStream() {
7624
7238
  let res;
7625
7239
  try {
@@ -7811,7 +7425,6 @@ var StreamableHttpTransport = class {
7811
7425
  getSessionId() {
7812
7426
  return this.sessionId;
7813
7427
  }
7814
- // ---------- internals ----------
7815
7428
  async consumeStream(body) {
7816
7429
  const parser = createParser3({
7817
7430
  onEvent: (ev) => {