reasonix 0.0.5 → 0.2.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
@@ -778,6 +778,93 @@ function signature2(call) {
778
778
  return `${call.function?.name ?? ""}::${call.function?.arguments ?? ""}`;
779
779
  }
780
780
 
781
+ // src/session.ts
782
+ import {
783
+ appendFileSync,
784
+ chmodSync,
785
+ existsSync,
786
+ mkdirSync,
787
+ readFileSync,
788
+ readdirSync,
789
+ statSync,
790
+ unlinkSync
791
+ } from "fs";
792
+ import { homedir } from "os";
793
+ import { dirname, join } from "path";
794
+ function sessionsDir() {
795
+ return join(homedir(), ".reasonix", "sessions");
796
+ }
797
+ function sessionPath(name) {
798
+ return join(sessionsDir(), `${sanitizeName(name)}.jsonl`);
799
+ }
800
+ function sanitizeName(name) {
801
+ const cleaned = name.replace(/[^\w\-\u4e00-\u9fa5]/g, "_").slice(0, 64);
802
+ return cleaned || "default";
803
+ }
804
+ function loadSessionMessages(name) {
805
+ const path = sessionPath(name);
806
+ if (!existsSync(path)) return [];
807
+ try {
808
+ const raw = readFileSync(path, "utf8");
809
+ const out = [];
810
+ for (const line of raw.split(/\r?\n/)) {
811
+ const trimmed = line.trim();
812
+ if (!trimmed) continue;
813
+ try {
814
+ const msg = JSON.parse(trimmed);
815
+ if (msg && typeof msg === "object" && "role" in msg) out.push(msg);
816
+ } catch {
817
+ }
818
+ }
819
+ return out;
820
+ } catch {
821
+ return [];
822
+ }
823
+ }
824
+ function appendSessionMessage(name, message) {
825
+ const path = sessionPath(name);
826
+ mkdirSync(dirname(path), { recursive: true });
827
+ appendFileSync(path, `${JSON.stringify(message)}
828
+ `, "utf8");
829
+ try {
830
+ chmodSync(path, 384);
831
+ } catch {
832
+ }
833
+ }
834
+ function listSessions() {
835
+ const dir = sessionsDir();
836
+ if (!existsSync(dir)) return [];
837
+ try {
838
+ const files = readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
839
+ return files.map((file) => {
840
+ const path = join(dir, file);
841
+ const stat = statSync(path);
842
+ const name = file.replace(/\.jsonl$/, "");
843
+ const messageCount = countLines(path);
844
+ return { name, path, size: stat.size, messageCount, mtime: stat.mtime };
845
+ }).sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
846
+ } catch {
847
+ return [];
848
+ }
849
+ }
850
+ function deleteSession(name) {
851
+ const path = sessionPath(name);
852
+ try {
853
+ unlinkSync(path);
854
+ return true;
855
+ } catch {
856
+ return false;
857
+ }
858
+ }
859
+ function countLines(path) {
860
+ try {
861
+ const raw = readFileSync(path, "utf8");
862
+ return raw.split(/\r?\n/).filter((l) => l.trim()).length;
863
+ } catch {
864
+ return 0;
865
+ }
866
+ }
867
+
781
868
  // src/telemetry.ts
