reasonix 0.0.4 → 0.0.6

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
@@ -1,11 +1,264 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- DeepSeekClient
4
- } from "./chunk-T2ODXAJP.js";
5
2
 
6
3
  // src/cli/index.ts
7
4
  import { Command } from "commander";
8
5
 
6
+ // src/client.ts
7
+ import { createParser } from "eventsource-parser";
8
+
9
+ // src/retry.ts
10
+ var DEFAULT_RETRYABLE_STATUSES = [408, 429, 500, 502, 503, 504];
11
+ async function fetchWithRetry(fetchFn, url, init, opts = {}) {
12
+ const maxAttempts = opts.maxAttempts ?? 4;
13
+ const initial = opts.initialBackoffMs ?? 500;
14
+ const cap = opts.maxBackoffMs ?? 1e4;
15
+ const retryable = new Set(opts.retryableStatuses ?? DEFAULT_RETRYABLE_STATUSES);
16
+ let lastError;
17
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
18
+ if (opts.signal?.aborted) throw new Error("aborted");
19
+ try {
20
+ const resp = await fetchFn(url, init);
21
+ if (resp.ok || !retryable.has(resp.status)) return resp;
22
+ if (attempt === maxAttempts - 1) return resp;
23
+ await resp.text().catch(() => void 0);
24
+ const waitMs = computeWait(attempt, initial, cap, resp.headers.get("Retry-After"));
25
+ opts.onRetry?.({ attempt: attempt + 1, reason: `http ${resp.status}`, waitMs });
26
+ await sleep(waitMs, opts.signal);
27
+ } catch (err) {
28
+ lastError = err;
29
+ if (isAbortError(err) || opts.signal?.aborted) throw err;
30
+ if (attempt === maxAttempts - 1) throw err;
31
+ const waitMs = computeWait(attempt, initial, cap, null);
32
+ opts.onRetry?.({
33
+ attempt: attempt + 1,
34
+ reason: `network: ${messageOf(err)}`,
35
+ waitMs
36
+ });
37
+ await sleep(waitMs, opts.signal);
38
+ }
39
+ }
40
+ throw lastError ?? new Error("fetchWithRetry: loop exited unexpectedly");
41
+ }
42
+ function computeWait(attempt, initial, cap, retryAfter) {
43
+ if (retryAfter) {
44
+ const seconds = Number.parseFloat(retryAfter);
45
+ if (Number.isFinite(seconds) && seconds > 0) {
46
+ return Math.min(seconds * 1e3, cap);
47
+ }
48
+ }
49
+ const exp = initial * 2 ** attempt;
50
+ const jitter = exp * (0.75 + Math.random() * 0.5);
51
+ return Math.min(Math.max(jitter, 0), cap);
52
+ }
53
+ function sleep(ms, signal) {
54
+ if (ms <= 0) return Promise.resolve();
55
+ return new Promise((resolve2, reject) => {
56
+ const timer = setTimeout(resolve2, ms);
57
+ if (signal) {
58
+ const onAbort = () => {
59
+ clearTimeout(timer);
60
+ reject(new Error("aborted"));
61
+ };
62
+ if (signal.aborted) onAbort();
63
+ else signal.addEventListener("abort", onAbort, { once: true });
64
+ }
65
+ });
66
+ }
67
+ function isAbortError(err) {
68
+ if (!err || typeof err !== "object") return false;
69
+ const name = err.name;
70
+ return name === "AbortError";
71
+ }
72
+ function messageOf(err) {
73
+ if (err instanceof Error) return err.message;
74
+ try {
75
+ return String(err);
76
+ } catch {
77
+ return "unknown error";
78
+ }
79
+ }
80
+
81
+ // src/client.ts
82
+ var Usage = class _Usage {
83
+ constructor(promptTokens = 0, completionTokens = 0, totalTokens = 0, promptCacheHitTokens = 0, promptCacheMissTokens = 0) {
84
+ this.promptTokens = promptTokens;
85
+ this.completionTokens = completionTokens;
86
+ this.totalTokens = totalTokens;
87
+ this.promptCacheHitTokens = promptCacheHitTokens;
88
+ this.promptCacheMissTokens = promptCacheMissTokens;
89
+ }
90
+ promptTokens;
91
+ completionTokens;
92
+ totalTokens;
93
+ promptCacheHitTokens;
94
+ promptCacheMissTokens;
95
+ get cacheHitRatio() {
96
+ const denom = this.promptCacheHitTokens + this.promptCacheMissTokens;
97
+ return denom > 0 ? this.promptCacheHitTokens / denom : 0;
98
+ }
99
+ static fromApi(raw) {
100
+ const u = raw ?? {};
101
+ return new _Usage(
102
+ u.prompt_tokens ?? 0,
103
+ u.completion_tokens ?? 0,
104
+ u.total_tokens ?? 0,
105
+ u.prompt_cache_hit_tokens ?? 0,
106
+ u.prompt_cache_miss_tokens ?? 0
107
+ );
108
+ }
109
+ };
110
+ var DeepSeekClient = class {
111
+ apiKey;
112
+ baseUrl;
113
+ timeoutMs;
114
+ retry;
115
+ _fetch;
116
+ constructor(opts = {}) {
117
+ const apiKey = opts.apiKey ?? process.env.DEEPSEEK_API_KEY;
118
+ if (!apiKey) {
119
+ throw new Error(
120
+ "DEEPSEEK_API_KEY is not set. Put it in .env or pass apiKey to DeepSeekClient."
121
+ );
122
+ }
123
+ this.apiKey = apiKey;
124
+ this.baseUrl = (opts.baseUrl ?? process.env.DEEPSEEK_BASE_URL ?? "https://api.deepseek.com").replace(/\/+$/, "");
125
+ this.timeoutMs = opts.timeoutMs ?? 12e4;
126
+ this._fetch = opts.fetch ?? globalThis.fetch.bind(globalThis);
127
+ this.retry = opts.retry ?? {};
128
+ }
129
+ buildPayload(opts, stream) {
130
+ const payload = {
131
+ model: opts.model,
132
+ messages: opts.messages,
133
+ stream
134
+ };
135
+ if (opts.tools?.length) payload.tools = opts.tools;
136
+ if (opts.temperature !== void 0) payload.temperature = opts.temperature;
137
+ if (opts.maxTokens !== void 0) payload.max_tokens = opts.maxTokens;
138
+ if (opts.responseFormat) payload.response_format = opts.responseFormat;
139
+ return payload;
140
+ }
141
+ async chat(opts) {
142
+ const ctrl = new AbortController();
143
+ const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
144
+ const signal = opts.signal ?? ctrl.signal;
145
+ try {
146
+ const resp = await fetchWithRetry(
147
+ this._fetch,
148
+ `${this.baseUrl}/chat/completions`,
149
+ {
150
+ method: "POST",
151
+ headers: {
152
+ Authorization: `Bearer ${this.apiKey}`,
153
+ "Content-Type": "application/json"
154
+ },
155
+ body: JSON.stringify(this.buildPayload(opts, false)),
156
+ signal
157
+ },
158
+ { ...this.retry, signal }
159
+ );
160
+ if (!resp.ok) {
161
+ throw new Error(`DeepSeek ${resp.status}: ${await resp.text()}`);
162
+ }
163
+ const data = await resp.json();
164
+ const choice = data.choices?.[0]?.message ?? {};
165
+ return {
166
+ content: choice.content ?? "",
167
+ reasoningContent: choice.reasoning_content ?? null,
168
+ toolCalls: choice.tool_calls ?? [],
169
+ usage: Usage.fromApi(data.usage),
170
+ raw: data
171
+ };
172
+ } finally {
173
+ clearTimeout(timer);
174
+ }
175
+ }
176
+ async *stream(opts) {
177
+ const ctrl = new AbortController();
178
+ const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
179
+ const signal = opts.signal ?? ctrl.signal;
180
+ let resp;
181
+ try {
182
+ resp = await fetchWithRetry(
183
+ this._fetch,
184
+ `${this.baseUrl}/chat/completions`,
185
+ {
186
+ method: "POST",
187
+ headers: {
188
+ Authorization: `Bearer ${this.apiKey}`,
189
+ "Content-Type": "application/json",
190
+ Accept: "text/event-stream"
191
+ },
192
+ body: JSON.stringify(this.buildPayload(opts, true)),
193
+ signal
194
+ },
195
+ { ...this.retry, signal }
196
+ );
197
+ } catch (err) {
198
+ clearTimeout(timer);
199
+ throw err;
200
+ }
201
+ if (!resp.ok || !resp.body) {
202
+ clearTimeout(timer);
203
+ throw new Error(`DeepSeek ${resp.status}: ${await resp.text().catch(() => "")}`);
204
+ }
205
+ const queue = [];
206
+ let done = false;
207
+ const parser = createParser({
208
+ onEvent: (ev) => {
209
+ if (!ev.data || ev.data === "[DONE]") {
210
+ done = true;
211
+ return;
212
+ }
213
+ try {
214
+ const json = JSON.parse(ev.data);
215
+ const delta = json.choices?.[0]?.delta ?? {};
216
+ const finishReason = json.choices?.[0]?.finish_reason ?? void 0;
217
+ const chunk = { raw: json, finishReason };
218
+ if (typeof delta.content === "string" && delta.content.length > 0) {
219
+ chunk.contentDelta = delta.content;
220
+ }
221
+ if (typeof delta.reasoning_content === "string" && delta.reasoning_content.length > 0) {
222
+ chunk.reasoningDelta = delta.reasoning_content;
223
+ }
224
+ if (Array.isArray(delta.tool_calls) && delta.tool_calls.length > 0) {
225
+ const tc = delta.tool_calls[0];
226
+ chunk.toolCallDelta = {
227
+ index: tc.index ?? 0,
228
+ id: tc.id,
229
+ name: tc.function?.name,
230
+ argumentsDelta: tc.function?.arguments
231
+ };
232
+ }
233
+ if (json.usage) {
234
+ chunk.usage = Usage.fromApi(json.usage);
235
+ }
236
+ queue.push(chunk);
237
+ } catch {
238
+ }
239
+ }
240
+ });
241
+ const reader = resp.body.getReader();
242
+ const decoder = new TextDecoder();
243
+ try {
244
+ while (true) {
245
+ if (queue.length > 0) {
246
+ yield queue.shift();
247
+ continue;
248
+ }
249
+ if (done) break;
250
+ const { value, done: streamDone } = await reader.read();
251
+ if (streamDone) break;
252
+ parser.feed(decoder.decode(value, { stream: true }));
253
+ }
254
+ while (queue.length > 0) yield queue.shift();
255
+ } finally {
256
+ clearTimeout(timer);
257
+ reader.releaseLock();
258
+ }
259
+ }
260
+ };
261
+
9
262
  // src/harvest.ts
