reasonix 0.12.19 → 0.12.20

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/cli/index.js CHANGED
@@ -548,8 +548,8 @@ var defaultSelector = (samples) => {
548
548
  })[0];
549
549
  };
550
550
  async function runBranches(client, request, opts = {}) {
551
- const budget = Math.max(1, opts.budget ?? 1);
552
- const temperatures = resolveTemperatures(budget, opts.temperatures);
551
+ const budget2 = Math.max(1, opts.budget ?? 1);
552
+ const temperatures = resolveTemperatures(budget2, opts.temperatures);
553
553
  const selector = opts.selector ?? defaultSelector;
554
554
  const samples = await Promise.all(
555
555
  temperatures.map(async (temperature, index) => {
@@ -586,12 +586,12 @@ function aggregateBranchUsage(samples) {
586
586
  promptCacheMissTokens
587
587
  };
588
588
  }
589
- function resolveTemperatures(budget, custom) {
590
- if (custom && custom.length >= budget) return [...custom.slice(0, budget)];
591
- if (budget === 1) return [0];
589
+ function resolveTemperatures(budget2, custom) {
590
+ if (custom && custom.length >= budget2) return [...custom.slice(0, budget2)];
591
+ if (budget2 === 1) return [0];
592
592
  const out = [];
593
- for (let i = 0; i < budget; i++) {
594
- out.push(Number((i / (budget - 1)).toFixed(2)));
593
+ for (let i = 0; i < budget2; i++) {
594
+ out.push(Number((i / (budget2 - 1)).toFixed(2)));
595
595
  }
596
596
  return out;
597
597
  }
@@ -1276,29 +1276,29 @@ function truncateForModelByTokens(s, maxTokens) {
1276
1276
 
1277
1277
  ${tail}`;
1278
1278
  }
1279
- function sizePrefixToTokens(s, budget) {
1280
- if (budget <= 0 || s.length === 0) return "";
1281
- let size = Math.min(s.length, budget * 4);
1279
+ function sizePrefixToTokens(s, budget2) {
1280
+ if (budget2 <= 0 || s.length === 0) return "";
1281
+ let size = Math.min(s.length, budget2 * 4);
1282
1282
  for (let iter = 0; iter < 6; iter++) {
1283
1283
  if (size <= 0) return "";
1284
1284
  const slice = s.slice(0, size);
1285
1285
  const count = countTokens(slice);
1286
- if (count <= budget) return slice;
1287
- const next = Math.floor(size * (budget / count) * 0.95);
1286
+ if (count <= budget2) return slice;
1287
+ const next = Math.floor(size * (budget2 / count) * 0.95);
1288
1288
  if (next >= size) return s.slice(0, Math.max(0, size - 1));
1289
1289
  size = next;
1290
1290
  }
1291
1291
  return s.slice(0, Math.max(0, size));
1292
1292
  }
1293
- function sizeSuffixToTokens(s, budget) {
1294
- if (budget <= 0 || s.length === 0) return "";
1295
- let size = Math.min(s.length, budget * 4);
1293
+ function sizeSuffixToTokens(s, budget2) {
1294
+ if (budget2 <= 0 || s.length === 0) return "";
1295
+ let size = Math.min(s.length, budget2 * 4);
1296
1296
  for (let iter = 0; iter < 6; iter++) {
1297
1297
  if (size <= 0) return "";
1298
1298
  const slice = s.slice(-size);
1299
1299
  const count = countTokens(slice);
1300
- if (count <= budget) return slice;
1301
- const next = Math.floor(size * (budget / count) * 0.95);
1300
+ if (count <= budget2) return slice;
1301
+ const next = Math.floor(size * (budget2 / count) * 0.95);
1302
1302
  if (next >= size) return s.slice(-Math.max(0, size - 1));
1303
1303
  size = next;
1304
1304
  }
@@ -1999,6 +1999,19 @@ var CacheFirstLoop = class {
1999
1999
  * flip it live alongside `model`.
2000
2000
  */
2001
2001
  autoEscalate = true;
2002
+ /**
2003
+ * Soft USD budget — see {@link CacheFirstLoopOptions.budgetUsd}.
2004
+ * Mutable so `/budget` slash can set / change / clear it mid-session.
2005
+ * `null` (the default) disables all budget checks.
2006
+ */
2007
+ budgetUsd;
2008
+ /**
2009
+ * Set the first time a turn crosses 80% of the budget so the warning
2010
+ * doesn't repeat every turn afterwards. Cleared by `setBudget` (any
2011
+ * change re-arms the warning, including raising the cap above the
2012
+ * current spend).
2013
+ */
2014
+ _budgetWarned = false;
2002
2015
  sessionName;
2003
2016
  /**
2004
2017
  * Hook list, mutable so `/hooks reload` can swap it without
@@ -2064,6 +2077,7 @@ var CacheFirstLoop = class {
2064
2077
  this.model = opts.model ?? "deepseek-v4-flash";
2065
2078
  this.reasoningEffort = opts.reasoningEffort ?? "max";
2066
2079
  if (opts.autoEscalate !== void 0) this.autoEscalate = opts.autoEscalate;
2080
+ this.budgetUsd = typeof opts.budgetUsd === "number" && opts.budgetUsd > 0 ? opts.budgetUsd : null;
2067
2081
  this.maxToolIters = opts.maxToolIters ?? 64;
2068
2082
  this.hooks = opts.hooks ?? [];
2069
2083
  this.hookCwd = opts.hookCwd ?? process.cwd();
@@ -2303,6 +2317,16 @@ var CacheFirstLoop = class {
2303
2317
  }
2304
2318
  this.stream = this.branchEnabled ? false : this._streamPreference;
2305
2319
  }
2320
+ /**
2321
+ * Set / change / clear the soft USD budget. `null` (or any non-
2322
+ * positive number) disables the cap entirely. Re-arms the 80%
2323
+ * warning so a user who bumps the cap mid-session sees a fresh
2324
+ * threshold message at the new boundary.
2325
+ */
2326
+ setBudget(usd) {
2327
+ this.budgetUsd = typeof usd === "number" && usd > 0 ? usd : null;
2328
+ this._budgetWarned = false;
2329
+ }
2306
2330
  /**
2307
2331
  * Arm pro for the next turn (consumed at turn start). Called by
2308
2332
  * `/pro`. Idempotent — repeated calls stay armed, `disarmPro()`
@@ -2464,6 +2488,26 @@ var CacheFirstLoop = class {
2464
2488
  return userText;
2465
2489
  }
2466
2490
  async *step(userInput) {
2491
+ if (this.budgetUsd !== null) {
2492
+ const spent = this.stats.totalCost;
2493
+ if (spent >= this.budgetUsd) {
2494
+ yield {
2495
+ turn: this._turn,
2496
+ role: "error",
2497
+ content: "",
2498
+ error: `session budget exhausted \u2014 spent $${spent.toFixed(4)} \u2265 cap $${this.budgetUsd.toFixed(2)}. Bump the cap with /budget <usd>, clear it with /budget off, or end the session.`
2499
+ };
2500
+ return;
2501
+ }
2502
+ if (!this._budgetWarned && spent >= this.budgetUsd * 0.8) {
2503
+ this._budgetWarned = true;
2504
+ yield {
2505
+ turn: this._turn,
2506
+ role: "warning",
2507
+ content: `\u25B2 budget 80% used \u2014 $${spent.toFixed(4)} of $${this.budgetUsd.toFixed(2)}. Next turn or two likely trips the cap.`
2508
+ };
2509
+ }
2510
+ }
2467
2511
  this._turn++;
2468
2512
  this.scratch.reset();
2469
2513
  this.repair.resetStorm();
@@ -2560,14 +2604,14 @@ var CacheFirstLoop = class {
2560
2604
  let preHarvestedPlanState;
2561
2605
  try {
2562
2606
  if (this.branchEnabled) {
2563
- const budget = this.branchOptions.budget ?? 1;
2607
+ const budget2 = this.branchOptions.budget ?? 1;
2564
2608
  yield {
2565
2609
  turn: this._turn,
2566
2610
  role: "branch_start",
2567
2611
  content: "",
2568
2612
  branchProgress: {
2569
2613
  completed: 0,
2570
- total: budget,
2614
+ total: budget2,
2571
2615
  latestIndex: -1,
2572
2616
  latestTemperature: -1,
2573
2617
  latestUncertainties: -1
@@ -2601,7 +2645,7 @@ var CacheFirstLoop = class {
2601
2645
  onSampleDone
2602
2646
  }
2603
2647
  );
2604
- for (let k = 0; k < budget; k++) {
2648
+ for (let k = 0; k < budget2; k++) {
2605
2649
  const sample = queue.shift() ?? await new Promise((resolve13) => {
2606
2650
  waiter = resolve13;
2607
2651
  });
@@ -2611,7 +2655,7 @@ var CacheFirstLoop = class {
2611
2655
  content: "",
2612
2656
  branchProgress: {
2613
2657
  completed: k + 1,
2614
- total: budget,
2658
+ total: budget2,
2615
2659
  latestIndex: sample.index,
2616
2660
  latestTemperature: sample.temperature,
2617
2661
  latestUncertainties: sample.planState.uncertainties.length
@@ -11401,13 +11445,13 @@ var MIN_DIFF_ROWS = 8;
11401
11445
  function EditConfirm({ block, onChoose }) {
11402
11446
  const { stdout: stdout3 } = useStdout2();
11403
11447
  const rows = stdout3?.rows ?? 40;
11404
- const budget = Math.max(MIN_DIFF_ROWS, rows - MODAL_OVERHEAD_ROWS);
11448
+ const budget2 = Math.max(MIN_DIFF_ROWS, rows - MODAL_OVERHEAD_ROWS);
11405
11449
  const allLines = useMemo(
11406
11450
  () => formatEditBlockDiff(block, { contextLines: 2, maxLines: 1e5, indent: " " }),
11407
11451
  [block]
11408
11452
  );
11409
11453
  const [scroll, setScroll] = useState2(0);
11410
- const maxScroll = Math.max(0, allLines.length - budget);
11454
+ const maxScroll = Math.max(0, allLines.length - budget2);
11411
11455
  const effectiveScroll = Math.min(scroll, maxScroll);
11412
11456
  useKeystroke((ev) => {
11413
11457
  if (ev.paste) return;
@@ -11438,11 +11482,11 @@ function EditConfirm({ block, onChoose }) {
11438
11482
  return;
11439
11483
  }
11440
11484
  if (key.pageDown || input === " " || input === "f") {
11441
- setScroll((s) => Math.min(maxScroll, s + Math.max(1, budget - 2)));
11485
+ setScroll((s) => Math.min(maxScroll, s + Math.max(1, budget2 - 2)));
11442
11486
  return;
11443
11487
  }
11444
11488
  if (key.pageUp || input === "b") {
11445
- setScroll((s) => Math.max(0, s - Math.max(1, budget - 2)));
11489
+ setScroll((s) => Math.max(0, s - Math.max(1, budget2 - 2)));
11446
11490
  return;
11447
11491
  }
11448
11492
  if (input === "g") {
@@ -11458,9 +11502,9 @@ function EditConfirm({ block, onChoose }) {
11458
11502
  const removed = isNew ? 0 : (block.search.match(/\n/g)?.length ?? 0) + 1;
11459
11503
  const added = block.replace === "" ? 0 : (block.replace.match(/\n/g)?.length ?? 0) + 1;
11460
11504
  const tag = isNew ? "NEW" : "EDIT";
11461
- const visibleLines = allLines.slice(effectiveScroll, effectiveScroll + budget);
11505
+ const visibleLines = allLines.slice(effectiveScroll, effectiveScroll + budget2);
11462
11506
  const hiddenAbove = effectiveScroll;
11463
- const hiddenBelow = Math.max(0, allLines.length - effectiveScroll - budget);
11507
+ const hiddenBelow = Math.max(0, allLines.length - effectiveScroll - budget2);
11464
11508
  const totalLines = allLines.length;
11465
11509
  const showScrollHud = hiddenAbove + hiddenBelow > 0;
11466
11510
  const subtitleParts = [`-${removed} +${added} lines`];
@@ -13547,13 +13591,13 @@ function buildViewport(line, cursorCol, visibleCells, pastes) {
13547
13591
  return clipAroundCursor(line, cursorCol, visibleCells, pastes);
13548
13592
  }
13549
13593
  function clipFromLeft(line, visibleCells, pastes) {
13550
- const budget = Math.max(1, visibleCells - 1);
13594
+ const budget2 = Math.max(1, visibleCells - 1);
13551
13595
  let used = 0;
13552
13596
  let end = 0;
13553
13597
  while (end < line.length) {
13554
13598
  const ch = line[end];
13555
13599
  const cw = charCellsAt(line, end, pastes);
13556
- if (used + cw > budget) break;
13600
+ if (used + cw > budget2) break;
13557
13601
  used += cw;
13558
13602
  end++;
13559
13603
  }
@@ -13561,10 +13605,10 @@ function clipFromLeft(line, visibleCells, pastes) {
13561
13605
  return { segments, cursorCell: null, hiddenLeft: false, hiddenRight: end < line.length };
13562
13606
  }
13563
13607
  function clipAroundCursor(line, cursorCol, visibleCells, pastes) {
13564
- let budget = visibleCells;
13608
+ let budget2 = visibleCells;
13565
13609
  const reservedForMarkers = 2;
13566
- budget = Math.max(1, budget - reservedForMarkers);
13567
- const halfBudget = Math.floor(budget / 2);
13610
+ budget2 = Math.max(1, budget2 - reservedForMarkers);
13611
+ const halfBudget = Math.floor(budget2 / 2);
13568
13612
  let start = cursorCol;
13569
13613
  let leftCells = 0;
13570
13614
  while (start > 0 && leftCells < halfBudget) {
@@ -13573,7 +13617,7 @@ function clipAroundCursor(line, cursorCol, visibleCells, pastes) {
13573
13617
  start--;
13574
13618
  leftCells += cw;
13575
13619
  }
13576
- const rightBudget = budget - leftCells;
13620
+ const rightBudget = budget2 - leftCells;
13577
13621
  let end = cursorCol;
13578
13622
  let rightCells = 0;
13579
13623
  const cursorChar = cursorCol < line.length ? charCellsAt(line, cursorCol, pastes) : 1;
@@ -14080,7 +14124,8 @@ function StatsPanel({
14080
14124
  busy,
14081
14125
  proArmed,
14082
14126
  escalated,
14083
- dashboardUrl
14127
+ dashboardUrl,
14128
+ budgetUsd
14084
14129
  }) {
14085
14130
  const branchOn = (branchBudget ?? 1) > 1;
14086
14131
  const ctxMax = DEEPSEEK_CONTEXT_TOKENS[model2] ?? DEFAULT_CONTEXT_TOKENS;
@@ -14126,7 +14171,12 @@ function StatsPanel({
14126
14171
  balance,
14127
14172
  coldStart
14128
14173
  }
14129
- ));
14174
+ ), budgetUsd !== null && budgetUsd !== void 0 ? /* @__PURE__ */ React21.createElement(BudgetRow, { spent: summary.totalCostUsd, cap: budgetUsd }) : null);
14175
+ }
14176
+ function BudgetRow({ spent, cap }) {
14177
+ const pct2 = Math.max(0, spent / cap * 100);
14178
+ const color = pct2 >= 100 ? "#f87171" : pct2 >= 80 ? "#fbbf24" : "#94a3b8";
14179
+ return /* @__PURE__ */ React21.createElement(Box19, null, /* @__PURE__ */ React21.createElement(Text17, { dimColor: true }, " budget "), /* @__PURE__ */ React21.createElement(Text17, { color }, `$${spent.toFixed(4)} / $${cap.toFixed(2)}`, /* @__PURE__ */ React21.createElement(Text17, { dimColor: true }, ` (${pct2.toFixed(0)}%)`)));
14130
14180
  }
14131
14181
  function Header({
14132
14182
  model: model2,
@@ -14828,6 +14878,12 @@ var SLASH_COMMANDS = [
14828
14878
  summary: "arm v4-pro for the NEXT turn only (one-shot \xB7 auto-disarms after turn)",
14829
14879
  argCompleter: ["off"]
14830
14880
  },
14881
+ {
14882
+ cmd: "budget",
14883
+ argsHint: "[usd|off]",
14884
+ summary: "session USD cap \u2014 warns at 80%, refuses next turn at 100%. Off by default. /budget alone shows status",
14885
+ argCompleter: ["off", "1", "5", "10", "20", "50"]
14886
+ },
14831
14887
  { cmd: "mcp", summary: "list MCP servers + tools attached to this session" },
14832
14888
  {
14833
14889
  cmd: "resource",
@@ -16317,6 +16373,42 @@ var pro = (args, loop2, ctx) => {
16317
16373
  };
16318
16374
  };
16319
16375
  var ESCALATION_MODEL_ID = "deepseek-v4-pro";
16376
+ var budget = (args, loop2) => {
16377
+ const arg = args[0]?.trim() ?? "";
16378
+ if (arg === "") {
16379
+ if (loop2.budgetUsd === null) {
16380
+ return {
16381
+ info: "no session budget set \u2014 Reasonix will keep going until you stop it. Set one with: /budget <usd> (e.g. /budget 5)"
16382
+ };
16383
+ }
16384
+ const spent2 = loop2.stats.totalCost;
16385
+ const pct2 = spent2 / loop2.budgetUsd * 100;
16386
+ return {
16387
+ info: `budget: $${spent2.toFixed(4)} of $${loop2.budgetUsd.toFixed(2)} (${pct2.toFixed(1)}%) \xB7 /budget off to clear, /budget <usd> to change`
16388
+ };
16389
+ }
16390
+ if (arg === "off" || arg === "none" || arg === "0") {
16391
+ loop2.setBudget(null);
16392
+ return { info: "budget \u2192 off (no cap)" };
16393
+ }
16394
+ const cleaned = arg.replace(/^\$/, "");
16395
+ const usd = Number(cleaned);
16396
+ if (!Number.isFinite(usd) || usd <= 0) {
16397
+ return {
16398
+ info: `usage: /budget <usd> (got "${arg}" \u2014 must be a positive number, e.g. /budget 5 or /budget 12.50)`
16399
+ };
16400
+ }
16401
+ loop2.setBudget(usd);
16402
+ const spent = loop2.stats.totalCost;
16403
+ if (spent >= usd) {
16404
+ return {
16405
+ info: `\u25B2 budget \u2192 $${usd.toFixed(2)} but already spent $${spent.toFixed(4)}. Next turn will be refused \u2014 bump the cap higher to keep going, or end the session.`
16406
+ };
16407
+ }
16408
+ return {
16409
+ info: `budget \u2192 $${usd.toFixed(2)} (so far: $${spent.toFixed(4)} \xB7 warns at 80%, refuses next turn at 100% \xB7 /budget off to clear)`
16410
+ };
16411
+ };
16320
16412
  var handlers9 = {
16321
16413
  model,
16322
16414
  models,
@@ -16324,7 +16416,8 @@ var handlers9 = {
16324
16416
  preset,
16325
16417
  branch,
16326
16418
  effort,
16327
- pro
16419
+ pro,
16420
+ budget
16328
16421
  };
16329
16422
 
16330
16423
  // src/cli/ui/slash/handlers/observability.ts
@@ -17577,6 +17670,7 @@ function App({
17577
17670
  transcript,
17578
17671
  harvest: harvest3,
17579
17672
  branch: branch2,
17673
+ budgetUsd,
17580
17674
  session,
17581
17675
  tools,
17582
17676
  mcpSpecs,
@@ -17790,6 +17884,7 @@ function App({
17790
17884
  model: model2,
17791
17885
  harvest: harvest3,
17792
17886
  branch: branch2,
17887
+ budgetUsd,
17793
17888
  session,
17794
17889
  hooks: hookList,
17795
17890
  hookCwd: currentRootDir,
@@ -17800,7 +17895,7 @@ function App({
17800
17895
  });
17801
17896
  loopRef.current = l;
17802
17897
  return l;
17803
- }, [model2, system, harvest3, branch2, session, tools, codeMode]);
17898
+ }, [model2, system, harvest3, branch2, budgetUsd, session, tools, codeMode]);
17804
17899
  useEffect6(() => {
17805
17900
  loop2.hooks = hookList;
17806
17901
  }, [loop2, hookList]);
@@ -20045,7 +20140,8 @@ Continue executing from the next pending step. Call mark_step_complete after eac
20045
20140
  updateAvailable,
20046
20141
  proArmed,
20047
20142
  escalated: turnOnPro,
20048
- dashboardUrl
20143
+ dashboardUrl,
20144
+ budgetUsd: loop2.budgetUsd
20049
20145
  }
20050
20146
  ), /* @__PURE__ */ React24.createElement(Static, { items: historical }, (item) => /* @__PURE__ */ React24.createElement(EventRow, { key: item.id, event: item, projectRoot: currentRootDir })), !historical.some((e) => e.role === "user" || e.role === "assistant") && !busy && !streaming ? /* @__PURE__ */ React24.createElement(WelcomeBanner, { inCodeMode: !!codeMode }) : null, !PLAIN_UI && !pendingShell && !pendingWorkspace && !pendingPlan && !stagedInput && !pendingEditReview && !pendingCheckpoint && !stagedCheckpointRevise && streaming ? /* @__PURE__ */ React24.createElement(Box22, { marginY: 1 }, /* @__PURE__ */ React24.createElement(EventRow, { event: streaming, projectRoot: currentRootDir })) : null, !PLAIN_UI && !pendingShell && !pendingWorkspace && !pendingPlan && !stagedInput && !pendingEditReview && !pendingCheckpoint && !stagedCheckpointRevise && ongoingTool ? /* @__PURE__ */ React24.createElement(OngoingToolRow, { tool: ongoingTool, progress: toolProgress }) : null, !PLAIN_UI && !pendingShell && !pendingWorkspace && !pendingPlan && !stagedInput && !pendingEditReview && !pendingCheckpoint && !stagedCheckpointRevise && subagentActivity ? /* @__PURE__ */ React24.createElement(SubagentRow, { activity: subagentActivity }) : null, !PLAIN_UI && !pendingShell && !pendingWorkspace && !pendingPlan && !stagedInput && !pendingEditReview && !pendingCheckpoint && !stagedCheckpointRevise && !ongoingTool && statusLine ? /* @__PURE__ */ React24.createElement(StatusRow, { text: statusLine }) : null, !PLAIN_UI && undoBanner && !pendingShell && !pendingWorkspace && !pendingPlan && !stagedInput && !pendingEditReview && !pendingCheckpoint && !stagedCheckpointRevise && !pendingChoice && !stagedChoiceCustom && !pendingRevision ? /* @__PURE__ */ React24.createElement(UndoBanner, { banner: undoBanner }) : null, !PLAIN_UI && !pendingShell && !pendingWorkspace && !pendingPlan && !stagedInput && !pendingEditReview && !pendingCheckpoint && !stagedCheckpointRevise && busy && !streaming && !ongoingTool && !statusLine ? /* @__PURE__ */ React24.createElement(StatusRow, { text: "processing\u2026" }) : null, stagedInput ? /* @__PURE__ */ React24.createElement(
20051
20147
  PlanRefineInput,
@@ -20316,6 +20412,7 @@ function Root({
20316
20412
  transcript: appProps.transcript,
20317
20413
  harvest: appProps.harvest,
20318
20414
  branch: appProps.branch,
20415
+ budgetUsd: appProps.budgetUsd,
20319
20416
  session: appProps.session,
20320
20417
  tools,
20321
20418
  mcpSpecs,
@@ -20477,6 +20574,7 @@ async function codeCommand(opts = {}) {
20477
20574
  await chatCommand({
20478
20575
  model: opts.model ?? "deepseek-v4-flash",
20479
20576
  harvest: opts.harvest ?? false,
20577
+ budgetUsd: opts.budgetUsd,
20480
20578
  system: codeSystemPrompt2(rootDir, { hasSemanticSearch: semantic2.enabled }),
20481
20579
  transcript: opts.transcript,
20482
20580
  session,
@@ -21213,7 +21311,8 @@ async function runCommand2(opts) {
21213
21311
  tools,
21214
21312
  model: opts.model,
21215
21313
  harvest: opts.harvest,
21216
- branch: opts.branch
21314
+ branch: opts.branch,
21315
+ budgetUsd: opts.budgetUsd
21217
21316
  });
21218
21317
  const prefixHash = prefix.fingerprint;
21219
21318
  let transcriptStream = null;
@@ -21769,6 +21868,17 @@ Your training data has a cutoff. When an answer's correctness depends on somethi
21769
21868
  The signal isn't a topic list \u2014 it's: "if I'm wrong about this, is it because reality moved on?". If yes, ground the answer in fresh evidence; if no (definitions, mechanisms, well-established APIs), answer from memory.
21770
21869
 
21771
21870
  ${ESCALATION_CONTRACT}`;
21871
+ function parseBudgetFlag(raw) {
21872
+ if (raw === void 0) return void 0;
21873
+ if (!Number.isFinite(raw) || raw <= 0) {
21874
+ process.stderr.write(
21875
+ `\u25B2 ignoring --budget=${raw} (must be a positive number) \u2014 running with no cap
21876
+ `
21877
+ );
21878
+ return void 0;
21879
+ }
21880
+ return raw;
21881
+ }
21772
21882
  var program = new Command();
21773
21883
  program.name("reasonix").description("DeepSeek-native agent framework \u2014 built for cache hits and cheap tokens.").version(VERSION).option(
21774
21884
  "-c, --continue",
@@ -21806,6 +21916,10 @@ program.command("code [dir]").description(
21806
21916
  ).option("-m, --model <id>", "Override default model (v4-flash)").option("--no-session", "Disable session persistence for this run").option("-r, --resume", "Skip the session picker \u2014 always continue prior messages").option("-n, --new", "Skip the session picker \u2014 always wipe prior messages and start fresh").option("--transcript <path>", "Write a JSONL transcript to this path").option(
21807
21917
  "--harvest",
21808
21918
  "Opt-in Pillar-2 plan-state extraction. Adds one flash call per turn; off by default (no preset enables it)."
21919
+ ).option(
21920
+ "--budget <usd>",
21921
+ "Soft USD cap on session spend. Off by default. Warns at 80%, refuses next turn at 100%. Mid-session: /budget <usd> or /budget off.",
21922
+ (v) => Number.parseFloat(v)
21809
21923
  ).option(
21810
21924
  "--no-dashboard",
21811
21925
  "Suppress the auto-launched embedded web dashboard. Default behavior boots it on TUI mount and shows the URL in the status bar (clickable in OSC-8-aware terminals)."
@@ -21818,6 +21932,7 @@ program.command("code [dir]").description(
21818
21932
  forceResume: !!opts.resume,
21819
21933
  forceNew: !!opts.new,
21820
21934
  harvest: !!opts.harvest,
21935
+ budgetUsd: parseBudgetFlag(opts.budget),
21821
21936
  noDashboard: opts.dashboard === false
21822
21937
  });
21823
21938
  });
@@ -21831,6 +21946,10 @@ program.command("chat").description("Interactive Ink TUI with live cache/cost pa
21831
21946
  "--branch <n>",
21832
21947
  "Self-consistency: run N parallel samples per turn (N\xD7 cost). Manual only \u2014 never auto-enabled.",
21833
21948
  (v) => Number.parseInt(v, 10)
21949
+ ).option(
21950
+ "--budget <usd>",
21951
+ "Soft USD cap on session spend. Off by default. Warns at 80%, refuses next turn at 100%. Mid-session: /budget <usd> or /budget off.",
21952
+ (v) => Number.parseFloat(v)
21834
21953
  ).option("--session <name>", "Use a named session (default: from config, usually 'default').").option("--no-session", "Disable session persistence for this run (ephemeral chat)").option("-r, --resume", "Skip the session picker \u2014 always continue prior messages").option(
21835
21954
  "-c, --continue",
21836
21955
  "Resume the most-recently-used session (any name) without showing the picker."
@@ -21868,6 +21987,7 @@ program.command("chat").description("Interactive Ink TUI with live cache/cost pa
21868
21987
  transcript: opts.transcript,
21869
21988
  harvest: defaults.harvest,
21870
21989
  branch: defaults.branch,
21990
+ budgetUsd: parseBudgetFlag(opts.budget),
21871
21991
  session: continueOpts.session,
21872
21992
  mcp: defaults.mcp,
21873
21993
  mcpPrefix: opts.mcpPrefix,
@@ -21880,6 +22000,10 @@ program.command("run <task>").description("Run a single task non-interactively,
21880
22000
  "--branch <n>",
21881
22001
  "Self-consistency: run N parallel samples per turn and pick the most confident",
21882
22002
  (v) => Number.parseInt(v, 10)
22003
+ ).option(
22004
+ "--budget <usd>",
22005
+ "Soft USD cap on session spend. Off by default. Refuses to start a new turn once cumulative cost \u2265 cap.",
22006
+ (v) => Number.parseFloat(v)
21883
22007
  ).option("--transcript <path>", "Write a JSONL transcript to this path for replay/diff").option(
21884
22008
  "--mcp <spec>",
21885
22009
  'MCP server spec; repeatable. "name=cmd args...", "cmd args...", or a URL (http/https \u2192 SSE).',
@@ -21903,6 +22027,7 @@ program.command("run <task>").description("Run a single task non-interactively,
21903
22027
  system: applyMemoryStack(opts.system, process.cwd()),
21904
22028
  harvest: defaults.harvest,
21905
22029
  branch: defaults.branch,
22030
+ budgetUsd: parseBudgetFlag(opts.budget),
21906
22031
  transcript: opts.transcript,
21907
22032
  mcp: defaults.mcp,
21908
22033
  mcpPrefix: opts.mcpPrefix