782
869
  var DEEPSEEK_PRICING = {
783
870
  "deepseek-chat": { inputCacheHit: 0.07, inputCacheMiss: 0.27, output: 1.1 },
@@ -934,6 +1021,9 @@ var CacheFirstLoop = class {
934
1021
  harvestOptions;
935
1022
  branchEnabled;
936
1023
  branchOptions;
1024
+ sessionName;
1025
+ /** Number of messages that were pre-loaded from the session file. */
1026
+ resumedMessageCount;
937
1027
  _turn = 0;
938
1028
  _streamPreference;
939
1029
  constructor(opts) {
@@ -957,6 +1047,23 @@ var CacheFirstLoop = class {
957
1047
  this.stream = this.branchEnabled ? false : this._streamPreference;
958
1048
  const allowedNames = /* @__PURE__ */ new Set([...this.prefix.toolSpecs.map((s) => s.function.name)]);
959
1049
  this.repair = new ToolCallRepair({ allowedToolNames: allowedNames });
1050
+ this.sessionName = opts.session ?? null;
1051
+ if (this.sessionName) {
1052
+ const prior = loadSessionMessages(this.sessionName);
1053
+ for (const msg of prior) this.log.append(msg);
1054
+ this.resumedMessageCount = prior.length;
1055
+ } else {
1056
+ this.resumedMessageCount = 0;
1057
+ }
1058
+ }
1059
+ appendAndPersist(message) {
1060
+ this.log.append(message);
1061
+ if (this.sessionName) {
1062
+ try {
1063
+ appendSessionMessage(this.sessionName, message);
1064
+ } catch {
1065
+ }
1066
+ }
960
1067
  }
961
1068
  /**
962
1069
  * Reconfigure model/harvest/branch/stream mid-session. The loop's log,
@@ -1144,7 +1251,7 @@ var CacheFirstLoop = class {
1144
1251
  }
1145
1252
  const turnStats = this.stats.record(this._turn, this.model, usage ?? new Usage());
1146
1253
  if (pendingUser !== null) {
1147
- this.log.append({ role: "user", content: pendingUser });
1254
+ this.appendAndPersist({ role: "user", content: pendingUser });
1148
1255
  pendingUser = null;
1149
1256
  }
1150
1257
  this.scratch.reasoning = reasoningContent || null;
@@ -1153,7 +1260,7 @@ var CacheFirstLoop = class {
1153
1260
  toolCalls,
1154
1261
  reasoningContent || null
1155
1262
  );
1156
- this.log.append(this.assistantMessage(assistantContent, repairedCalls));
1263
+ this.appendAndPersist(this.assistantMessage(assistantContent, repairedCalls));
1157
1264
  yield {
1158
1265
  turn: this._turn,
1159
1266
  role: "assistant_final",
@@ -1171,13 +1278,19 @@ var CacheFirstLoop = class {
1171
1278
  const name = call.function?.name ?? "";
1172
1279
  const args = call.function?.arguments ?? "{}";
1173
1280
  const result = await this.tools.dispatch(name, args);
1174
- this.log.append({
1281
+ this.appendAndPersist({
1175
1282
  role: "tool",
1176
1283
  tool_call_id: call.id ?? "",
1177
1284
  name,
1178
1285
  content: result
1179
1286
  });
1180
- yield { turn: this._turn, role: "tool", content: result, toolName: name };
1287
+ yield {
1288
+ turn: this._turn,
1289
+ role: "tool",
1290
+ content: result,
1291
+ toolName: name,
1292
+ toolArgs: args
1293
+ };
1181
1294
  }
1182
1295
  }
1183
1296
  yield { turn: this._turn, role: "done", content: "[max_tool_iters reached]" };
@@ -1207,12 +1320,12 @@ function summarizeBranch(chosen, samples) {
1207
1320
  }
1208
1321
 
1209
1322
  // src/env.ts
1210
- import { readFileSync } from "fs";
1323
+ import { readFileSync as readFileSync2 } from "fs";
1211
1324
  import { resolve } from "path";
1212
1325
  function loadDotenv(path = ".env") {
1213
1326
  let raw;
1214
1327
  try {
1215
- raw = readFileSync(resolve(process.cwd(), path), "utf8");
1328
+ raw = readFileSync2(resolve(process.cwd(), path), "utf8");
1216
1329
  } catch {
1217
1330
  return;
1218
1331
  }
@@ -1230,16 +1343,439 @@ function loadDotenv(path = ".env") {
1230
1343
  }
1231
1344
  }
1232
1345
 
1346
+ // src/transcript.ts
1347
+ import { createWriteStream, readFileSync as readFileSync3 } from "fs";
1348
+ function recordFromLoopEvent(ev, extra) {
1349
+ const rec = {
1350
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
1351
+ turn: ev.turn,
1352
+ role: ev.role,
1353
+ content: ev.content
1354
+ };
1355
+ if (ev.toolName !== void 0) rec.tool = ev.toolName;
1356
+ if (ev.toolArgs !== void 0) rec.args = ev.toolArgs;
1357
+ if (ev.error !== void 0) rec.error = ev.error;
1358
+ if (ev.stats) {
1359
+ rec.usage = {
1360
+ prompt_tokens: ev.stats.usage.promptTokens,
1361
+ completion_tokens: ev.stats.usage.completionTokens,
1362
+ total_tokens: ev.stats.usage.totalTokens,
1363
+ prompt_cache_hit_tokens: ev.stats.usage.promptCacheHitTokens,
1364
+ prompt_cache_miss_tokens: ev.stats.usage.promptCacheMissTokens
1365
+ };
1366
+ rec.cost = ev.stats.cost;
1367
+ rec.model = ev.stats.model;
1368
+ rec.prefixHash = extra.prefixHash;
1369
+ } else if (ev.role === "assistant_final") {
1370
+ rec.model = extra.model;
1371
+ rec.prefixHash = extra.prefixHash;
1372
+ }
1373
+ return rec;
1374
+ }
1375
+ function writeRecord(stream, record) {
1376
+ stream.write(`${JSON.stringify(record)}
1377
+ `);
1378
+ }
1379
+ function writeMeta(stream, meta) {
1380
+ const line = { role: "_meta", meta };
1381
+ stream.write(`${JSON.stringify(line)}
1382
+ `);
1383
+ }
1384
+ function openTranscriptFile(path, meta) {
1385
+ const stream = createWriteStream(path, { flags: "a" });
1386
+ writeMeta(stream, meta);
1387
+ return stream;
1388
+ }
1389
+ function readTranscript(path) {
1390
+ const raw = readFileSync3(path, "utf8");
1391
+ return parseTranscript(raw);
1392
+ }
1393
+ function parseTranscript(raw) {
1394
+ const out = { meta: null, records: [] };
1395
+ for (const line of raw.split(/\r?\n/)) {
1396
+ const trimmed = line.trim();
1397
+ if (!trimmed) continue;
1398
+ let obj;
1399
+ try {
1400
+ obj = JSON.parse(trimmed);
1401
+ } catch {
1402
+ continue;
1403
+ }
1404
+ if (!obj || typeof obj !== "object") continue;
1405
+ const rec = obj;
1406
+ if (rec.role === "_meta" && rec.meta && typeof rec.meta === "object") {
1407
+ out.meta = rec.meta;
1408
+ continue;
1409
+ }
1410
+ if (typeof rec.ts === "string" && typeof rec.turn === "number" && typeof rec.role === "string" && typeof rec.content === "string") {
1411
+ out.records.push(rec);
1412
+ }
1413
+ }
1414
+ return out;
1415
+ }
1416
+
1417
+ // src/replay.ts
1418
+ function replayFromFile(path) {
1419
+ const parsed = readTranscript(path);
1420
+ return { parsed, stats: computeReplayStats(parsed.records) };
1421
+ }
1422
+ function computeReplayStats(records) {
1423
+ const turns = [];
1424
+ const models = /* @__PURE__ */ new Set();
1425
+ const prefixHashes = /* @__PURE__ */ new Set();
1426
+ let userTurns = 0;
1427
+ let toolCalls = 0;
1428
+ for (const rec of records) {
1429
+ if (rec.role === "user") userTurns++;
1430
+ else if (rec.role === "tool") toolCalls++;
1431
+ else if (rec.role === "assistant_final") {
1432
+ if (rec.model) models.add(rec.model);
1433
+ if (rec.prefixHash) prefixHashes.add(rec.prefixHash);
1434
+ if (rec.usage && rec.model) {
1435
+ const u = new Usage(
1436
+ rec.usage.prompt_tokens ?? 0,
1437
+ rec.usage.completion_tokens ?? 0,
1438
+ rec.usage.total_tokens ?? 0,
1439
+ rec.usage.prompt_cache_hit_tokens ?? 0,
1440
+ rec.usage.prompt_cache_miss_tokens ?? 0
1441
+ );
1442
+ turns.push({
1443
+ turn: rec.turn,
1444
+ model: rec.model,
1445
+ usage: u,
1446
+ // `rec.cost` wins when present — honors whatever the writer computed
1447
+ // even if pricing tables have since changed. Only recompute when
1448
+ // the transcript didn't record it (old format).
1449
+ cost: rec.cost ?? costUsd(rec.model, u),
1450
+ cacheHitRatio: u.cacheHitRatio
1451
+ });
1452
+ }
1453
+ }
1454
+ }
1455
+ return {
1456
+ perTurn: turns,
1457
+ models: [...models],
1458
+ prefixHashes: [...prefixHashes],
1459
+ userTurns,
1460
+ toolCalls,
1461
+ ...summarizeTurns(turns)
1462
+ };
1463
+ }
1464
+ function summarizeTurns(turns) {
1465
+ const totalCost = turns.reduce((s, t) => s + t.cost, 0);
1466
+ const totalClaude = turns.reduce((s, t) => s + claudeEquivalentCost(t.usage), 0);
1467
+ let hit = 0;
1468
+ let miss = 0;
1469
+ for (const t of turns) {
1470
+ hit += t.usage.promptCacheHitTokens;
1471
+ miss += t.usage.promptCacheMissTokens;
1472
+ }
1473
+ const cacheHitRatio = hit + miss > 0 ? hit / (hit + miss) : 0;
1474
+ const savingsVsClaude = totalClaude > 0 ? 1 - totalCost / totalClaude : 0;
1475
+ return {
1476
+ turns: turns.length,
1477
+ totalCostUsd: round2(totalCost, 6),
1478
+ claudeEquivalentUsd: round2(totalClaude, 6),
1479
+ savingsVsClaudePct: round2(savingsVsClaude * 100, 2),
1480
+ cacheHitRatio: round2(cacheHitRatio, 4)
1481
+ };
1482
+ }
1483
+ function round2(n, digits) {
1484
+ const f = 10 ** digits;
1485
+ return Math.round(n * f) / f;
1486
+ }
1487
+
1488
+ // src/diff.ts
1489
+ function diffTranscripts(a, b) {
1490
+ const aSide = {
1491
+ label: a.label,
1492
+ meta: a.parsed.meta,
1493
+ records: a.parsed.records,
1494
+ stats: computeReplayStats(a.parsed.records)
1495
+ };
1496
+ const bSide = {
1497
+ label: b.label,
1498
+ meta: b.parsed.meta,
1499
+ records: b.parsed.records,
1500
+ stats: computeReplayStats(b.parsed.records)
1501
+ };
1502
+ const aByTurn = groupByTurn(a.parsed.records);
1503
+ const bByTurn = groupByTurn(b.parsed.records);
1504
+ const turns = [.../* @__PURE__ */ new Set([...aByTurn.keys(), ...bByTurn.keys()])].sort((x, y) => x - y);
1505
+ const pairs = [];
1506
+ let firstDivergenceTurn = null;
1507
+ for (const turn of turns) {
1508
+ const aGroup = aByTurn.get(turn) ?? { assistant: void 0, tools: [] };
1509
+ const bGroup = bByTurn.get(turn) ?? { assistant: void 0, tools: [] };
1510
+ const aAssistant = aGroup.assistant;
1511
+ const bAssistant = bGroup.assistant;
1512
+ const aTools = aGroup.tools;
1513
+ const bTools = bGroup.tools;
1514
+ let kind;
1515
+ let divergenceNote;
1516
+ if (!aAssistant && bAssistant) kind = "only_in_b";
1517
+ else if (aAssistant && !bAssistant) kind = "only_in_a";
1518
+ else if (!aAssistant && !bAssistant)
1519
+ kind = "diverge";
1520
+ else {
1521
+ divergenceNote = classifyDivergence(aAssistant, bAssistant, aTools, bTools);
1522
+ kind = divergenceNote ? "diverge" : "match";
1523
+ }
1524
+ if (kind !== "match" && firstDivergenceTurn === null) firstDivergenceTurn = turn;
1525
+ pairs.push({ turn, aAssistant, bAssistant, aTools, bTools, kind, divergenceNote });
1526
+ }
1527
+ return { a: aSide, b: bSide, pairs, firstDivergenceTurn };
1528
+ }
1529
+ function classifyDivergence(a, b, aTools, bTools) {
1530
+ const aNames = aTools.map((t) => t.tool ?? "").sort();
1531
+ const bNames = bTools.map((t) => t.tool ?? "").sort();
1532
+ if (aNames.join(",") !== bNames.join(",")) {
1533
+ return `tool calls differ: A=[${aNames.join(",") || "\u2014"}] B=[${bNames.join(",") || "\u2014"}]`;
1534
+ }
1535
+ for (let i = 0; i < aTools.length; i++) {
1536
+ const at = aTools[i];
1537
+ const bt = bTools[i];
1538
+ if (at.tool !== bt.tool) continue;
1539
+ if ((at.args ?? "") !== (bt.args ?? "")) {
1540
+ return `"${at.tool}" args differ`;
1541
+ }
1542
+ }
1543
+ const simRatio = similarity(a.content, b.content);
1544
+ if (simRatio < 0.75) return `text similarity ${(simRatio * 100).toFixed(0)}%`;
1545
+ return void 0;
1546
+ }
1547
+ function similarity(a, b) {
1548
+ if (a === b) return 1;
1549
+ if (!a && !b) return 1;
1550
+ if (!a || !b) return 0;
1551
+ const maxLen = Math.max(a.length, b.length);
1552
+ if (maxLen > 2e3) return tokenOverlap(a, b);
1553
+ const dist = levenshtein(a, b);
1554
+ return 1 - dist / maxLen;
1555
+ }
1556
+ function tokenOverlap(a, b) {
1557
+ const ta = new Set(a.toLowerCase().split(/\s+/).filter(Boolean));
1558
+ const tb = new Set(b.toLowerCase().split(/\s+/).filter(Boolean));
1559
+ if (ta.size === 0 && tb.size === 0) return 1;
1560
+ let shared = 0;
1561
+ for (const t of ta) if (tb.has(t)) shared++;
1562
+ return 2 * shared / (ta.size + tb.size);
1563
+ }
1564
+ function levenshtein(a, b) {
1565
+ const m = a.length;
1566
+ const n = b.length;
1567
+ if (m === 0) return n;
1568
+ if (n === 0) return m;
1569
+ let prev = new Array(n + 1);
1570
+ let curr = new Array(n + 1);
1571
+ for (let j = 0; j <= n; j++) prev[j] = j;
1572
+ for (let i = 1; i <= m; i++) {
1573
+ curr[0] = i;
1574
+ for (let j = 1; j <= n; j++) {
1575
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
1576
+ curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
1577
+ }
1578
+ [prev, curr] = [curr, prev];
1579
+ }
1580
+ return prev[n];
1581
+ }
1582
+ function groupByTurn(records) {
1583
+ const out = /* @__PURE__ */ new Map();
1584
+ for (const rec of records) {
1585
+ if (rec.role === "user") continue;
1586
+ const g = out.get(rec.turn) ?? { tools: [] };
1587
+ if (rec.role === "assistant_final") g.assistant = rec;
1588
+ else if (rec.role === "tool") g.tools.push(rec);
1589
+ out.set(rec.turn, g);
1590
+ }
1591
+ return out;
1592
+ }
1593
+ function renderSummaryTable(report, _opts = {}) {
1594
+ const a = report.a;
1595
+ const b = report.b;
1596
+ const lines = [];
1597
+ lines.push("Comparing:");
1598
+ lines.push(` A ${a.label}`);
1599
+ lines.push(` B ${b.label}`);
1600
+ lines.push("");
1601
+ lines.push(row(["", "A", "B", "\u0394"], [20, 14, 14, 14]));
1602
+ lines.push(
1603
+ row(["\u2500".repeat(20), "\u2500".repeat(14), "\u2500".repeat(14), "\u2500".repeat(14)], [20, 14, 14, 14])
1604
+ );
1605
+ lines.push(statRow("model calls", a.stats.turns, b.stats.turns));
1606
+ lines.push(statRow("user turns", a.stats.userTurns, b.stats.userTurns));
1607
+ lines.push(statRow("tool calls", a.stats.toolCalls, b.stats.toolCalls));
1608
+ lines.push(
1609
+ row(
1610
+ [
1611
+ "cache hit",
1612
+ `${pct(a.stats.cacheHitRatio)}`,
1613
+ `${pct(b.stats.cacheHitRatio)}`,
1614
+ signPct(b.stats.cacheHitRatio - a.stats.cacheHitRatio)
1615
+ ],
1616
+ [20, 14, 14, 14]
1617
+ )
1618
+ );
1619
+ lines.push(
1620
+ row(
1621
+ [
1622
+ "cost (USD)",
1623
+ `$${a.stats.totalCostUsd.toFixed(6)}`,
1624
+ `$${b.stats.totalCostUsd.toFixed(6)}`,
1625
+ costDelta(a.stats.totalCostUsd, b.stats.totalCostUsd)
1626
+ ],
1627
+ [20, 14, 14, 14]
1628
+ )
1629
+ );
1630
+ lines.push(statRow("prefix hashes", a.stats.prefixHashes.length, b.stats.prefixHashes.length));
1631
+ lines.push("");
1632
+ const aPrefixStable = a.stats.prefixHashes.length <= 1;
1633
+ const bPrefixStable = b.stats.prefixHashes.length <= 1;
1634
+ if (aPrefixStable !== bPrefixStable) {
1635
+ const stable = aPrefixStable ? "A" : "B";
1636
+ const churn = aPrefixStable ? "B" : "A";
1637
+ const churnCount = aPrefixStable ? b.stats.prefixHashes.length : a.stats.prefixHashes.length;
1638
+ lines.push(
1639
+ `prefix stability: ${stable} stayed byte-stable across ${Math.max(
1640
+ a.stats.turns,
1641
+ b.stats.turns
1642
+ )} turns; ${churn} churned ${churnCount} distinct prefixes.`
1643
+ );
1644
+ lines.push("");
1645
+ } else if (a.stats.prefixHashes[0] && a.stats.prefixHashes[0] === b.stats.prefixHashes[0]) {
1646
+ lines.push(
1647
+ `prefix: A and B share the same prefix hash (${a.stats.prefixHashes[0].slice(0, 12)}\u2026) \u2014 cache delta is attributable to log stability, not prompt change.`
1648
+ );
1649
+ lines.push("");
1650
+ }
1651
+ if (report.firstDivergenceTurn !== null) {
1652
+ const p = report.pairs.find((p2) => p2.turn === report.firstDivergenceTurn);
1653
+ lines.push(
1654
+ `first divergence: turn ${report.firstDivergenceTurn} \u2014 ${p?.divergenceNote ?? "?"}`
1655
+ );
1656
+ if (p?.aAssistant) lines.push(` A \u2192 ${truncate(p.aAssistant.content, 100)}`);
1657
+ if (p?.bAssistant) lines.push(` B \u2192 ${truncate(p.bAssistant.content, 100)}`);
1658
+ } else {
1659
+ lines.push("no material divergence detected (texts within similarity threshold).");
1660
+ }
1661
+ return lines.join("\n");
1662
+ }
1663
+ function renderMarkdown(report) {
1664
+ const a = report.a;
1665
+ const b = report.b;
1666
+ const out = [];
1667
+ out.push(`# Transcript diff: ${a.label} vs ${b.label}`);
1668
+ out.push("");
1669
+ if (a.meta || b.meta) {
1670
+ out.push("## Meta");
1671
+ out.push("");
1672
+ out.push(`| | ${a.label} | ${b.label} |`);
1673
+ out.push("|---|---|---|");
1674
+ out.push(`| source | ${a.meta?.source ?? "\u2014"} | ${b.meta?.source ?? "\u2014"} |`);
1675
+ out.push(`| model | ${a.meta?.model ?? "\u2014"} | ${b.meta?.model ?? "\u2014"} |`);
1676
+ out.push(`| task | ${a.meta?.task ?? "\u2014"} | ${b.meta?.task ?? "\u2014"} |`);
1677
+ out.push(`| startedAt | ${a.meta?.startedAt ?? "\u2014"} | ${b.meta?.startedAt ?? "\u2014"} |`);
1678
+ out.push("");
1679
+ }
1680
+ out.push("## Summary");
1681
+ out.push("");
1682
+ out.push(`| metric | ${a.label} | ${b.label} | delta |`);
1683
+ out.push("|---|---:|---:|---:|");
1684
+ out.push(
1685
+ `| model calls | ${a.stats.turns} | ${b.stats.turns} | ${signed(b.stats.turns - a.stats.turns)} |`
1686
+ );
1687
+ out.push(
1688
+ `| user turns | ${a.stats.userTurns} | ${b.stats.userTurns} | ${signed(b.stats.userTurns - a.stats.userTurns)} |`
1689
+ );
1690
+ out.push(
1691
+ `| tool calls | ${a.stats.toolCalls} | ${b.stats.toolCalls} | ${signed(b.stats.toolCalls - a.stats.toolCalls)} |`
1692
+ );
1693
+ out.push(
1694
+ `| cache hit | ${pct(a.stats.cacheHitRatio)} | ${pct(b.stats.cacheHitRatio)} | **${signPct(b.stats.cacheHitRatio - a.stats.cacheHitRatio)}** |`
1695
+ );
1696
+ out.push(
1697
+ `| cost (USD) | $${a.stats.totalCostUsd.toFixed(6)} | $${b.stats.totalCostUsd.toFixed(6)} | ${costDelta(a.stats.totalCostUsd, b.stats.totalCostUsd)} |`
1698
+ );
1699
+ out.push(
1700
+ `| prefix hashes | ${a.stats.prefixHashes.length} | ${b.stats.prefixHashes.length} | \u2014 |`
1701
+ );
1702
+ out.push("");
1703
+ out.push("## Turn-by-turn");
1704
+ out.push("");
1705
+ out.push(`| turn | kind | ${a.label} tool calls | ${b.label} tool calls | note |`);
1706
+ out.push("|---:|:---:|---|---|---|");
1707
+ for (const p of report.pairs) {
1708
+ const aTools = p.aTools.map((t) => t.tool).filter(Boolean).join(", ") || "\u2014";
1709
+ const bTools = p.bTools.map((t) => t.tool).filter(Boolean).join(", ") || "\u2014";
1710
+ out.push(`| ${p.turn} | ${p.kind} | ${aTools} | ${bTools} | ${p.divergenceNote ?? ""} |`);
1711
+ }
1712
+ out.push("");
1713
+ if (report.firstDivergenceTurn !== null) {
1714
+ const p = report.pairs.find((x) => x.turn === report.firstDivergenceTurn);
1715
+ out.push(`## First divergence (turn ${report.firstDivergenceTurn})`);
1716
+ out.push("");
1717
+ out.push(p?.divergenceNote ?? "");
1718
+ out.push("");
1719
+ if (p?.aAssistant) {
1720
+ out.push(`**${a.label}:**`);
1721
+ out.push("");
1722
+ out.push("```");
1723
+ out.push(p.aAssistant.content);
1724
+ out.push("```");
1725
+ out.push("");
1726
+ }
1727
+ if (p?.bAssistant) {
1728
+ out.push(`**${b.label}:**`);
1729
+ out.push("");
1730
+ out.push("```");
1731
+ out.push(p.bAssistant.content);
1732
+ out.push("```");
1733
+ out.push("");
1734
+ }
1735
+ }
1736
+ return out.join("\n");
1737
+ }
1738
+ function row(cols, widths) {
1739
+ return cols.map((c, i) => padRight(c, widths[i] ?? c.length)).join(" ");
1740
+ }
1741
+ function statRow(label, av, bv) {
1742
+ return row([label, `${av}`, `${bv}`, signed(bv - av)], [20, 14, 14, 14]);
1743
+ }
1744
+ function padRight(s, w) {
1745
+ return s.length >= w ? s : s + " ".repeat(w - s.length);
1746
+ }
1747
+ function signed(n) {
1748
+ if (n === 0) return "0";
1749
+ return `${n > 0 ? "+" : ""}${n}`;
1750
+ }
1751
+ function signPct(diff) {
1752
+ if (diff === 0) return "0pp";
1753
+ const s = (diff * 100).toFixed(1);
1754
+ return `${diff > 0 ? "+" : ""}${s}pp`;
1755
+ }
1756
+ function pct(x) {
1757
+ return `${(x * 100).toFixed(1)}%`;
1758
+ }
1759
+ function costDelta(a, b) {
1760
+ if (a === 0 && b === 0) return "\u2014";
1761
+ if (a === 0) return "new";
1762
+ const pctChange = (b - a) / a * 100;
1763
+ return `${pctChange > 0 ? "+" : ""}${pctChange.toFixed(1)}%`;
1764
+ }
1765
+ function truncate(s, n) {
1766
+ return s.length > n ? `${s.slice(0, n)}\u2026` : s;
1767
+ }
1768
+
1233
1769
  // src/config.ts
1234
- import { chmodSync, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
1235
- import { homedir } from "os";
1236
- import { dirname, join } from "path";
1770
+ import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync } from "fs";
1771
+ import { homedir as homedir2 } from "os";
1772
+ import { dirname as dirname2, join as join2 } from "path";
1237
1773
  function defaultConfigPath() {
1238
- return join(homedir(), ".reasonix", "config.json");
1774
+ return join2(homedir2(), ".reasonix", "config.json");
1239
1775
  }
1240
1776
  function readConfig(path = defaultConfigPath()) {
1241
1777
  try {
1242
- const raw = readFileSync2(path, "utf8");
1778
+ const raw = readFileSync4(path, "utf8");
1243
1779
  const parsed = JSON.parse(raw);
1244
1780
  if (parsed && typeof parsed === "object") return parsed;
1245
1781
  } catch {
@@ -1247,10 +1783,10 @@ function readConfig(path = defaultConfigPath()) {
1247
1783
  return {};
1248
1784
  }
1249
1785
  function writeConfig(cfg, path = defaultConfigPath()) {
1250
- mkdirSync(dirname(path), { recursive: true });
1786
+ mkdirSync2(dirname2(path), { recursive: true });
1251
1787
  writeFileSync(path, JSON.stringify(cfg, null, 2), "utf8");
1252
1788
  try {
1253
- chmodSync(path, 384);
1789
+ chmodSync2(path, 384);
1254
1790
  } catch {
1255
1791
  }
1256
1792
  }
@@ -1274,7 +1810,7 @@ function redactKey(key) {
1274
1810
  }
1275
1811
 
1276
1812
  // src/index.ts
1277
- var VERSION = "0.0.1";
1813
+ var VERSION = "0.2.0";
1278
1814
  export {
1279
1815
  AppendOnlyLog,
1280
1816
  CacheFirstLoop,
@@ -1289,25 +1825,44 @@ export {
1289
1825
  VolatileScratch,
1290
1826
  aggregateBranchUsage,
1291
1827
  analyzeSchema,
1828
+ appendSessionMessage,
1292
1829
  claudeEquivalentCost,
1830
+ computeReplayStats,
1293
1831
  costUsd,
1294
1832
  defaultConfigPath,
1295
1833
  defaultSelector,
1834
+ deleteSession,
1835
+ diffTranscripts,
1296
1836
  emptyPlanState,
1297
1837
  fetchWithRetry,
1298
1838
  flattenSchema,
1299
1839
  harvest,
1300
1840
  isPlanStateEmpty,
1301
1841
  isPlausibleKey,
1842
+ listSessions,
1302
1843
  loadApiKey,
1303
1844
  loadDotenv,
1845
+ loadSessionMessages,
1304
1846
  nestArguments,
1847
+ openTranscriptFile,
1848
+ parseTranscript,
1305
1849
  readConfig,
1850
+ readTranscript,
1851
+ recordFromLoopEvent,
1306
1852
  redactKey,
1853
+ renderMarkdown as renderDiffMarkdown,
1854
+ renderSummaryTable as renderDiffSummary,
1307
1855
  repairTruncatedJson,
1856
+ replayFromFile,
1308
1857
  runBranches,
1858
+ sanitizeName as sanitizeSessionName,
1309
1859
  saveApiKey,
1310
1860
  scavengeToolCalls,
1311
- writeConfig
1861
+ sessionPath,
1862
+ sessionsDir,
1863
+ similarity,
1864
+ writeConfig,
1865
+ writeMeta,
1866
+ writeRecord
1312
1867
  };
1313
1868
  //# sourceMappingURL=index.js.map