10
263
  function emptyPlanState() {
11
264
  return { subgoals: [], hypotheses: [], uncertainties: [], rejectedPaths: [] };
@@ -96,6 +349,66 @@ function sanitizeArray(raw, maxItems, maxItemLen) {
96
349
  return out;
97
350
  }
98
351
 
352
+ // src/consistency.ts
353
+ var defaultSelector = (samples) => {
354
+ if (samples.length === 0) throw new Error("defaultSelector: samples is empty");
355
+ return samples.slice().sort((a, b) => {
356
+ const uDiff = a.planState.uncertainties.length - b.planState.uncertainties.length;
357
+ if (uDiff !== 0) return uDiff;
358
+ const aLen = a.response.content?.length ?? 0;
359
+ const bLen = b.response.content?.length ?? 0;
360
+ return aLen - bLen;
361
+ })[0];
362
+ };
363
+ async function runBranches(client, request, opts = {}) {
364
+ const budget = Math.max(1, opts.budget ?? 1);
365
+ const temperatures = resolveTemperatures(budget, opts.temperatures);
366
+ const selector = opts.selector ?? defaultSelector;
367
+ const samples = await Promise.all(
368
+ temperatures.map(async (temperature, index) => {
369
+ const response = await client.chat({ ...request, temperature });
370
+ const planState = await harvest(response.reasoningContent, client, opts.harvestOptions);
371
+ const sample = { index, temperature, response, planState };
372
+ try {
373
+ opts.onSampleDone?.(sample);
374
+ } catch {
375
+ }
376
+ return sample;
377
+ })
378
+ );
379
+ return { chosen: selector(samples), samples };
380
+ }
381
+ function aggregateBranchUsage(samples) {
382
+ let promptTokens = 0;
383
+ let completionTokens = 0;
384
+ let totalTokens = 0;
385
+ let promptCacheHitTokens = 0;
386
+ let promptCacheMissTokens = 0;
387
+ for (const s of samples) {
388
+ promptTokens += s.response.usage.promptTokens;
389
+ completionTokens += s.response.usage.completionTokens;
390
+ totalTokens += s.response.usage.totalTokens;
391
+ promptCacheHitTokens += s.response.usage.promptCacheHitTokens;
392
+ promptCacheMissTokens += s.response.usage.promptCacheMissTokens;
393
+ }
394
+ return {
395
+ promptTokens,
396
+ completionTokens,
397
+ totalTokens,
398
+ promptCacheHitTokens,
399
+ promptCacheMissTokens
400
+ };
401
+ }
402
+ function resolveTemperatures(budget, custom) {
403
+ if (custom && custom.length >= budget) return [...custom.slice(0, budget)];
404
+ if (budget === 1) return [0];
405
+ const out = [];
406
+ for (let i = 0; i < budget; i++) {
407
+ out.push(Number((i / (budget - 1)).toFixed(2)));
408
+ }
409
+ return out;
410
+ }
411
+
99
412
  // src/memory.ts
100
413
  import { createHash } from "crypto";
101
414
  var ImmutablePrefix = class {
@@ -470,6 +783,93 @@ function signature2(call) {
470
783
  return `${call.function?.name ?? ""}::${call.function?.arguments ?? ""}`;
471
784
  }
472
785
 
786
+ // src/session.ts
787
+ import {
788
+ appendFileSync,
789
+ chmodSync,
790
+ existsSync,
791
+ mkdirSync,
792
+ readFileSync,
793
+ readdirSync,
794
+ statSync,
795
+ unlinkSync
796
+ } from "fs";
797
+ import { homedir } from "os";
798
+ import { dirname, join } from "path";
799
+ function sessionsDir() {
800
+ return join(homedir(), ".reasonix", "sessions");
801
+ }
802
+ function sessionPath(name) {
803
+ return join(sessionsDir(), `${sanitizeName(name)}.jsonl`);
804
+ }
805
+ function sanitizeName(name) {
806
+ const cleaned = name.replace(/[^\w\-\u4e00-\u9fa5]/g, "_").slice(0, 64);
807
+ return cleaned || "default";
808
+ }
809
+ function loadSessionMessages(name) {
810
+ const path = sessionPath(name);
811
+ if (!existsSync(path)) return [];
812
+ try {
813
+ const raw = readFileSync(path, "utf8");
814
+ const out = [];
815
+ for (const line of raw.split(/\r?\n/)) {
816
+ const trimmed = line.trim();
817
+ if (!trimmed) continue;
818
+ try {
819
+ const msg = JSON.parse(trimmed);
820
+ if (msg && typeof msg === "object" && "role" in msg) out.push(msg);
821
+ } catch {
822
+ }
823
+ }
824
+ return out;
825
+ } catch {
826
+ return [];
827
+ }
828
+ }
829
+ function appendSessionMessage(name, message) {
830
+ const path = sessionPath(name);
831
+ mkdirSync(dirname(path), { recursive: true });
832
+ appendFileSync(path, `${JSON.stringify(message)}
833
+ `, "utf8");
834
+ try {
835
+ chmodSync(path, 384);
836
+ } catch {
837
+ }
838
+ }
839
+ function listSessions() {
840
+ const dir = sessionsDir();
841
+ if (!existsSync(dir)) return [];
842
+ try {
843
+ const files = readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
844
+ return files.map((file) => {
845
+ const path = join(dir, file);
846
+ const stat = statSync(path);
847
+ const name = file.replace(/\.jsonl$/, "");
848
+ const messageCount = countLines(path);
849
+ return { name, path, size: stat.size, messageCount, mtime: stat.mtime };
850
+ }).sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
851
+ } catch {
852
+ return [];
853
+ }
854
+ }
855
+ function deleteSession(name) {
856
+ const path = sessionPath(name);
857
+ try {
858
+ unlinkSync(path);
859
+ return true;
860
+ } catch {
861
+ return false;
862
+ }
863
+ }
864
+ function countLines(path) {
865
+ try {
866
+ const raw = readFileSync(path, "utf8");
867
+ return raw.split(/\r?\n/).filter((l) => l.trim()).length;
868
+ } catch {
869
+ return 0;
870
+ }
871
+ }
872
+
473
873
  // src/telemetry.ts
474
874
  var DEEPSEEK_PRICING = {
475
875
  "deepseek-chat": { inputCacheHit: 0.07, inputCacheMiss: 0.27, output: 1.1 },
@@ -613,27 +1013,92 @@ var CacheFirstLoop = class {
613
1013
  client;
614
1014
  prefix;
615
1015
  tools;
616
- model;
617
1016
  maxToolIters;
618
- stream;
619
- harvestEnabled;
620
- harvestOptions;
621
1017
  log = new AppendOnlyLog();
622
1018
  scratch = new VolatileScratch();
623
1019
  stats = new SessionStats();
624
1020
  repair;
1021
+ // Mutable via configure() — slash commands in the TUI / library callers tweak
1022
+ // these mid-session so users don't have to restart to try harvest or branch.
1023
+ model;
1024
+ stream;
1025
+ harvestEnabled;
1026
+ harvestOptions;
1027
+ branchEnabled;
1028
+ branchOptions;
1029
+ sessionName;
1030
+ /** Number of messages that were pre-loaded from the session file. */
1031
+ resumedMessageCount;
625
1032
  _turn = 0;
1033
+ _streamPreference;
626
1034
  constructor(opts) {
627
1035
  this.client = opts.client;
628
1036
  this.prefix = opts.prefix;
629
1037
  this.tools = opts.tools ?? new ToolRegistry();
630
1038
  this.model = opts.model ?? "deepseek-chat";
631
1039
  this.maxToolIters = opts.maxToolIters ?? 8;
632
- this.stream = opts.stream ?? true;
633
- this.harvestEnabled = opts.harvest === true || typeof opts.harvest === "object" && opts.harvest !== null;
634
- this.harvestOptions = typeof opts.harvest === "object" && opts.harvest !== null ? opts.harvest : {};
1040
+ if (typeof opts.branch === "number") {
1041
+ this.branchOptions = { budget: opts.branch };
1042
+ } else if (opts.branch && typeof opts.branch === "object") {
1043
+ this.branchOptions = opts.branch;
1044
+ } else {
1045
+ this.branchOptions = {};
1046
+ }
1047
+ this.branchEnabled = (this.branchOptions.budget ?? 1) > 1;
1048
+ const harvestForced = this.branchEnabled;
1049
+ this.harvestEnabled = harvestForced || opts.harvest === true || typeof opts.harvest === "object" && opts.harvest !== null;
1050
+ this.harvestOptions = typeof opts.harvest === "object" && opts.harvest !== null ? opts.harvest : this.branchOptions.harvestOptions ?? {};
1051
+ this._streamPreference = opts.stream ?? true;
1052
+ this.stream = this.branchEnabled ? false : this._streamPreference;
635
1053
  const allowedNames = /* @__PURE__ */ new Set([...this.prefix.toolSpecs.map((s) => s.function.name)]);
636
1054
  this.repair = new ToolCallRepair({ allowedToolNames: allowedNames });
1055
+ this.sessionName = opts.session ?? null;
1056
+ if (this.sessionName) {
1057
+ const prior = loadSessionMessages(this.sessionName);
1058
+ for (const msg of prior) this.log.append(msg);
1059
+ this.resumedMessageCount = prior.length;
1060
+ } else {
1061
+ this.resumedMessageCount = 0;
1062
+ }
1063
+ }
1064
+ appendAndPersist(message) {
1065
+ this.log.append(message);
1066
+ if (this.sessionName) {
1067
+ try {
1068
+ appendSessionMessage(this.sessionName, message);
1069
+ } catch {
1070
+ }
1071
+ }
1072
+ }
1073
+ /**
1074
+ * Reconfigure model/harvest/branch/stream mid-session. The loop's log,
1075
+ * scratch, and stats are preserved — only the per-turn behavior changes.
1076
+ * Used by the TUI's slash commands and by library callers who want to
1077
+ * flip a knob between turns.
1078
+ */
1079
+ configure(opts) {
1080
+ if (opts.model !== void 0) this.model = opts.model;
1081
+ if (opts.stream !== void 0) this._streamPreference = opts.stream;
1082
+ if (opts.branch !== void 0) {
1083
+ if (typeof opts.branch === "number") {
1084
+ this.branchOptions = { budget: opts.branch };
1085
+ } else if (opts.branch && typeof opts.branch === "object") {
1086
+ this.branchOptions = opts.branch;
1087
+ } else {
1088
+ this.branchOptions = {};
1089
+ }
1090
+ this.branchEnabled = (this.branchOptions.budget ?? 1) > 1;
1091
+ }
1092
+ if (opts.harvest !== void 0) {
1093
+ const want = opts.harvest === true || typeof opts.harvest === "object" && opts.harvest !== null;
1094
+ this.harvestEnabled = want || this.branchEnabled;
1095
+ if (typeof opts.harvest === "object" && opts.harvest !== null) {
1096
+ this.harvestOptions = opts.harvest;
1097
+ }
1098
+ } else if (this.branchEnabled) {
1099
+ this.harvestEnabled = true;
1100
+ }
1101
+ this.stream = this.branchEnabled ? false : this._streamPreference;
637
1102
  }
638
1103
  buildMessages(pendingUser) {
639
1104
  const msgs = [...this.prefix.toMessages(), ...this.log.toMessages()];
@@ -651,8 +1116,85 @@ var CacheFirstLoop = class {
651
1116
  let reasoningContent = "";
652
1117
  let toolCalls = [];
653
1118
  let usage = null;
1119
+ let branchSummary;
1120
+ let preHarvestedPlanState;
654
1121
  try {
655
- if (this.stream) {
1122
+ if (this.branchEnabled) {
1123
+ const budget = this.branchOptions.budget ?? 1;
1124
+ yield {
1125
+ turn: this._turn,
1126
+ role: "branch_start",
1127
+ content: "",
1128
+ branchProgress: {
1129
+ completed: 0,
1130
+ total: budget,
1131
+ latestIndex: -1,
1132
+ latestTemperature: -1,
1133
+ latestUncertainties: -1
1134
+ }
1135
+ };
1136
+ const queue = [];
1137
+ let waiter = null;
1138
+ const onSampleDone = (sample) => {
1139
+ if (waiter) {
1140
+ const w = waiter;
1141
+ waiter = null;
1142
+ w(sample);
1143
+ } else {
1144
+ queue.push(sample);
1145
+ }
1146
+ };
1147
+ const branchPromise = runBranches(
1148
+ this.client,
1149
+ {
1150
+ model: this.model,
1151
+ messages,
1152
+ tools: toolSpecs.length ? toolSpecs : void 0
1153
+ },
1154
+ {
1155
+ ...this.branchOptions,
1156
+ harvestOptions: this.harvestOptions,
1157
+ onSampleDone
1158
+ }
1159
+ );
1160
+ for (let k = 0; k < budget; k++) {
1161
+ const sample = queue.shift() ?? await new Promise((resolve2) => {
1162
+ waiter = resolve2;
1163
+ });
1164
+ yield {
1165
+ turn: this._turn,
1166
+ role: "branch_progress",
1167
+ content: "",
1168
+ branchProgress: {
1169
+ completed: k + 1,
1170
+ total: budget,
1171
+ latestIndex: sample.index,
1172
+ latestTemperature: sample.temperature,
1173
+ latestUncertainties: sample.planState.uncertainties.length
1174
+ }
1175
+ };
1176
+ }
1177
+ const result = await branchPromise;
1178
+ assistantContent = result.chosen.response.content;
1179
+ reasoningContent = result.chosen.response.reasoningContent ?? "";
1180
+ toolCalls = result.chosen.response.toolCalls;
1181
+ const agg = aggregateBranchUsage(result.samples);
1182
+ usage = new Usage(
1183
+ agg.promptTokens,
1184
+ agg.completionTokens,
1185
+ agg.totalTokens,
1186
+ agg.promptCacheHitTokens,
1187
+ agg.promptCacheMissTokens
1188
+ );
1189
+ preHarvestedPlanState = result.chosen.planState;
1190
+ branchSummary = summarizeBranch(result.chosen, result.samples);
1191
+ yield {
1192
+ turn: this._turn,
1193
+ role: "branch_done",
1194
+ content: "",
1195
+ branch: branchSummary
1196
+ };
1197
+ } else if (this.stream) {
656
1198
  const callBuf = /* @__PURE__ */ new Map();
657
1199
  for await (const chunk of this.client.stream({
658
1200
  model: this.model,
@@ -712,29 +1254,26 @@ var CacheFirstLoop = class {
712
1254
  };
713
1255
  return;
714
1256
  }
715
- const turnStats = this.stats.record(
716
- this._turn,
717
- this.model,
718
- usage ?? new (await import("./client-RIVGDOJP.js")).Usage()
719
- );
1257
+ const turnStats = this.stats.record(this._turn, this.model, usage ?? new Usage());
720
1258
  if (pendingUser !== null) {
721
- this.log.append({ role: "user", content: pendingUser });
1259
+ this.appendAndPersist({ role: "user", content: pendingUser });
722
1260
  pendingUser = null;
723
1261
  }
724
1262
  this.scratch.reasoning = reasoningContent || null;
725
- const planState = this.harvestEnabled ? await harvest(reasoningContent || null, this.client, this.harvestOptions) : emptyPlanState();
1263
+ const planState = preHarvestedPlanState ? preHarvestedPlanState : this.harvestEnabled ? await harvest(reasoningContent || null, this.client, this.harvestOptions) : emptyPlanState();
726
1264
  const { calls: repairedCalls, report } = this.repair.process(
727
1265
  toolCalls,
728
1266
  reasoningContent || null
729
1267
  );
730
- this.log.append(this.assistantMessage(assistantContent, repairedCalls));
1268
+ this.appendAndPersist(this.assistantMessage(assistantContent, repairedCalls));
731
1269
  yield {
732
1270
  turn: this._turn,
733
1271
  role: "assistant_final",
734
1272
  content: assistantContent,
735
1273
  stats: turnStats,
736
1274
  planState,
737
- repair: report
1275
+ repair: report,
1276
+ branch: branchSummary
738
1277
  };
739
1278
  if (repairedCalls.length === 0) {
740
1279
  yield { turn: this._turn, role: "done", content: assistantContent };
@@ -744,7 +1283,7 @@ var CacheFirstLoop = class {
744
1283
  const name = call.function?.name ?? "";
745
1284
  const args = call.function?.arguments ?? "{}";
746
1285
  const result = await this.tools.dispatch(name, args);
747
- this.log.append({
1286
+ this.appendAndPersist({
748
1287
  role: "tool",
749
1288
  tool_call_id: call.id ?? "",
750
1289
  name,
@@ -770,14 +1309,22 @@ var CacheFirstLoop = class {
770
1309
  return msg;
771
1310
  }
772
1311
  };
1312
+ function summarizeBranch(chosen, samples) {
1313
+ return {
1314
+ budget: samples.length,
1315
+ chosenIndex: chosen.index,
1316
+ uncertainties: samples.map((s) => s.planState.uncertainties.length),
1317
+ temperatures: samples.map((s) => s.temperature)
1318
+ };
1319
+ }
773
1320
 
774
1321
  // src/env.ts
775
- import { readFileSync } from "fs";
1322
+ import { readFileSync as readFileSync2 } from "fs";
776
1323
  import { resolve } from "path";
777
1324
  function loadDotenv(path = ".env") {
778
1325
  let raw;
779
1326
  try {
780
- raw = readFileSync(resolve(process.cwd(), path), "utf8");
1327
+ raw = readFileSync2(resolve(process.cwd(), path), "utf8");
781
1328
  } catch {
782
1329
  return;
783
1330
  }
@@ -796,15 +1343,15 @@ function loadDotenv(path = ".env") {
796
1343
  }
797
1344
 
798
1345
  // src/config.ts
799
- import { chmodSync, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
800
- import { homedir } from "os";
801
- import { dirname, join } from "path";
1346
+ import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync } from "fs";
1347
+ import { homedir as homedir2 } from "os";
1348
+ import { dirname as dirname2, join as join2 } from "path";
802
1349
  function defaultConfigPath() {
803
- return join(homedir(), ".reasonix", "config.json");
1350
+ return join2(homedir2(), ".reasonix", "config.json");
804
1351
  }
805
1352
  function readConfig(path = defaultConfigPath()) {
806
1353
  try {
807
- const raw = readFileSync2(path, "utf8");
1354
+ const raw = readFileSync3(path, "utf8");
808
1355
  const parsed = JSON.parse(raw);
809
1356
  if (parsed && typeof parsed === "object") return parsed;
810
1357
  } catch {
@@ -812,10 +1359,10 @@ function readConfig(path = defaultConfigPath()) {
812
1359
  return {};
813
1360
  }
814
1361
  function writeConfig(cfg, path = defaultConfigPath()) {
815
- mkdirSync(dirname(path), { recursive: true });
1362
+ mkdirSync2(dirname2(path), { recursive: true });
816
1363
  writeFileSync(path, JSON.stringify(cfg, null, 2), "utf8");
817
1364
  try {
818
- chmodSync(path, 384);
1365
+ chmodSync2(path, 384);
819
1366
  } catch {
820
1367
  }
821
1368
  }
@@ -843,22 +1390,67 @@ var VERSION = "0.0.1";
843
1390
 
844
1391
  // src/cli/commands/chat.tsx
845
1392
  import { render } from "ink";
846
- import React7, { useState as useState3 } from "react";
1393
+ import React7, { useState as useState4 } from "react";
847
1394
 
848
1395
  // src/cli/ui/App.tsx
849
1396
  import { createWriteStream } from "fs";
850
- import { Box as Box5, Static, useApp } from "ink";
851
- import React5, { useCallback, useEffect, useMemo, useRef, useState } from "react";
1397
+ import { Box as Box5, Static, Text as Text5, useApp } from "ink";
1398
+ import React5, { useCallback, useEffect as useEffect2, useMemo, useRef, useState as useState2 } from "react";
852
1399
 
853
1400
  // src/cli/ui/EventLog.tsx
854
1401
  import { Box as Box2, Text as Text2 } from "ink";
855
- import React2 from "react";
1402
+ import React2, { useEffect, useState } from "react";
856
1403
 
857
1404
  // src/cli/ui/markdown.tsx
858
1405
  import { Box, Text } from "ink";
859
1406
  import React from "react";
1407
+ var SUPERSCRIPT = {
1408
+ "0": "\u2070",
1409
+ "1": "\xB9",
1410
+ "2": "\xB2",
1411
+ "3": "\xB3",
1412
+ "4": "\u2074",
1413
+ "5": "\u2075",
1414
+ "6": "\u2076",
1415
+ "7": "\u2077",
1416
+ "8": "\u2078",
1417
+ "9": "\u2079",
1418
+ "+": "\u207A",
1419
+ "-": "\u207B",
1420
+ n: "\u207F"
1421
+ };
1422
+ var SUBSCRIPT = {
1423
+ "0": "\u2080",
1424
+ "1": "\u2081",
1425
+ "2": "\u2082",
1426
+ "3": "\u2083",
1427
+ "4": "\u2084",
1428
+ "5": "\u2085",
1429
+ "6": "\u2086",
1430
+ "7": "\u2087",
1431
+ "8": "\u2088",
1432
+ "9": "\u2089",
1433
+ "+": "\u208A",
1434
+ "-": "\u208B"
1435
+ };
1436
+ function toSuperscript(s) {
1437
+ let out = "";
1438
+ for (const c of s) out += SUPERSCRIPT[c] ?? c;
1439
+ return out;
1440
+ }
1441
+ function toSubscript(s) {
1442
+ let out = "";
1443
+ for (const c of s) out += SUBSCRIPT[c] ?? c;
1444
+ return out;
1445
+ }
860
1446
  function stripMath(s) {
861
- return s.replace(/\\\(\s*/g, "").replace(/\s*\\\)/g, "").replace(/\\\[\s*/g, "\n").replace(/\s*\\\]/g, "\n").replace(/\\boxed\{([^}]+)\}/g, "\u3010$1\u3011").replace(/\\sqrt\{([^}]+)\}/g, "\u221A($1)").replace(/\\frac\{([^}]+)\}\{([^}]+)\}/g, "($1)/($2)").replace(/\\text\{([^}]+)\}/g, "$1").replace(/\\cdot/g, "\xB7").replace(/\\times/g, "\xD7").replace(/\\div/g, "\xF7").replace(/\\pm/g, "\xB1").replace(/\\mp/g, "\u2213").replace(/\\leq/g, "\u2264").replace(/\\geq/g, "\u2265").replace(/\\neq/g, "\u2260").replace(/\\approx/g, "\u2248").replace(/\\in\b/g, "\u2208").replace(/\\notin\b/g, "\u2209").replace(/\\infty/g, "\u221E").replace(/\\sum\b/g, "\u03A3").replace(/\\prod\b/g, "\u03A0").replace(/\\int\b/g, "\u222B").replace(/\\alpha/g, "\u03B1").replace(/\\beta/g, "\u03B2").replace(/\\gamma/g, "\u03B3").replace(/\\delta/g, "\u03B4").replace(/\\theta/g, "\u03B8").replace(/\\lambda/g, "\u03BB").replace(/\\mu/g, "\u03BC").replace(/\\pi/g, "\u03C0").replace(/\\sigma/g, "\u03C3").replace(/\\phi/g, "\u03C6").replace(/\\omega/g, "\u03C9").replace(/\\implies\b/g, "\u21D2").replace(/\\iff\b/g, "\u21D4").replace(/\\to\b/g, "\u2192").replace(/\\rightarrow/g, "\u2192").replace(/\\Rightarrow/g, "\u21D2").replace(/\\leftarrow/g, "\u2190").replace(/\\Leftarrow/g, "\u21D0").replace(/\\ldots/g, "\u2026").replace(/\\cdots/g, "\u22EF").replace(/\\quad/g, " ").replace(/\\qquad/g, " ").replace(/\\,/g, " ").replace(/\\;/g, " ").replace(/\\!/g, "").replace(/\\\\/g, "\n").replace(/[ \t]{2,}/g, " ");
1447
+ return s.replace(/\\\(\s*/g, "").replace(/\s*\\\)/g, "").replace(/\\\[\s*/g, "\n").replace(/\s*\\\]/g, "\n").replace(
1448
+ /\\[dt]?frac\s*\{((?:[^{}]|\{[^{}]*\})+)\}\s*\{((?:[^{}]|\{[^{}]*\})+)\}/g,
1449
+ (_m, num, den) => `(${num.trim()})/(${den.trim()})`
1450
+ ).replace(
1451
+ /\\binom\s*\{([^{}]+)\}\s*\{([^{}]+)\}/g,
1452
+ (_m, n, k) => `C(${n.trim()},${k.trim()})`
1453
+ ).replace(/\\sqrt\s*\{([^{}]+)\}/g, (_m, g) => `\u221A(${g.trim()})`).replace(/\\boxed\s*\{([^{}]+)\}/g, (_m, g) => `\u3010${g.trim()}\u3011`).replace(/\\text\s*\{([^{}]+)\}/g, (_m, g) => g.trim()).replace(/\\overline\s*\{([^{}]+)\}/g, (_m, g) => `${g.trim()}\u0304`).replace(/\\hat\s*\{([^{}]+)\}/g, (_m, g) => `${g.trim()}\u0302`).replace(/\\vec\s*\{([^{}]+)\}/g, (_m, g) => `\u2192${g.trim()}`).replace(/\\cdot/g, "\xB7").replace(/\\times/g, "\xD7").replace(/\\div/g, "\xF7").replace(/\\pm/g, "\xB1").replace(/\\mp/g, "\u2213").replace(/\\leq/g, "\u2264").replace(/\\geq/g, "\u2265").replace(/\\neq/g, "\u2260").replace(/\\approx/g, "\u2248").replace(/\\in\b/g, "\u2208").replace(/\\notin\b/g, "\u2209").replace(/\\infty/g, "\u221E").replace(/\\sum\b/g, "\u03A3").replace(/\\prod\b/g, "\u03A0").replace(/\\int\b/g, "\u222B").replace(/\\alpha/g, "\u03B1").replace(/\\beta/g, "\u03B2").replace(/\\gamma/g, "\u03B3").replace(/\\delta/g, "\u03B4").replace(/\\theta/g, "\u03B8").replace(/\\lambda/g, "\u03BB").replace(/\\mu/g, "\u03BC").replace(/\\pi/g, "\u03C0").replace(/\\sigma/g, "\u03C3").replace(/\\phi/g, "\u03C6").replace(/\\omega/g, "\u03C9").replace(/\\implies\b/g, "\u21D2").replace(/\\iff\b/g, "\u21D4").replace(/\\to\b/g, "\u2192").replace(/\\rightarrow/g, "\u2192").replace(/\\Rightarrow/g, "\u21D2").replace(/\\leftarrow/g, "\u2190").replace(/\\Leftarrow/g, "\u21D0").replace(/\\ldots/g, "\u2026").replace(/\\cdots/g, "\u22EF").replace(/\\quad/g, " ").replace(/\\qquad/g, " ").replace(/\\,/g, " ").replace(/\\;/g, " ").replace(/\\!/g, "").replace(/\\\\/g, "\n").replace(/\^\{([\w+-]+)\}/g, (_m, g) => toSuperscript(g)).replace(/\^([0-9+\-n])/g, (_m, g) => toSuperscript(g)).replace(/_\{([\w+-]+)\}/g, (_m, g) => toSubscript(g)).replace(/_([0-9+\-])/g, (_m, g) => toSubscript(g)).replace(/\\[a-zA-Z]+\s*\{([^{}]+)\}\s*\{([^{}]+)\}/g, "($1)/($2)").replace(/\\[a-zA-Z]+\s*\{([^{}]+)\}/g, "$1").replace(/\\[a-zA-Z]+/g, "").replace(/[ \t]{2,}/g, " ");
862
1454
  }
863
1455
  var INLINE_RE = /(\*\*([^*\n]+?)\*\*|`([^`\n]+?)`|(?<![*\w])\*([^*\n]+?)\*(?!\w))/g;
864
1456
  function InlineMd({ text }) {
@@ -1006,7 +1598,7 @@ var EventRow = React2.memo(function EventRow2({ event }) {
1006
1598
  }
1007
1599
  if (event.role === "assistant") {
1008
1600
  if (event.streaming) return /* @__PURE__ */ React2.createElement(StreamingAssistant, { event });
1009
- return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "green" }, "assistant")), event.reasoning ? /* @__PURE__ */ React2.createElement(ReasoningBlock, { reasoning: event.reasoning }) : null, !isPlanStateEmpty(event.planState) ? /* @__PURE__ */ React2.createElement(PlanStateBlock, { planState: event.planState }) : null, event.text ? /* @__PURE__ */ React2.createElement(Markdown, { text: event.text }) : /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, "(no content)"), event.stats ? /* @__PURE__ */ React2.createElement(StatsLine, { stats: event.stats }) : null, event.repair ? /* @__PURE__ */ React2.createElement(Text2, { color: "magenta" }, event.repair) : null);
1601
+ return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "green" }, "assistant")), event.branch ? /* @__PURE__ */ React2.createElement(BranchBlock, { branch: event.branch }) : null, event.reasoning ? /* @__PURE__ */ React2.createElement(ReasoningBlock, { reasoning: event.reasoning }) : null, !isPlanStateEmpty(event.planState) ? /* @__PURE__ */ React2.createElement(PlanStateBlock, { planState: event.planState }) : null, event.text ? /* @__PURE__ */ React2.createElement(Markdown, { text: event.text }) : /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, "(no content)"), event.stats ? /* @__PURE__ */ React2.createElement(StatsLine, { stats: event.stats }) : null, event.repair ? /* @__PURE__ */ React2.createElement(Text2, { color: "magenta" }, event.repair) : null);
1010
1602
  }
1011
1603
  if (event.role === "tool") {
1012
1604
  return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React2.createElement(Text2, { color: "yellow" }, `tool<${event.toolName ?? "?"}> \u2192`), /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, " ", truncate(event.text, 400)));
@@ -1019,6 +1611,14 @@ var EventRow = React2.memo(function EventRow2({ event }) {
1019
1611
  }
1020
1612
  return /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Text2, null, event.text));
1021
1613
  });
1614
+ function BranchBlock({ branch }) {
1615
+ const per = branch.uncertainties.map((u, i) => {
1616
+ const marker = i === branch.chosenIndex ? "\u25B8" : " ";
1617
+ const t = (branch.temperatures[i] ?? 0).toFixed(1);
1618
+ return `${marker} #${i} T=${t} u=${u}`;
1619
+ }).join(" ");
1620
+ return /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Text2, { color: "blue" }, "\u{1F500} branched ", /* @__PURE__ */ React2.createElement(Text2, { bold: true }, branch.budget), ` samples \u2192 picked #${branch.chosenIndex} `, /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, per)));
1621
+ }
1022
1622
  function PlanStateBlock({ planState }) {
1023
1623
  const lines = [];
1024
1624
  if (planState.subgoals.length) lines.push(["subgoals", planState.subgoals]);
@@ -1033,10 +1633,29 @@ function ReasoningBlock({ reasoning }) {
1033
1633
  const preview = flat.length <= max ? flat : `${flat.slice(0, max)}\u2026 (+${flat.length - max} chars)`;
1034
1634
  return /* @__PURE__ */ React2.createElement(Box2, { marginBottom: 1 }, /* @__PURE__ */ React2.createElement(Text2, { dimColor: true, italic: true }, "\u21B3 thinking: ", preview));
1035
1635
  }
1636
+ function Elapsed() {
1637
+ const [s, setS] = useState(0);
1638
+ useEffect(() => {
1639
+ const start = Date.now();
1640
+ const id = setInterval(() => setS(Math.floor((Date.now() - start) / 1e3)), 1e3);
1641
+ return () => clearInterval(id);
1642
+ }, []);
1643
+ const mm = String(Math.floor(s / 60)).padStart(2, "0");
1644
+ const ss = String(s % 60).padStart(2, "0");
1645
+ return /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, `${mm}:${ss}`);
1646
+ }
1036
1647
  function StreamingAssistant({ event }) {
1648
+ if (event.branchProgress) {
1649
+ const p = event.branchProgress;
1650
+ if (p.completed === 0) {
1651
+ return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "green" }, "assistant", " "), /* @__PURE__ */ React2.createElement(Text2, { color: "blue" }, "\u{1F500} launching ", p.total, " parallel samples (R1 thinking in parallel)\u2026", " "), /* @__PURE__ */ React2.createElement(Elapsed, null)), /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, " ", "spread across T=0.0/0.5/1.0 \xB7 typical wait 30-90s for reasoner"));
1652
+ }
1653
+ const pct = Math.round(p.completed / p.total * 100);
1654
+ return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "green" }, "assistant", " "), /* @__PURE__ */ React2.createElement(Text2, { color: "blue" }, "\u{1F500} branching ", p.completed, "/", p.total, " (", pct, "%)", " "), /* @__PURE__ */ React2.createElement(Elapsed, null)), /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, " latest #", p.latestIndex, " T=", p.latestTemperature.toFixed(1), " u=", p.latestUncertainties, p.completed < p.total ? " \xB7 waiting for other samples\u2026" : " \xB7 selecting winner\u2026"));
1655
+ }
1037
1656
  const tail = lastLine(event.text, 140);
1038
1657
  const reasoningTail = event.reasoning ? lastLine(event.reasoning, 120) : "";
1039
- return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "green" }, "assistant", " "), /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, "(streaming \xB7 ", event.text.length, event.reasoning ? ` + think ${event.reasoning.length}` : "", " chars)")), reasoningTail ? /* @__PURE__ */ React2.createElement(Text2, { dimColor: true, italic: true }, "\u21B3 thinking: ", reasoningTail) : null, tail ? /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, "\u25B8 ", tail) : /* @__PURE__ */ React2.createElement(Text2, { dimColor: true, italic: true }, " (waiting for first token\u2026)"));
1658
+ return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "green" }, "assistant", " "), /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, "(streaming \xB7 ", event.text.length, event.reasoning ? ` + think ${event.reasoning.length}` : "", " chars)", " "), /* @__PURE__ */ React2.createElement(Elapsed, null)), reasoningTail ? /* @__PURE__ */ React2.createElement(Text2, { dimColor: true, italic: true }, "\u21B3 thinking: ", reasoningTail) : null, tail ? /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, "\u25B8 ", tail) : /* @__PURE__ */ React2.createElement(Text2, { dimColor: true, italic: true }, " (waiting for first token\u2026)"));
1040
1659
  }
1041
1660
  function lastLine(s, maxChars) {
1042
1661
  const flat = s.replace(/\s+/g, " ").trim();
@@ -1062,7 +1681,7 @@ function PromptInput({
1062
1681
  disabled,
1063
1682
  placeholder
1064
1683
  }) {
1065
- const effectivePlaceholder = disabled ? placeholder ?? "\u2026waiting for response\u2026" : placeholder ?? 'type a message, or "/exit"';
1684
+ const effectivePlaceholder = disabled ? placeholder ?? "\u2026waiting for response\u2026" : placeholder ?? "type a message, or /command";
1066
1685
  return /* @__PURE__ */ React3.createElement(Box3, { borderStyle: "round", borderColor: disabled ? "gray" : "cyan", paddingX: 1 }, /* @__PURE__ */ React3.createElement(Text3, { bold: true, color: disabled ? "gray" : "cyan" }, "you \u203A", " "), /* @__PURE__ */ React3.createElement(
1067
1686
  TextInput,
1068
1687
  {
@@ -1078,21 +1697,156 @@ function PromptInput({
1078
1697
  // src/cli/ui/StatsPanel.tsx
1079
1698
  import { Box as Box4, Text as Text4 } from "ink";
1080
1699
  import React4 from "react";
1081
- function StatsPanel({ summary, model, prefixHash }) {
1700
+ function StatsPanel({
1701
+ summary,
1702
+ model,
1703
+ prefixHash,
1704
+ harvestOn,
1705
+ branchBudget
1706
+ }) {
1082
1707
  const hitPct = (summary.cacheHitRatio * 100).toFixed(1);
1083
1708
  const hitColor = summary.cacheHitRatio >= 0.7 ? "green" : summary.cacheHitRatio >= 0.4 ? "yellow" : "red";
1084
- return /* @__PURE__ */ React4.createElement(Box4, { borderStyle: "round", borderColor: "cyan", flexDirection: "column", paddingX: 1 }, /* @__PURE__ */ React4.createElement(Box4, { justifyContent: "space-between" }, /* @__PURE__ */ React4.createElement(Text4, null, /* @__PURE__ */ React4.createElement(Text4, { color: "cyan", bold: true }, "Reasonix"), /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, " \xB7 model "), /* @__PURE__ */ React4.createElement(Text4, { color: "yellow" }, model), /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, " \xB7 prefix "), /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, prefixHash)), /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, "turns ", summary.turns)), /* @__PURE__ */ React4.createElement(Box4, { marginTop: 1, gap: 3 }, /* @__PURE__ */ React4.createElement(Text4, null, /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, "cache hit "), /* @__PURE__ */ React4.createElement(Text4, { color: hitColor, bold: true }, hitPct, "%")), /* @__PURE__ */ React4.createElement(Text4, null, /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, "cost "), /* @__PURE__ */ React4.createElement(Text4, { color: "green" }, "$", summary.totalCostUsd.toFixed(6))), /* @__PURE__ */ React4.createElement(Text4, null, /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, "vs Claude "), /* @__PURE__ */ React4.createElement(Text4, null, "$", summary.claudeEquivalentUsd.toFixed(6))), /* @__PURE__ */ React4.createElement(Text4, null, /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, "saving "), /* @__PURE__ */ React4.createElement(Text4, { color: "green", bold: true }, summary.savingsVsClaudePct.toFixed(1), "%"))));
1709
+ const branchOn = (branchBudget ?? 1) > 1;
1710
+ return /* @__PURE__ */ React4.createElement(Box4, { borderStyle: "round", borderColor: "cyan", flexDirection: "column", paddingX: 1 }, /* @__PURE__ */ React4.createElement(Box4, { justifyContent: "space-between" }, /* @__PURE__ */ React4.createElement(Text4, null, /* @__PURE__ */ React4.createElement(Text4, { color: "cyan", bold: true }, "Reasonix"), /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, " \xB7 model "), /* @__PURE__ */ React4.createElement(Text4, { color: "yellow" }, model), /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, " \xB7 prefix "), /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, prefixHash), harvestOn ? /* @__PURE__ */ React4.createElement(Text4, { color: "magenta" }, " \xB7 harvest") : null, branchOn ? /* @__PURE__ */ React4.createElement(Text4, { color: "blue" }, " \xB7 branch", branchBudget) : null), /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, "turns ", summary.turns, " \xB7 type /help")), /* @__PURE__ */ React4.createElement(Box4, { marginTop: 1, gap: 3 }, /* @__PURE__ */ React4.createElement(Text4, null, /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, "cache hit "), /* @__PURE__ */ React4.createElement(Text4, { color: hitColor, bold: true }, hitPct, "%")), /* @__PURE__ */ React4.createElement(Text4, null, /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, "cost "), /* @__PURE__ */ React4.createElement(Text4, { color: "green" }, "$", summary.totalCostUsd.toFixed(6))), /* @__PURE__ */ React4.createElement(Text4, null, /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, "vs Claude "), /* @__PURE__ */ React4.createElement(Text4, null, "$", summary.claudeEquivalentUsd.toFixed(6))), /* @__PURE__ */ React4.createElement(Text4, null, /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, "saving "), /* @__PURE__ */ React4.createElement(Text4, { color: "green", bold: true }, summary.savingsVsClaudePct.toFixed(1), "%"))));
1711
+ }
1712
+
1713
+ // src/cli/ui/slash.ts
1714
+ function parseSlash(text) {
1715
+ if (!text.startsWith("/")) return null;
1716
+ const parts = text.slice(1).trim().split(/\s+/);
1717
+ const cmd = parts[0]?.toLowerCase() ?? "";
1718
+ if (!cmd) return null;
1719
+ return { cmd, args: parts.slice(1) };
1720
+ }
1721
+ function handleSlash(cmd, args, loop) {
1722
+ switch (cmd) {
1723
+ case "exit":
1724
+ case "quit":
1725
+ return { exit: true };
1726
+ case "clear":
1727
+ return { clear: true };
1728
+ case "help":
1729
+ case "?":
1730
+ return {
1731
+ info: [
1732
+ "Commands:",
1733
+ " /help this message",
1734
+ " /status show current settings",
1735
+ " /preset <fast|smart|max> one-tap presets \u2014 see below",
1736
+ " /model <id> deepseek-chat or deepseek-reasoner",
1737
+ " /harvest [on|off] Pillar 2: structured plan-state extraction",
1738
+ " /branch <N|off> run N parallel samples (N>=2), pick most confident",
1739
+ " /sessions list saved sessions (current is marked with \u25B8)",
1740
+ " /forget delete the current session from disk",
1741
+ " /clear clear displayed history (log + session kept)",
1742
+ " /exit quit",
1743
+ "",
1744
+ "Presets:",
1745
+ " fast deepseek-chat no harvest no branch ~1\xA2/100turns \u2190 default",
1746
+ " smart reasoner harvest ~10x cost, slower",
1747
+ " max reasoner harvest branch 3 ~30x cost, slowest",
1748
+ "",
1749
+ "Sessions (auto-enabled by default, named 'default'):",
1750
+ " reasonix chat --session <name> use a different named session",
1751
+ " reasonix chat --no-session disable persistence for this run"
1752
+ ].join("\n")
1753
+ };
1754
+ case "sessions": {
1755
+ const items = listSessions();
1756
+ if (items.length === 0) {
1757
+ return {
1758
+ info: "no saved sessions yet \u2014 chat normally and your messages will be saved automatically"
1759
+ };
1760
+ }
1761
+ const lines = ["Saved sessions:"];
1762
+ for (const s of items) {
1763
+ const sizeKb = (s.size / 1024).toFixed(1);
1764
+ const when = s.mtime.toISOString().replace("T", " ").slice(0, 16);
1765
+ const marker = s.name === loop.sessionName ? "\u25B8" : " ";
1766
+ lines.push(
1767
+ ` ${marker} ${s.name.padEnd(22)} ${String(s.messageCount).padStart(5)} msgs ${sizeKb.padStart(7)} KB ${when}`
1768
+ );
1769
+ }
1770
+ lines.push("");
1771
+ lines.push("Resume with: reasonix chat --session <name>");
1772
+ return { info: lines.join("\n") };
1773
+ }
1774
+ case "forget": {
1775
+ if (!loop.sessionName) {
1776
+ return { info: "not in a session \u2014 nothing to forget" };
1777
+ }
1778
+ const name = loop.sessionName;
1779
+ const ok = deleteSession(name);
1780
+ return {
1781
+ info: ok ? `\u25B8 deleted session "${name}" \u2014 current screen still shows the conversation, but next launch starts fresh` : `could not delete session "${name}" (already gone?)`
1782
+ };
1783
+ }
1784
+ case "status": {
1785
+ const branchBudget = loop.branchOptions.budget ?? 1;
1786
+ return {
1787
+ info: `model=${loop.model} harvest=${loop.harvestEnabled ? "on" : "off"} branch=${branchBudget > 1 ? branchBudget : "off"} stream=${loop.stream ? "on" : "off"}`
1788
+ };
1789
+ }
1790
+ case "model": {
1791
+ const id = args[0];
1792
+ if (!id) return { info: "usage: /model <id> (try deepseek-chat or deepseek-reasoner)" };
1793
+ loop.configure({ model: id });
1794
+ return { info: `model \u2192 ${id}` };
1795
+ }
1796
+ case "harvest": {
1797
+ const arg = (args[0] ?? "").toLowerCase();
1798
+ const on = arg === "" ? !loop.harvestEnabled : arg === "on" || arg === "true" || arg === "1";
1799
+ loop.configure({ harvest: on });
1800
+ return { info: `harvest \u2192 ${loop.harvestEnabled ? "on" : "off"}` };
1801
+ }
1802
+ case "preset": {
1803
+ const name = (args[0] ?? "").toLowerCase();
1804
+ if (name === "fast" || name === "default") {
1805
+ loop.configure({ model: "deepseek-chat", harvest: false, branch: 1 });
1806
+ return { info: "preset \u2192 fast (deepseek-chat, no harvest, no branch)" };
1807
+ }
1808
+ if (name === "smart") {
1809
+ loop.configure({ model: "deepseek-reasoner", harvest: true, branch: 1 });
1810
+ return { info: "preset \u2192 smart (reasoner + harvest, ~10x cost vs fast)" };
1811
+ }
1812
+ if (name === "max" || name === "best") {
1813
+ loop.configure({ model: "deepseek-reasoner", harvest: true, branch: 3 });
1814
+ return {
1815
+ info: "preset \u2192 max (reasoner + harvest + branch3, ~30x cost vs fast, slowest)"
1816
+ };
1817
+ }
1818
+ return { info: "usage: /preset <fast|smart|max>" };
1819
+ }
1820
+ case "branch": {
1821
+ const raw = (args[0] ?? "").toLowerCase();
1822
+ if (raw === "" || raw === "off" || raw === "0" || raw === "1") {
1823
+ loop.configure({ branch: 1 });
1824
+ return { info: "branch \u2192 off" };
1825
+ }
1826
+ const n = Number.parseInt(raw, 10);
1827
+ if (!Number.isFinite(n) || n < 2) {
1828
+ return { info: "usage: /branch <N> (N>=2, or 'off')" };
1829
+ }
1830
+ if (n > 8) {
1831
+ return { info: "branch budget capped at 8 to prevent runaway cost" };
1832
+ }
1833
+ loop.configure({ branch: n });
1834
+ return { info: `branch \u2192 ${n} (harvest auto-enabled; streaming disabled)` };
1835
+ }
1836
+ default:
1837
+ return { unknown: true, info: `unknown command: /${cmd} (try /help)` };
1838
+ }
1085
1839
  }
1086
1840
 
1087
1841
  // src/cli/ui/App.tsx
1088
1842
  var FLUSH_INTERVAL_MS = 60;
1089
- function App({ model, system, transcript, harvest: harvest2 }) {
1843
+ function App({ model, system, transcript, harvest: harvest2, branch, session }) {
1090
1844
  const { exit } = useApp();
1091
- const [historical, setHistorical] = useState([]);
1092
- const [streaming, setStreaming] = useState(null);
1093
- const [input, setInput] = useState("");
1094
- const [busy, setBusy] = useState(false);
1095
- const [summary, setSummary] = useState({
1845
+ const [historical, setHistorical] = useState2([]);
1846
+ const [streaming, setStreaming] = useState2(null);
1847
+ const [input, setInput] = useState2("");
1848
+ const [busy, setBusy] = useState2(false);
1849
+ const [summary, setSummary] = useState2({
1096
1850
  turns: 0,
1097
1851
  totalCostUsd: 0,
1098
1852
  claudeEquivalentUsd: 0,
@@ -1103,7 +1857,7 @@ function App({ model, system, transcript, harvest: harvest2 }) {
1103
1857
  if (transcript && !transcriptRef.current) {
1104
1858
  transcriptRef.current = createWriteStream(transcript, { flags: "a" });
1105
1859
  }
1106
- useEffect(() => {
1860
+ useEffect2(() => {
1107
1861
  return () => {
1108
1862
  transcriptRef.current?.end();
1109
1863
  };
@@ -1113,10 +1867,43 @@ function App({ model, system, transcript, harvest: harvest2 }) {
1113
1867
  if (loopRef.current) return loopRef.current;
1114
1868
  const client = new DeepSeekClient();
1115
1869
  const prefix = new ImmutablePrefix({ system });
1116
- const l = new CacheFirstLoop({ client, prefix, model, harvest: harvest2 });
1870
+ const l = new CacheFirstLoop({ client, prefix, model, harvest: harvest2, branch, session });
1117
1871
  loopRef.current = l;
1118
1872
  return l;
1119
- }, [model, system, harvest2]);
1873
+ }, [model, system, harvest2, branch, session]);
1874
+ const sessionBannerShown = useRef(false);
1875
+ useEffect2(() => {
1876
+ if (sessionBannerShown.current) return;
1877
+ sessionBannerShown.current = true;
1878
+ if (!session) {
1879
+ setHistorical((prev) => [
1880
+ ...prev,
1881
+ {
1882
+ id: `sys-session-${Date.now()}`,
1883
+ role: "info",
1884
+ text: "\u25B8 ephemeral chat (no session persistence) \u2014 drop --no-session to enable"
1885
+ }
1886
+ ]);
1887
+ } else if (loop.resumedMessageCount > 0) {
1888
+ setHistorical((prev) => [
1889
+ ...prev,
1890
+ {
1891
+ id: `sys-resume-${Date.now()}`,
1892
+ role: "info",
1893
+ text: `\u25B8 resumed session "${session}" with ${loop.resumedMessageCount} prior messages \xB7 /forget to start over \xB7 /sessions to list`
1894
+ }
1895
+ ]);
1896
+ } else {
1897
+ setHistorical((prev) => [
1898
+ ...prev,
1899
+ {
1900
+ id: `sys-newsession-${Date.now()}`,
1901
+ role: "info",
1902
+ text: `\u25B8 session "${session}" (new) \u2014 auto-saved as you chat \xB7 /forget to delete \xB7 /sessions to list`
1903
+ }
1904
+ ]);
1905
+ }
1906
+ }, [session, loop]);
1120
1907
  const prefixHash = loop.prefix.fingerprint;
1121
1908
  const writeTranscript = useCallback((ev) => {
1122
1909
  transcriptRef.current?.write(
@@ -1135,13 +1922,28 @@ function App({ model, system, transcript, harvest: harvest2 }) {
1135
1922
  const text = raw.trim();
1136
1923
  if (!text || busy) return;
1137
1924
  setInput("");
1138
- if (text === "/exit" || text === "/quit") {
1139
- transcriptRef.current?.end();
1140
- exit();
1141
- return;
1142
- }
1143
- if (text === "/clear") {
1144
- setHistorical([]);
1925
+ const slash = parseSlash(text);
1926
+ if (slash) {
1927
+ const result = handleSlash(slash.cmd, slash.args, loop);
1928
+ if (result.exit) {
1929
+ transcriptRef.current?.end();
1930
+ exit();
1931
+ return;
1932
+ }
1933
+ if (result.clear) {
1934
+ setHistorical([]);
1935
+ return;
1936
+ }
1937
+ if (result.info) {
1938
+ setHistorical((prev) => [
1939
+ ...prev,
1940
+ {
1941
+ id: `sys-${Date.now()}`,
1942
+ role: "info",
1943
+ text: result.info
1944
+ }
1945
+ ]);
1946
+ }
1145
1947
  return;
1146
1948
  }
1147
1949
  setHistorical((prev) => [...prev, { id: `u-${Date.now()}`, role: "user", text }]);
@@ -1172,6 +1974,23 @@ function App({ model, system, transcript, harvest: harvest2 }) {
1172
1974
  if (ev.role === "assistant_delta") {
1173
1975
  if (ev.content) contentBuf.current += ev.content;
1174
1976
  if (ev.reasoningDelta) reasoningBuf.current += ev.reasoningDelta;
1977
+ } else if (ev.role === "branch_start") {
1978
+ setStreaming({
1979
+ id: assistantId,
1980
+ role: "assistant",
1981
+ text: "",
1982
+ streaming: true,
1983
+ branchProgress: ev.branchProgress
1984
+ });
1985
+ } else if (ev.role === "branch_progress") {
1986
+ setStreaming({
1987
+ id: assistantId,
1988
+ role: "assistant",
1989
+ text: "",
1990
+ streaming: true,
1991
+ branchProgress: ev.branchProgress
1992
+ });
1993
+ } else if (ev.role === "branch_done") {
1175
1994
  } else if (ev.role === "assistant_final") {
1176
1995
  flush();
1177
1996
  const repairNote = ev.repair ? describeRepair(ev.repair) : "";
@@ -1184,6 +2003,7 @@ function App({ model, system, transcript, harvest: harvest2 }) {
1184
2003
  text: ev.content || streamRef.text,
1185
2004
  reasoning: streamRef.reasoning || void 0,
1186
2005
  planState: ev.planState,
2006
+ branch: ev.branch,
1187
2007
  stats: ev.stats,
1188
2008
  repair: repairNote || void 0,
1189
2009
  streaming: false
@@ -1217,7 +2037,19 @@ function App({ model, system, transcript, harvest: harvest2 }) {
1217
2037
  },
1218
2038
  [busy, exit, loop, writeTranscript]
1219
2039
  );
1220
- return /* @__PURE__ */ React5.createElement(Box5, { flexDirection: "column" }, /* @__PURE__ */ React5.createElement(StatsPanel, { summary, model, prefixHash }), /* @__PURE__ */ React5.createElement(Static, { items: historical }, (item) => /* @__PURE__ */ React5.createElement(EventRow, { key: item.id, event: item })), streaming ? /* @__PURE__ */ React5.createElement(Box5, { marginY: 1 }, /* @__PURE__ */ React5.createElement(EventRow, { event: streaming })) : null, /* @__PURE__ */ React5.createElement(PromptInput, { value: input, onChange: setInput, onSubmit: handleSubmit, disabled: busy }));
2040
+ return /* @__PURE__ */ React5.createElement(Box5, { flexDirection: "column" }, /* @__PURE__ */ React5.createElement(
2041
+ StatsPanel,
2042
+ {
2043
+ summary,
2044
+ model: loop.model,
2045
+ prefixHash,
2046
+ harvestOn: loop.harvestEnabled,
2047
+ branchBudget: loop.branchOptions.budget
2048
+ }
2049
+ ), /* @__PURE__ */ React5.createElement(Static, { items: historical }, (item) => /* @__PURE__ */ React5.createElement(EventRow, { key: item.id, event: item })), streaming ? /* @__PURE__ */ React5.createElement(Box5, { marginY: 1 }, /* @__PURE__ */ React5.createElement(EventRow, { event: streaming })) : null, /* @__PURE__ */ React5.createElement(PromptInput, { value: input, onChange: setInput, onSubmit: handleSubmit, disabled: busy }), /* @__PURE__ */ React5.createElement(CommandStrip, null));
2050
+ }
2051
+ function CommandStrip() {
2052
+ return /* @__PURE__ */ React5.createElement(Box5, { paddingX: 2 }, /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "/help \xB7 /preset ", "<fast|smart|max>", " \xB7 /sessions \xB7 /model \xB7 /harvest \xB7 /branch \xB7 /clear \xB7 /exit"));
1221
2053
  }
1222
2054
  function describeRepair(repair) {
1223
2055
  const parts = [];
@@ -1228,12 +2060,12 @@ function describeRepair(repair) {
1228
2060
  }
1229
2061
 
1230
2062
  // src/cli/ui/Setup.tsx
1231
- import { Box as Box6, Text as Text5, useApp as useApp2 } from "ink";
2063
+ import { Box as Box6, Text as Text6, useApp as useApp2 } from "ink";
1232
2064
  import TextInput2 from "ink-text-input";
1233
- import React6, { useState as useState2 } from "react";
2065
+ import React6, { useState as useState3 } from "react";
1234
2066
  function Setup({ onReady }) {
1235
- const [value, setValue] = useState2("");
1236
- const [error, setError] = useState2(null);
2067
+ const [value, setValue] = useState3("");
2068
+ const [error, setError] = useState3(null);
1237
2069
  const { exit } = useApp2();
1238
2070
  const handleSubmit = (raw) => {
1239
2071
  const trimmed = raw.trim();
@@ -1254,7 +2086,7 @@ function Setup({ onReady }) {
1254
2086
  }
1255
2087
  onReady(trimmed);
1256
2088
  };
1257
- return /* @__PURE__ */ React6.createElement(Box6, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React6.createElement(Text5, { bold: true, color: "cyan" }, "Welcome to Reasonix."), /* @__PURE__ */ React6.createElement(Box6, { marginTop: 1 }, /* @__PURE__ */ React6.createElement(Text5, null, "Paste your DeepSeek API key to get started.")), /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, "Get one (free credit on signup): https://platform.deepseek.com/api_keys"), /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, "Saved locally to ", defaultConfigPath()), /* @__PURE__ */ React6.createElement(Box6, { marginTop: 1 }, /* @__PURE__ */ React6.createElement(Text5, { bold: true, color: "cyan" }, "key \u203A "), /* @__PURE__ */ React6.createElement(
2089
+ return /* @__PURE__ */ React6.createElement(Box6, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React6.createElement(Text6, { bold: true, color: "cyan" }, "Welcome to Reasonix."), /* @__PURE__ */ React6.createElement(Box6, { marginTop: 1 }, /* @__PURE__ */ React6.createElement(Text6, null, "Paste your DeepSeek API key to get started.")), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "Get one (free credit on signup): https://platform.deepseek.com/api_keys"), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "Saved locally to ", defaultConfigPath()), /* @__PURE__ */ React6.createElement(Box6, { marginTop: 1 }, /* @__PURE__ */ React6.createElement(Text6, { bold: true, color: "cyan" }, "key \u203A "), /* @__PURE__ */ React6.createElement(
1258
2090
  TextInput2,
1259
2091
  {
1260
2092
  value,
@@ -1263,12 +2095,12 @@ function Setup({ onReady }) {
1263
2095
  mask: "\u2022",
1264
2096
  placeholder: "sk-..."
1265
2097
  }
1266
- )), error ? /* @__PURE__ */ React6.createElement(Box6, { marginTop: 1 }, /* @__PURE__ */ React6.createElement(Text5, { color: "red" }, error)) : value ? /* @__PURE__ */ React6.createElement(Box6, { marginTop: 1 }, /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, "preview: ", redactKey(value))) : null, /* @__PURE__ */ React6.createElement(Box6, { marginTop: 1 }, /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, "(Type /exit to abort.)")));
2098
+ )), error ? /* @__PURE__ */ React6.createElement(Box6, { marginTop: 1 }, /* @__PURE__ */ React6.createElement(Text6, { color: "red" }, error)) : value ? /* @__PURE__ */ React6.createElement(Box6, { marginTop: 1 }, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "preview: ", redactKey(value))) : null, /* @__PURE__ */ React6.createElement(Box6, { marginTop: 1 }, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "(Type /exit to abort.)")));
1267
2099
  }
1268
2100
 
1269
2101
  // src/cli/commands/chat.tsx
1270
2102
  function Root({ initialKey, ...appProps }) {
1271
- const [key, setKey] = useState3(initialKey);
2103
+ const [key, setKey] = useState4(initialKey);
1272
2104
  if (!key) {
1273
2105
  return /* @__PURE__ */ React7.createElement(
1274
2106
  Setup,
@@ -1287,7 +2119,9 @@ function Root({ initialKey, ...appProps }) {
1287
2119
  model: appProps.model,
1288
2120
  system: appProps.system,
1289
2121
  transcript: appProps.transcript,
1290
- harvest: appProps.harvest
2122
+ harvest: appProps.harvest,
2123
+ branch: appProps.branch,
2124
+ session: appProps.session
1291
2125
  }
1292
2126
  );
1293
2127
  }
@@ -1360,13 +2194,13 @@ async function runCommand(opts) {
1360
2194
  }
1361
2195
 
1362
2196
  // src/cli/commands/stats.ts
1363
- import { existsSync, readFileSync as readFileSync3 } from "fs";
2197
+ import { existsSync as existsSync2, readFileSync as readFileSync4 } from "fs";
1364
2198
  function statsCommand(opts) {
1365
- if (!existsSync(opts.transcript)) {
2199
+ if (!existsSync2(opts.transcript)) {
1366
2200
  console.error(`no such transcript: ${opts.transcript}`);
1367
2201
  process.exit(1);
1368
2202
  }
1369
- const lines = readFileSync3(opts.transcript, "utf8").split(/\r?\n/).filter(Boolean);
2203
+ const lines = readFileSync4(opts.transcript, "utf8").split(/\r?\n/).filter(Boolean);
1370
2204
  let assistantTurns = 0;
1371
2205
  let toolCalls = 0;
1372
2206
  let lastTurn = 0;
@@ -1397,12 +2231,29 @@ program.name("reasonix").description("DeepSeek-native agent framework \u2014 bui
1397
2231
  program.command("chat").description("Interactive Ink TUI with live cache/cost panel.").option("-m, --model <id>", "DeepSeek model id", "deepseek-chat").option("-s, --system <prompt>", "System prompt (pinned in the immutable prefix)", DEFAULT_SYSTEM).option("--transcript <path>", "Write a JSONL transcript to this path").option(
1398
2232
  "--harvest",
1399
2233
  "Extract typed plan state from R1 reasoning (Pillar 2, adds a cheap V3 call per turn)"
1400
- ).action(async (opts) => {
2234
+ ).option(
2235
+ "--branch <n>",
2236
+ "Self-consistency: run N parallel samples per turn and pick the most confident (disables streaming; enables harvest)",
2237
+ (v) => Number.parseInt(v, 10)
2238
+ ).option(
2239
+ "--session <name>",
2240
+ "Use a named session (default: 'default'). Resume the same session next time."
2241
+ ).option("--no-session", "Disable session persistence for this run (ephemeral chat)").action(async (opts) => {
2242
+ let session;
2243
+ if (opts.session === false) {
2244
+ session = void 0;
2245
+ } else if (typeof opts.session === "string" && opts.session.length > 0) {
2246
+ session = opts.session;
2247
+ } else {
2248
+ session = "default";
2249
+ }
1401
2250
  await chatCommand({
1402
2251
  model: opts.model,
1403
2252
  system: opts.system,
1404
2253
  transcript: opts.transcript,
1405
- harvest: !!opts.harvest
2254
+ harvest: !!opts.harvest,
2255
+ branch: Number.isFinite(opts.branch) && opts.branch > 1 ? opts.branch : void 0,
2256
+ session
1406
2257
  });
1407
2258
  });
1408
2259
  program.command("run <task>").description("Run a single task non-interactively, streaming output.").option("-m, --model <id>", "DeepSeek model id", "deepseek-chat").option("-s, --system <prompt>", "System prompt", DEFAULT_SYSTEM).action(async (task, opts) => {