@cognisos/liminal 2.5.0 → 2.6.0-beta.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/bin.js CHANGED
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
4
6
  var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
5
7
  get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
6
8
  }) : x)(function(x) {
@@ -14,6 +16,15 @@ var __export = (target, all) => {
14
16
  for (var name in all)
15
17
  __defProp(target, name, { get: all[name], enumerable: true });
16
18
  };
19
+ var __copyProps = (to, from, except, desc) => {
20
+ if (from && typeof from === "object" || typeof from === "function") {
21
+ for (let key of __getOwnPropNames(from))
22
+ if (!__hasOwnProp.call(to, key) && key !== except)
23
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
24
+ }
25
+ return to;
26
+ };
27
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
17
28
 
18
29
  // src/ui/format.ts
19
30
  function disableColor() {
@@ -215,7 +226,7 @@ var init_version = __esm({
215
226
  "src/version.ts"() {
216
227
  "use strict";
217
228
  init_format();
218
- VERSION = true ? "2.5.0" : "2.5.0";
229
+ VERSION = true ? "2.6.0-beta.0" : "2.5.0";
219
230
  BANNER_LINES = [
220
231
  " ___ ___ _____ ______ ___ ________ ________ ___",
221
232
  "|\\ \\ |\\ \\|\\ _ \\ _ \\|\\ \\|\\ ___ \\|\\ __ \\|\\ \\",
@@ -727,6 +738,35 @@ var init_pipeline = __esm({
727
738
  }
728
739
  });
729
740
 
741
+ // src/rsc/tokenizer.ts
742
+ var tokenizer_exports = {};
743
+ __export(tokenizer_exports, {
744
+ countTokens: () => countTokens,
745
+ countTokensBytes: () => countTokensBytes
746
+ });
747
+ function countTokens(text) {
748
+ if (!_countTokens) {
749
+ try {
750
+ const mod = __require("@anthropic-ai/tokenizer");
751
+ _countTokens = mod.countTokens;
752
+ } catch {
753
+ _countTokens = (t) => Math.ceil(Buffer.byteLength(t, "utf-8") / 4);
754
+ }
755
+ }
756
+ return _countTokens(text);
757
+ }
758
+ function countTokensBytes(bytes) {
759
+ const text = Buffer.from(bytes).toString("utf-8");
760
+ return countTokens(text);
761
+ }
762
+ var _countTokens;
763
+ var init_tokenizer = __esm({
764
+ "src/rsc/tokenizer.ts"() {
765
+ "use strict";
766
+ _countTokens = null;
767
+ }
768
+ });
769
+
730
770
  // src/daemon/lifecycle.ts
731
771
  import { readFileSync as readFileSync6, writeFileSync as writeFileSync4, unlinkSync as unlinkSync3, existsSync as existsSync8 } from "fs";
732
772
  import { fork } from "child_process";
@@ -828,10 +868,35 @@ var init_aggregator = __esm({
828
868
  tools = /* @__PURE__ */ new Map();
829
869
  cursorMetrics = null;
830
870
  costPerMillionTokens;
871
+ /** Actual input_tokens from Anthropic API responses (verified ground truth) */
872
+ actualInputTokensTotal = 0;
873
+ /** Actual output_tokens from Anthropic API responses */
874
+ actualOutputTokensTotal = 0;
875
+ /** Pre-compression input token count from tokenizer */
876
+ originalInputEstimateTotal = 0;
877
+ /** Verified tokens saved: countTokens(pre) - countTokens(post) */
878
+ verifiedSavedTotal = 0;
879
+ /** Number of API responses with verified usage data */
880
+ verifiedRequestCount = 0;
831
881
  constructor(costPerMillionTokens = DEFAULT_COST_PER_MILLION_TOKENS) {
832
882
  this.costPerMillionTokens = costPerMillionTokens;
833
883
  }
834
884
  // ── Recording events ──────────────────────────────────────────────
885
+ /**
886
+ * Record verified token counts from an Anthropic API response.
887
+ *
888
+ * @param actualInput - usage.input_tokens from Anthropic (post-compression ground truth)
889
+ * @param actualOutput - usage.output_tokens from Anthropic (generated tokens ground truth)
890
+ * @param verifiedSaved - tokens saved, computed as countTokens(pre) - countTokens(post) using real tokenizer
891
+ * @param originalInputTokens - token count of full request before compression
892
+ */
893
+ recordApiUsage(actualInput, actualOutput, verifiedSaved, originalInputTokens) {
894
+ this.actualInputTokensTotal += actualInput;
895
+ this.actualOutputTokensTotal += actualOutput;
896
+ this.originalInputEstimateTotal += originalInputTokens;
897
+ this.verifiedSavedTotal += verifiedSaved;
898
+ this.verifiedRequestCount++;
899
+ }
835
900
  recordCompression(toolId, tokensProcessed, tokensSaved, latencyMs) {
836
901
  const m = this.getOrCreateTool(toolId);
837
902
  m.calls++;
@@ -899,10 +964,18 @@ var init_aggregator = __esm({
899
964
  for (const [id, m] of this.tools) {
900
965
  byTool[id] = { ...m };
901
966
  }
967
+ const verifiedInputSaved = this.verifiedSavedTotal;
968
+ const verifiedSavingsRate = this.originalInputEstimateTotal > 0 ? verifiedInputSaved / this.originalInputEstimateTotal : 0;
902
969
  return {
903
970
  sessionStartedAt: this.startedAt.toISOString(),
904
971
  uptimeMs,
905
972
  ...totals,
973
+ actualInputTokens: this.actualInputTokensTotal,
974
+ actualOutputTokens: this.actualOutputTokensTotal,
975
+ originalInputEstimate: this.originalInputEstimateTotal,
976
+ verifiedInputSaved,
977
+ verifiedSavingsRate,
978
+ verifiedRequestCount: this.verifiedRequestCount,
906
979
  savingsRate: rate,
907
980
  contextExtension: this.contextExtension(rate),
908
981
  estimatedCostSavedUsd: this.estimatedCostSaved(totals.tokensSaved),
@@ -1007,6 +1080,101 @@ var init_store = __esm({
1007
1080
  }
1008
1081
  });
1009
1082
 
1083
+ // src/cursor/stats.ts
1084
+ var stats_exports = {};
1085
+ __export(stats_exports, {
1086
+ parseCursorHookStats: () => parseCursorHookStats,
1087
+ readCursorLogEntries: () => readCursorLogEntries
1088
+ });
1089
+ import { existsSync as existsSync10, readFileSync as readFileSync8, readdirSync } from "fs";
1090
+ import { join as join7 } from "path";
1091
+ function parseCursorHookStats(cwd = process.cwd()) {
1092
+ const logPath = join7(cwd, ".fabric", "compress.log");
1093
+ if (!existsSync10(logPath)) return null;
1094
+ let compressions = 0, errors = 0;
1095
+ let tokensProcessed = 0, tokensSaved = 0;
1096
+ let apiMsSumMs = 0;
1097
+ const files = /* @__PURE__ */ new Set();
1098
+ const content = readFileSync8(logPath, "utf-8").trim();
1099
+ if (!content) return null;
1100
+ for (const line of content.split("\n")) {
1101
+ try {
1102
+ const entry = JSON.parse(line);
1103
+ if (entry.type === "compressed") {
1104
+ compressions++;
1105
+ tokensProcessed += entry.inputTokens ?? Math.ceil((entry.inputSize ?? 0) / 3);
1106
+ tokensSaved += entry.tokensSaved ?? 0;
1107
+ apiMsSumMs += entry.apiMs ?? 0;
1108
+ files.add(entry.file);
1109
+ } else if (entry.type === "error") {
1110
+ errors++;
1111
+ }
1112
+ } catch {
1113
+ }
1114
+ }
1115
+ let cacheCount = 0;
1116
+ try {
1117
+ cacheCount = countFilesRecursive(join7(cwd, ".fabric", "cache"));
1118
+ } catch {
1119
+ }
1120
+ return {
1121
+ files: files.size,
1122
+ compressions,
1123
+ errors,
1124
+ tokensProcessed,
1125
+ tokensSaved,
1126
+ apiMsSumMs,
1127
+ cacheCount
1128
+ };
1129
+ }
1130
+ function readCursorLogEntries(maxEntries = 50, cwd = process.cwd()) {
1131
+ const logPath = join7(cwd, ".fabric", "compress.log");
1132
+ if (!existsSync10(logPath)) return [];
1133
+ const content = readFileSync8(logPath, "utf-8").trim();
1134
+ if (!content) return [];
1135
+ const lines = content.split("\n");
1136
+ const recent = lines.slice(-maxEntries);
1137
+ const entries = [];
1138
+ for (const line of recent) {
1139
+ try {
1140
+ const entry = JSON.parse(line);
1141
+ const ts = entry.ts ? new Date(entry.ts) : /* @__PURE__ */ new Date();
1142
+ const time = ts.toTimeString().slice(0, 8);
1143
+ if (entry.type === "compressed") {
1144
+ const pct = entry.savedPct ?? (entry.inputTokens && entry.tokensSaved ? +(entry.tokensSaved / entry.inputTokens * 100).toFixed(1) : 0);
1145
+ entries.push({
1146
+ ts: entry.ts,
1147
+ line: `[${time}] [CURSOR] ${entry.file} \u2192 ${entry.tokensSaved} tok saved (${pct}%) ${entry.apiMs}ms`
1148
+ });
1149
+ } else if (entry.type === "error") {
1150
+ entries.push({
1151
+ ts: entry.ts,
1152
+ line: `[${time}] [CURSOR] ${entry.file} \u2192 ERROR: ${entry.error ?? "unknown"}`
1153
+ });
1154
+ }
1155
+ } catch {
1156
+ }
1157
+ }
1158
+ return entries;
1159
+ }
1160
+ function countFilesRecursive(dir) {
1161
+ if (!existsSync10(dir)) return 0;
1162
+ let count = 0;
1163
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
1164
+ if (entry.isDirectory()) {
1165
+ count += countFilesRecursive(join7(dir, entry.name));
1166
+ } else {
1167
+ count++;
1168
+ }
1169
+ }
1170
+ return count;
1171
+ }
1172
+ var init_stats = __esm({
1173
+ "src/cursor/stats.ts"() {
1174
+ "use strict";
1175
+ }
1176
+ });
1177
+
1010
1178
  // src/ui/screen.ts
1011
1179
  var SEQ, Screen;
1012
1180
  var init_screen = __esm({
@@ -1402,15 +1570,39 @@ var init_stats_view = __esm({
1402
1570
  lines.push(formatRow("Savings Rate", sRate, aRate, colW));
1403
1571
  lines.push(formatRow("Context Extension", sExt, aExt, colW));
1404
1572
  lines.push(blank());
1573
+ lines.push(divider("Verified (Anthropic API)", w));
1574
+ lines.push(blank());
1575
+ if (health && health.verifiedRequestCount && health.verifiedRequestCount > 0) {
1576
+ const actualIn = health.actualInputTokens ?? 0;
1577
+ const actualOut = health.actualOutputTokens ?? 0;
1578
+ const origEst = health.originalInputEstimate ?? 0;
1579
+ const saved = health.verifiedInputSaved ?? 0;
1580
+ const rate = health.verifiedSavingsRate ?? 0;
1581
+ const reqs = health.verifiedRequestCount;
1582
+ lines.push(formatRow("Input (actual)", formatNum(actualIn), "\u2014", colW));
1583
+ lines.push(formatRow("Input (pre-comp)", formatNum(origEst), "\u2014", colW));
1584
+ lines.push(formatRow("Input Saved", formatNum(saved), "\u2014", colW));
1585
+ lines.push(formatRow("Input Savings Rate", `${(rate * 100).toFixed(1)}%`, "\u2014", colW));
1586
+ lines.push(blank());
1587
+ lines.push(formatRow("Output (actual)", formatNum(actualOut), "\u2014", colW));
1588
+ lines.push(formatRow("Total (in + out)", formatNum(actualIn + actualOut), "\u2014", colW));
1589
+ lines.push(blank());
1590
+ lines.push(formatRow("Verified Requests", String(reqs), "\u2014", colW));
1591
+ } else {
1592
+ lines.push(` ${c.dim}No verified data yet \u2014 waiting for API responses${c.reset}`);
1593
+ }
1594
+ lines.push(blank());
1405
1595
  lines.push(divider("Cost Impact", w));
1406
1596
  lines.push(blank());
1407
1597
  lines.push(hdr);
1408
1598
  lines.push(sep);
1409
1599
  lines.push(formatRow("Est. Cost Saved", sCost, aCost, colW));
1410
1600
  lines.push(blank());
1601
+ let byToolRendered = false;
1411
1602
  if (health && health.sessions.length > 0) {
1412
1603
  lines.push(divider("By Tool", w));
1413
1604
  lines.push(blank());
1605
+ byToolRendered = true;
1414
1606
  const byTool = /* @__PURE__ */ new Map();
1415
1607
  for (const s of health.sessions) {
1416
1608
  const existing = byTool.get(s.connector) ?? { calls: 0, compressed: 0, failed: 0, processed: 0, saved: 0, p95: null };
@@ -1432,6 +1624,19 @@ var init_stats_view = __esm({
1432
1624
  lines.push(blank());
1433
1625
  }
1434
1626
  }
1627
+ if (health?.cursor) {
1628
+ const cur = health.cursor;
1629
+ if (!byToolRendered) {
1630
+ lines.push(divider("By Tool", w));
1631
+ lines.push(blank());
1632
+ }
1633
+ const curPct = cur.tokensProcessed > 0 ? `${(cur.tokensSaved / cur.tokensProcessed * 100).toFixed(1)}%` : "\u2014";
1634
+ const avgMs = cur.compressions > 0 ? Math.round(cur.apiMsSumMs / cur.compressions) : 0;
1635
+ lines.push(`${c.bold}${formatLabel("cursor")}${c.reset}`);
1636
+ lines.push(` Files: ${cur.files} unique (${cur.compressions} compressions${cur.errors > 0 ? `, ${c.red}${cur.errors} errors${c.reset}` : ""}) | Saved: ${formatNum(cur.tokensSaved)} tok (${curPct})`);
1637
+ lines.push(` Cache: ${cur.cacheCount} files | Avg API: ${avgMs}ms/file`);
1638
+ lines.push(blank());
1639
+ }
1435
1640
  if (cum.sessionCount > 0) {
1436
1641
  lines.push(`${c.dim}${cum.sessionCount} session${cum.sessionCount !== 1 ? "s" : ""} recorded${c.reset}`);
1437
1642
  }
@@ -1473,6 +1678,17 @@ var init_config_view = __esm({
1473
1678
  });
1474
1679
 
1475
1680
  // src/ui/views/logs-view.ts
1681
+ function getCursorLogs() {
1682
+ if (!_readCursorLogs) {
1683
+ try {
1684
+ const mod = (init_stats(), __toCommonJS(stats_exports));
1685
+ _readCursorLogs = () => mod.readCursorLogEntries(50);
1686
+ } catch {
1687
+ _readCursorLogs = () => [];
1688
+ }
1689
+ }
1690
+ return _readCursorLogs();
1691
+ }
1476
1692
  function colorizeLog(line, maxWidth) {
1477
1693
  const display = line.length > maxWidth ? line.slice(0, maxWidth - 1) + "\u2026" : line;
1478
1694
  const match = display.match(/^\[(\d{2}:\d{2}:\d{2}(?:\.\d+)?)\]\s*(.*)$/);
@@ -1495,21 +1711,33 @@ function colorizeLog(line, maxWidth) {
1495
1711
  }
1496
1712
  return `${c.dim}${ts} ${rest}${c.reset}`;
1497
1713
  }
1498
- var logsView;
1714
+ var _readCursorLogs, logsView;
1499
1715
  var init_logs_view = __esm({
1500
1716
  "src/ui/views/logs-view.ts"() {
1501
1717
  "use strict";
1502
1718
  init_format();
1503
1719
  init_layout();
1720
+ _readCursorLogs = null;
1504
1721
  logsView = {
1505
1722
  id: "logs",
1506
1723
  label: "Logs",
1507
1724
  render(state, size) {
1508
1725
  const lines = [];
1509
1726
  const w = Math.max(40, size.cols - 6);
1510
- lines.push(divider("Daemon Logs", w));
1727
+ lines.push(divider("All Logs", w));
1511
1728
  lines.push(blank());
1512
- if (state.recentLogs.length === 0) {
1729
+ const cursorEntries = getCursorLogs();
1730
+ const allLogs = [];
1731
+ for (const line of state.recentLogs) {
1732
+ const match = line.match(/^\[(\d{2}:\d{2}:\d{2}(?:\.\d+)?)\]/);
1733
+ allLogs.push({ ts: match?.[1] ?? "99:99:99", line, source: "daemon" });
1734
+ }
1735
+ for (const entry of cursorEntries) {
1736
+ allLogs.push({ ts: entry.ts, line: entry.line, source: "cursor" });
1737
+ }
1738
+ const daemonLogs = state.recentLogs;
1739
+ const combined = [...daemonLogs, ...cursorEntries.map((e) => e.line)];
1740
+ if (combined.length === 0) {
1513
1741
  if (!state.daemonRunning) {
1514
1742
  lines.push(`${c.dim}Daemon not running \u2014 no logs to show.${c.reset}`);
1515
1743
  } else {
@@ -1519,12 +1747,12 @@ var init_logs_view = __esm({
1519
1747
  return lines;
1520
1748
  }
1521
1749
  const maxLines = Math.max(5, size.rows - 7);
1522
- const tail = state.recentLogs.slice(-maxLines);
1750
+ const tail = combined.slice(-maxLines);
1523
1751
  for (const line of tail) {
1524
1752
  lines.push(colorizeLog(line, w));
1525
1753
  }
1526
1754
  lines.push(blank());
1527
- lines.push(`${c.dim}Showing last ${tail.length} lines \u2014 refreshes every 2s${c.reset}`);
1755
+ lines.push(`${c.dim}Showing last ${tail.length} lines (daemon + cursor) \u2014 refreshes every 2s${c.reset}`);
1528
1756
  return lines;
1529
1757
  }
1530
1758
  };
@@ -1536,7 +1764,7 @@ var hub_exports = {};
1536
1764
  __export(hub_exports, {
1537
1765
  runHub: () => runHub
1538
1766
  });
1539
- import { existsSync as existsSync15, statSync as statSync3 } from "fs";
1767
+ import { existsSync as existsSync16, statSync as statSync3 } from "fs";
1540
1768
  function createInitialState() {
1541
1769
  const state = {
1542
1770
  health: null,
@@ -1597,7 +1825,7 @@ async function refreshState(state) {
1597
1825
  }
1598
1826
  function tailLogFile(maxLines) {
1599
1827
  try {
1600
- if (!existsSync15(LOG_FILE)) return [];
1828
+ if (!existsSync16(LOG_FILE)) return [];
1601
1829
  const stat = statSync3(LOG_FILE);
1602
1830
  const readSize = Math.min(stat.size, 32 * 1024);
1603
1831
  if (readSize === 0) return [];
@@ -1907,18 +2135,37 @@ async function createApiKey(accessToken, userId, source = "cli") {
1907
2135
  );
1908
2136
  const apiKey = `${prefix}${randomBytes(32).toString("hex")}`;
1909
2137
  const keyHash = createHash("sha256").update(apiKey).digest("hex");
2138
+ let projectId;
2139
+ let orgId;
2140
+ try {
2141
+ const projRes = await fetch(
2142
+ `${SUPABASE_URL}/rest/v1/projects?id=eq.${userId}&select=id,org_id&limit=1`,
2143
+ { headers: supabaseHeaders(accessToken), signal: AbortSignal.timeout(1e4) }
2144
+ );
2145
+ if (projRes.ok) {
2146
+ const rows = await projRes.json();
2147
+ if (Array.isArray(rows) && rows.length > 0) {
2148
+ projectId = rows[0].id;
2149
+ orgId = rows[0].org_id;
2150
+ }
2151
+ }
2152
+ } catch {
2153
+ }
2154
+ const insertPayload = {
2155
+ user_id: userId,
2156
+ key_name: keyName,
2157
+ key_hash: keyHash,
2158
+ is_active: true
2159
+ };
2160
+ if (projectId) insertPayload.project_id = projectId;
2161
+ if (orgId) insertPayload.organization_id = orgId;
1910
2162
  const res = await fetch(`${SUPABASE_URL}/rest/v1/user_api_keys`, {
1911
2163
  method: "POST",
1912
2164
  headers: {
1913
2165
  ...supabaseHeaders(accessToken),
1914
2166
  "Prefer": "return=representation"
1915
2167
  },
1916
- body: JSON.stringify({
1917
- user_id: userId,
1918
- key_name: keyName,
1919
- key_hash: keyHash,
1920
- is_active: true
1921
- })
2168
+ body: JSON.stringify(insertPayload)
1922
2169
  });
1923
2170
  if (!res.ok) {
1924
2171
  const body = await res.json().catch(() => ({}));
@@ -2929,6 +3176,7 @@ init_loader();
2929
3176
  import { RSCCircuitOpenError as RSCCircuitOpenError2 } from "@cognisos/rsc-sdk";
2930
3177
 
2931
3178
  // src/rsc/message-compressor.ts
3179
+ import { createHash as createHash2 } from "crypto";
2932
3180
  import { RSCCircuitOpenError } from "@cognisos/rsc-sdk";
2933
3181
 
2934
3182
  // src/rsc/content-segmenter.ts
@@ -3030,6 +3278,10 @@ function isIndentedCodeLine(line) {
3030
3278
  }
3031
3279
 
3032
3280
  // src/rsc/message-compressor.ts
3281
+ init_tokenizer();
3282
+ function contentHash(text) {
3283
+ return createHash2("sha256").update(text).digest("hex").slice(0, 16);
3284
+ }
3033
3285
  var PASSTHROUGH_BLOCK_TYPES = /* @__PURE__ */ new Set(["thinking", "tool_use", "image"]);
3034
3286
  function sanitizeCompressedText(text) {
3035
3287
  return text.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
@@ -3097,43 +3349,90 @@ async function compressConversation(pipeline, session, plan, options = { compres
3097
3349
  }
3098
3350
  let anyCompressed = false;
3099
3351
  let totalTokensSaved = 0;
3100
- if (options.semaphore) await options.semaphore.acquire(options.semaphoreTimeoutMs);
3101
- try {
3102
- const batchSegments = compressible.map((entry) => ({
3103
- index: entry.planIdx,
3104
- text: entry.batchText
3105
- }));
3106
- const batchResults = await pipeline.normalizeBatch(batchSegments);
3107
- for (const entry of compressible) {
3108
- const result = batchResults.get(entry.planIdx);
3109
- if (!result || result.metrics.skipped) {
3110
- results[entry.planIdx] = entry.tm.message;
3111
- continue;
3112
- }
3113
- const compressed = sanitizeCompressedText(result.text);
3114
- const saved = Math.max(0, result.metrics.tokensSaved);
3352
+ const cache = options.stickyCache;
3353
+ const uncached = [];
3354
+ for (const entry of compressible) {
3355
+ if (!cache) {
3356
+ uncached.push(entry);
3357
+ continue;
3358
+ }
3359
+ const hash = contentHash(entry.batchText);
3360
+ const cached = cache.get(hash);
3361
+ if (cached !== void 0) {
3362
+ log?.(`[STICKY] Cache hit for message #${entry.tm.index}`);
3363
+ const originalTokens = countTokens(entry.batchText);
3364
+ const compressedTokens = countTokens(cached);
3365
+ const saved = Math.max(0, originalTokens - compressedTokens);
3115
3366
  if (saved > 0) {
3116
3367
  anyCompressed = true;
3117
3368
  totalTokensSaved += saved;
3118
3369
  }
3119
- session.recordCompression(result.metrics);
3120
- results[entry.planIdx] = reassembleMessage(entry.tm.message, compressed, entry.batchedIndices);
3121
- }
3122
- } catch (err) {
3123
- const errMsg = err instanceof Error ? err.message : String(err);
3124
- if (err instanceof RSCCircuitOpenError) {
3125
- session.recordFailure();
3126
- log?.(`[COMPRESS-ERROR] Circuit open \u2014 passing through (${errMsg})`);
3370
+ session.recordCompression({
3371
+ inputTokens: originalTokens,
3372
+ outputTokens: compressedTokens,
3373
+ tokensSaved: saved,
3374
+ skipped: false,
3375
+ fabricHits: 0,
3376
+ fabricMisses: 0,
3377
+ ratio: compressedTokens / Math.max(originalTokens, 1),
3378
+ deltaMdlBits: 0,
3379
+ processingTimeMs: 0
3380
+ });
3381
+ results[entry.planIdx] = reassembleMessage(entry.tm.message, cached, entry.batchedIndices);
3127
3382
  } else {
3128
- log?.(`[COMPRESS-ERROR] ${errMsg}`);
3383
+ uncached.push(entry);
3129
3384
  }
3130
- for (const entry of compressible) {
3131
- if (!results[entry.planIdx]) {
3132
- results[entry.planIdx] = entry.tm.message;
3385
+ }
3386
+ if (uncached.length > 0) {
3387
+ if (options.semaphore) await options.semaphore.acquire(options.semaphoreTimeoutMs);
3388
+ try {
3389
+ const batchSegments = uncached.map((entry) => ({
3390
+ index: entry.planIdx,
3391
+ text: entry.batchText
3392
+ }));
3393
+ const batchResults = await pipeline.normalizeBatch(batchSegments);
3394
+ for (const entry of uncached) {
3395
+ const result = batchResults.get(entry.planIdx);
3396
+ if (!result || result.metrics.skipped) {
3397
+ results[entry.planIdx] = entry.tm.message;
3398
+ continue;
3399
+ }
3400
+ const compressed = sanitizeCompressedText(result.text);
3401
+ const originalTokens = countTokens(entry.batchText);
3402
+ const compressedTokens = countTokens(compressed);
3403
+ const saved = Math.max(0, originalTokens - compressedTokens);
3404
+ const realMetrics = {
3405
+ ...result.metrics,
3406
+ inputTokens: originalTokens,
3407
+ outputTokens: compressedTokens,
3408
+ tokensSaved: saved
3409
+ };
3410
+ if (saved > 0) {
3411
+ anyCompressed = true;
3412
+ totalTokensSaved += saved;
3413
+ }
3414
+ session.recordCompression(realMetrics);
3415
+ results[entry.planIdx] = reassembleMessage(entry.tm.message, compressed, entry.batchedIndices);
3416
+ if (cache) {
3417
+ cache.set(contentHash(entry.batchText), compressed);
3418
+ }
3419
+ }
3420
+ } catch (err) {
3421
+ const errMsg = err instanceof Error ? err.message : String(err);
3422
+ if (err instanceof RSCCircuitOpenError) {
3423
+ session.recordFailure();
3424
+ log?.(`[COMPRESS-ERROR] Circuit open \u2014 passing through (${errMsg})`);
3425
+ } else {
3426
+ log?.(`[COMPRESS-ERROR] ${errMsg}`);
3133
3427
  }
3428
+ for (const entry of uncached) {
3429
+ if (!results[entry.planIdx]) {
3430
+ results[entry.planIdx] = entry.tm.message;
3431
+ }
3432
+ }
3433
+ } finally {
3434
+ if (options.semaphore) options.semaphore.release();
3134
3435
  }
3135
- } finally {
3136
- if (options.semaphore) options.semaphore.release();
3137
3436
  }
3138
3437
  return { messages: results, anyCompressed, totalTokensSaved };
3139
3438
  }
@@ -3156,6 +3455,7 @@ function extractBatchableText(msg, compressToolResults) {
3156
3455
  batchedIndices.add(i);
3157
3456
  }
3158
3457
  if (part.type === "tool_result" && compressToolResults) {
3458
+ if (part.is_error) continue;
3159
3459
  const extracted = extractToolResultText(part);
3160
3460
  if (extracted) {
3161
3461
  textSegments.push(extracted);
@@ -3190,7 +3490,8 @@ function reassembleMessage(msg, compressedText, batchedIndices) {
3190
3490
  isFirstEligible = false;
3191
3491
  } else {
3192
3492
  if (parts[i].type === "tool_result") {
3193
- newParts.push({ ...parts[i], content: "" });
3493
+ const cleared = parts[i].is_error ? parts[i] : { ...parts[i], content: "" };
3494
+ newParts.push(cleared);
3194
3495
  }
3195
3496
  }
3196
3497
  }
@@ -3272,7 +3573,7 @@ async function compressArrayContent(msg, pipeline, session, record, options = {
3272
3573
  return part;
3273
3574
  }
3274
3575
  }
3275
- if (part.type === "tool_result" && options.compressToolResults) {
3576
+ if (part.type === "tool_result" && options.compressToolResults && !part.is_error) {
3276
3577
  return compressToolResult(part, pipeline, session, record);
3277
3578
  }
3278
3579
  return part;
@@ -3331,8 +3632,11 @@ async function compressTextWithSegmentation(text, pipeline, session, record, sem
3331
3632
  if (semaphore) await semaphore.acquire(semaphoreTimeoutMs);
3332
3633
  try {
3333
3634
  const result = await pipeline.compressForLLM(text);
3334
- session.recordCompression(result.metrics);
3335
- const saved = Math.max(0, result.metrics.tokensSaved);
3635
+ const originalTok = countTokens(text);
3636
+ const compressedTok = countTokens(sanitizeCompressedText(result.text));
3637
+ const saved = Math.max(0, originalTok - compressedTok);
3638
+ const realMetrics = { ...result.metrics, inputTokens: originalTok, outputTokens: compressedTok, tokensSaved: saved };
3639
+ session.recordCompression(realMetrics);
3336
3640
  record(!result.metrics.skipped, saved);
3337
3641
  return sanitizeCompressedText(result.text);
3338
3642
  } finally {
@@ -3346,8 +3650,11 @@ async function compressTextWithSegmentation(text, pipeline, session, record, sem
3346
3650
  if (semaphore) await semaphore.acquire(semaphoreTimeoutMs);
3347
3651
  try {
3348
3652
  const result = await pipeline.compressForLLM(seg.text);
3349
- session.recordCompression(result.metrics);
3350
- const saved = Math.max(0, result.metrics.tokensSaved);
3653
+ const originalTok = countTokens(seg.text);
3654
+ const compressedTok = countTokens(sanitizeCompressedText(result.text));
3655
+ const saved = Math.max(0, originalTok - compressedTok);
3656
+ const realMetrics = { ...result.metrics, inputTokens: originalTok, outputTokens: compressedTok, tokensSaved: saved };
3657
+ session.recordCompression(realMetrics);
3351
3658
  record(!result.metrics.skipped, saved);
3352
3659
  return sanitizeCompressedText(result.text);
3353
3660
  } catch (err) {
@@ -3372,7 +3679,7 @@ function estimateBlockTokens(block, compressToolResults) {
3372
3679
  if (block.type === "text" && typeof block.text === "string") {
3373
3680
  return estimateTokens(block.text);
3374
3681
  }
3375
- if (block.type === "tool_result" && compressToolResults) {
3682
+ if (block.type === "tool_result" && compressToolResults && !block.is_error) {
3376
3683
  if (typeof block.content === "string") {
3377
3684
  return estimateTokens(block.content);
3378
3685
  }
@@ -3586,7 +3893,7 @@ function extractBearerToken(req) {
3586
3893
  if (!auth || !auth.startsWith("Bearer ")) return null;
3587
3894
  return auth.slice(7);
3588
3895
  }
3589
- async function handleChatCompletions(req, res, body, pipeline, config, logger, semaphore, latencyMonitor, sessionKey) {
3896
+ async function handleChatCompletions(req, res, body, pipeline, config, logger, semaphore, latencyMonitor, sessionKey, stickyCache) {
3590
3897
  const request = body;
3591
3898
  if (!request.messages || !Array.isArray(request.messages)) {
3592
3899
  sendJSON(res, 400, {
@@ -3645,7 +3952,8 @@ async function handleChatCompletions(req, res, body, pipeline, config, logger, s
3645
3952
  compressionThreshold: config.compressionThreshold,
3646
3953
  logFn: blockLogFn,
3647
3954
  semaphore,
3648
- semaphoreTimeoutMs: config.concurrencyTimeoutMs
3955
+ semaphoreTimeoutMs: config.concurrencyTimeoutMs,
3956
+ stickyCache
3649
3957
  }
3650
3958
  );
3651
3959
  const totalBlocks = batchedCount + skippedCount + hotCount;
@@ -3696,7 +4004,7 @@ async function handleChatCompletions(req, res, body, pipeline, config, logger, s
3696
4004
  return;
3697
4005
  }
3698
4006
  if (request.stream && upstreamResponse.body) {
3699
- const learningBuffer = anyCompressed ? createStreamLearningBuffer(pipeline.pipeline) : null;
4007
+ const learningBuffer = createStreamLearningBuffer(pipeline.pipeline);
3700
4008
  logger.log(formatResponseLog(request.model, totalTokensSaved, true));
3701
4009
  await pipeSSEResponse(
3702
4010
  upstreamResponse,
@@ -3727,7 +4035,7 @@ async function handleChatCompletions(req, res, body, pipeline, config, logger, s
3727
4035
  setCORSHeaders(res);
3728
4036
  res.writeHead(200, { "Content-Type": "application/json" });
3729
4037
  res.end(finalBody);
3730
- if (anyCompressed) {
4038
+ {
3731
4039
  try {
3732
4040
  const parsed = JSON.parse(responseBody);
3733
4041
  const content = parsed?.choices?.[0]?.message?.content;
@@ -3752,18 +4060,33 @@ async function handleChatCompletions(req, res, body, pipeline, config, logger, s
3752
4060
  import { RSCCircuitOpenError as RSCCircuitOpenError3 } from "@cognisos/rsc-sdk";
3753
4061
 
3754
4062
  // src/proxy/anthropic-streaming.ts
3755
- function adjustMessageStartLine(dataLine, tokensSaved) {
4063
+ function parseMessageStart(dataLine, tokensSaved) {
4064
+ try {
4065
+ const json = JSON.parse(dataLine.slice(6));
4066
+ const usage2 = json?.message?.usage;
4067
+ if (usage2?.input_tokens != null) {
4068
+ const actual = usage2.input_tokens + (usage2.cache_creation_input_tokens ?? 0) + (usage2.cache_read_input_tokens ?? 0);
4069
+ if (tokensSaved > 0) {
4070
+ json.message.usage.input_tokens += tokensSaved;
4071
+ return [`data: ${JSON.stringify(json)}`, actual];
4072
+ }
4073
+ return [null, actual];
4074
+ }
4075
+ } catch {
4076
+ }
4077
+ return [null, null];
4078
+ }
4079
+ function parseMessageDelta(dataLine) {
3756
4080
  try {
3757
4081
  const json = JSON.parse(dataLine.slice(6));
3758
- if (json?.message?.usage?.input_tokens != null) {
3759
- json.message.usage.input_tokens += tokensSaved;
3760
- return `data: ${JSON.stringify(json)}`;
4082
+ if (json?.usage?.output_tokens != null) {
4083
+ return json.usage.output_tokens;
3761
4084
  }
3762
4085
  } catch {
3763
4086
  }
3764
4087
  return null;
3765
4088
  }
3766
- async function pipeAnthropicSSEResponse(upstreamResponse, clientRes, onContentDelta, onComplete, totalTokensSaved = 0) {
4089
+ async function pipeAnthropicSSEResponse(upstreamResponse, clientRes, onContentDelta, onComplete, totalTokensSaved = 0, onUsage) {
3767
4090
  clientRes.writeHead(200, {
3768
4091
  "Content-Type": "text/event-stream",
3769
4092
  "Cache-Control": "no-cache",
@@ -3774,14 +4097,19 @@ async function pipeAnthropicSSEResponse(upstreamResponse, clientRes, onContentDe
3774
4097
  const decoder = new TextDecoder();
3775
4098
  let lineBuf = "";
3776
4099
  let currentEvent = "";
3777
- let usageAdjusted = false;
4100
+ let capturedInputTokens = null;
4101
+ let capturedOutputTokens = null;
4102
+ let inputAdjusted = false;
3778
4103
  const needsAdjustment = totalTokensSaved > 0;
4104
+ const needsCapture = !!onUsage;
3779
4105
  try {
3780
4106
  while (true) {
3781
4107
  const { done, value } = await reader.read();
3782
4108
  if (done) break;
3783
4109
  const chunk = decoder.decode(value, { stream: true });
3784
- if (!needsAdjustment || usageAdjusted) {
4110
+ const inputDone = !needsAdjustment || inputAdjusted;
4111
+ const captureDone = !needsCapture || capturedInputTokens != null && capturedOutputTokens != null;
4112
+ if (inputDone && captureDone) {
3785
4113
  clientRes.write(chunk);
3786
4114
  lineBuf += chunk;
3787
4115
  const lines2 = lineBuf.split("\n");
@@ -3804,21 +4132,32 @@ async function pipeAnthropicSSEResponse(upstreamResponse, clientRes, onContentDe
3804
4132
  lineBuf += chunk;
3805
4133
  const lines = lineBuf.split("\n");
3806
4134
  lineBuf = lines.pop() || "";
3807
- let adjusted = false;
4135
+ let lineModified = false;
3808
4136
  const outputLines = [];
3809
4137
  for (const line of lines) {
3810
4138
  if (line.startsWith("event: ")) {
3811
4139
  currentEvent = line.slice(7).trim();
3812
4140
  outputLines.push(line);
3813
- } else if (line.startsWith("data: ") && currentEvent === "message_start" && !usageAdjusted) {
3814
- const adjustedLine = adjustMessageStartLine(line, totalTokensSaved);
3815
- if (adjustedLine) {
3816
- outputLines.push(adjustedLine);
3817
- usageAdjusted = true;
3818
- adjusted = true;
4141
+ } else if (line.startsWith("data: ") && currentEvent === "message_start" && capturedInputTokens == null) {
4142
+ const [adjusted, actual] = parseMessageStart(line, totalTokensSaved);
4143
+ if (actual != null) {
4144
+ capturedInputTokens = actual;
4145
+ inputAdjusted = true;
4146
+ if (adjusted && needsAdjustment) {
4147
+ outputLines.push(adjusted);
4148
+ lineModified = true;
4149
+ } else {
4150
+ outputLines.push(line);
4151
+ }
3819
4152
  } else {
3820
4153
  outputLines.push(line);
3821
4154
  }
4155
+ } else if (line.startsWith("data: ") && currentEvent === "message_delta" && capturedOutputTokens == null) {
4156
+ const outputTok = parseMessageDelta(line);
4157
+ if (outputTok != null) {
4158
+ capturedOutputTokens = outputTok;
4159
+ }
4160
+ outputLines.push(line);
3822
4161
  } else {
3823
4162
  outputLines.push(line);
3824
4163
  if (line.startsWith("data: ") && currentEvent === "content_block_delta") {
@@ -3832,14 +4171,20 @@ async function pipeAnthropicSSEResponse(upstreamResponse, clientRes, onContentDe
3832
4171
  }
3833
4172
  }
3834
4173
  }
3835
- if (adjusted) {
3836
- const reconstructed = outputLines.join("\n") + "\n" + (lineBuf ? "" : "");
4174
+ if (lineModified) {
4175
+ const reconstructed = outputLines.join("\n") + "\n";
3837
4176
  clientRes.write(reconstructed);
3838
4177
  } else {
3839
4178
  clientRes.write(chunk);
3840
4179
  }
3841
4180
  }
3842
4181
  } finally {
4182
+ if (onUsage && (capturedInputTokens != null || capturedOutputTokens != null)) {
4183
+ onUsage({
4184
+ inputTokens: capturedInputTokens ?? 0,
4185
+ outputTokens: capturedOutputTokens ?? 0
4186
+ });
4187
+ }
3843
4188
  clientRes.end();
3844
4189
  onComplete();
3845
4190
  }
@@ -3880,7 +4225,7 @@ function convertCompressedToAnthropic(messages) {
3880
4225
  content: msg.content
3881
4226
  }));
3882
4227
  }
3883
- async function handleAnthropicMessages(req, res, body, pipeline, config, logger, semaphore, latencyMonitor, sessionKey) {
4228
+ async function handleAnthropicMessages(req, res, body, pipeline, config, logger, semaphore, latencyMonitor, sessionKey, onUsage, stickyCache) {
3884
4229
  const request = body;
3885
4230
  if (!request.messages || !Array.isArray(request.messages)) {
3886
4231
  sendAnthropicError(res, 400, "invalid_request_error", "messages is required and must be an array");
@@ -3895,6 +4240,8 @@ async function handleAnthropicMessages(req, res, body, pipeline, config, logger,
3895
4240
  sendAnthropicError(res, 401, "authentication_error", "Authentication required (x-api-key or Authorization header)");
3896
4241
  return;
3897
4242
  }
4243
+ const { countTokens: countTok } = await Promise.resolve().then(() => (init_tokenizer(), tokenizer_exports));
4244
+ const originalInputTokens = countTok(JSON.stringify(request));
3898
4245
  let messages = request.messages;
3899
4246
  let anyCompressed = false;
3900
4247
  let totalTokensSaved = 0;
@@ -3940,7 +4287,8 @@ async function handleAnthropicMessages(req, res, body, pipeline, config, logger,
3940
4287
  compressionThreshold: config.compressionThreshold,
3941
4288
  logFn: blockLogFn,
3942
4289
  semaphore,
3943
- semaphoreTimeoutMs: config.concurrencyTimeoutMs
4290
+ semaphoreTimeoutMs: config.concurrencyTimeoutMs,
4291
+ stickyCache
3944
4292
  }
3945
4293
  );
3946
4294
  const totalBlocks = batchedCount + skippedCount + hotCount;
@@ -3968,6 +4316,8 @@ async function handleAnthropicMessages(req, res, body, pipeline, config, logger,
3968
4316
  }
3969
4317
  const upstreamUrl = `${config.anthropicUpstreamUrl}/v1/messages`;
3970
4318
  const upstreamBody = { ...request, messages };
4319
+ const compressedInputTokens = countTok(JSON.stringify(upstreamBody));
4320
+ const verifiedTokensSaved = Math.max(0, originalInputTokens - compressedInputTokens);
3971
4321
  const upstreamHeaders = {
3972
4322
  ...authHeaders,
3973
4323
  "anthropic-version": req.headers["anthropic-version"] || "2023-06-01",
@@ -3996,35 +4346,44 @@ async function handleAnthropicMessages(req, res, body, pipeline, config, logger,
3996
4346
  return;
3997
4347
  }
3998
4348
  if (request.stream && upstreamResponse.body) {
3999
- const learningBuffer = anyCompressed ? createStreamLearningBuffer(pipeline.pipeline) : null;
4349
+ const learningBuffer = createStreamLearningBuffer(pipeline.pipeline);
4000
4350
  logger.log(formatResponseLog(request.model, totalTokensSaved, true));
4001
4351
  await pipeAnthropicSSEResponse(
4002
4352
  upstreamResponse,
4003
4353
  res,
4004
4354
  (text) => learningBuffer?.append(text),
4005
4355
  () => learningBuffer?.flush(),
4006
- totalTokensSaved
4356
+ totalTokensSaved,
4357
+ onUsage ? (usage2) => {
4358
+ onUsage(usage2, verifiedTokensSaved, originalInputTokens);
4359
+ logger.log(`[TOKENS] input: ${usage2.inputTokens} | output: ${usage2.outputTokens} | verified_saved: ${verifiedTokensSaved} | orig_tok: ${originalInputTokens} | comp_tok: ${compressedInputTokens}`);
4360
+ } : void 0
4007
4361
  );
4008
4362
  return;
4009
4363
  }
4010
4364
  const responseBody = await upstreamResponse.text();
4011
4365
  logger.log(formatResponseLog(request.model, totalTokensSaved));
4012
4366
  let finalBody = responseBody;
4013
- if (totalTokensSaved > 0) {
4014
- try {
4015
- const parsed = JSON.parse(responseBody);
4016
- if (parsed?.usage?.input_tokens != null) {
4367
+ try {
4368
+ const parsed = JSON.parse(responseBody);
4369
+ if (parsed?.usage) {
4370
+ const actualInput = (parsed.usage.input_tokens ?? 0) + (parsed.usage.cache_creation_input_tokens ?? 0) + (parsed.usage.cache_read_input_tokens ?? 0);
4371
+ const actualOutput = parsed.usage.output_tokens ?? 0;
4372
+ if (onUsage) {
4373
+ onUsage({ inputTokens: actualInput, outputTokens: actualOutput }, verifiedTokensSaved, originalInputTokens);
4374
+ logger.log(`[TOKENS] input: ${actualInput} | output: ${actualOutput} | verified_saved: ${verifiedTokensSaved} | orig_tok: ${originalInputTokens} | comp_tok: ${compressedInputTokens}`);
4375
+ }
4376
+ if (totalTokensSaved > 0 && parsed.usage.input_tokens != null) {
4017
4377
  parsed.usage.input_tokens += totalTokensSaved;
4018
4378
  finalBody = JSON.stringify(parsed);
4019
- logger.log(`[TOKENS] Adjusted input_tokens by +${totalTokensSaved}`);
4020
4379
  }
4021
- } catch {
4022
4380
  }
4381
+ } catch {
4023
4382
  }
4024
4383
  setCORSHeaders2(res);
4025
4384
  res.writeHead(200, { "Content-Type": "application/json" });
4026
4385
  res.end(finalBody);
4027
- if (anyCompressed) {
4386
+ {
4028
4387
  try {
4029
4388
  const parsed = JSON.parse(responseBody);
4030
4389
  const textBlocks = parsed?.content?.filter(
@@ -4317,7 +4676,7 @@ async function handleResponses(req, res, body, pipeline, config, logger, semapho
4317
4676
  return;
4318
4677
  }
4319
4678
  if (request.stream && upstreamResponse.body) {
4320
- const learningBuffer = anyCompressed ? createStreamLearningBuffer(pipeline.pipeline) : null;
4679
+ const learningBuffer = createStreamLearningBuffer(pipeline.pipeline);
4321
4680
  logger.log(formatResponseLog(request.model, totalTokensSaved, true));
4322
4681
  await pipeResponsesSSE(
4323
4682
  upstreamResponse,
@@ -4348,7 +4707,7 @@ async function handleResponses(req, res, body, pipeline, config, logger, semapho
4348
4707
  setCORSHeaders3(res);
4349
4708
  res.writeHead(200, { "Content-Type": "application/json" });
4350
4709
  res.end(finalBody);
4351
- if (anyCompressed) {
4710
+ {
4352
4711
  try {
4353
4712
  const parsed = JSON.parse(responseBody);
4354
4713
  if (parsed?.output) {
@@ -4604,12 +4963,14 @@ function createRequestHandler(deps) {
4604
4963
  calls_failed: s.failedCalls,
4605
4964
  p95_latency_ms: latencyMonitor.getSessionP95(s.key),
4606
4965
  last_active_ago_ms: Date.now() - s.lastAccessedAt
4607
- }))
4966
+ })),
4967
+ ...deps.getVerifiedTokens ? deps.getVerifiedTokens() : {},
4968
+ ...deps.getCursorMetrics ? { cursor: deps.getCursorMetrics() } : {}
4608
4969
  });
4609
4970
  return;
4610
4971
  }
4611
4972
  const sessionKey = identifySession(req, url);
4612
- const pipeline = sessions.getOrCreate(sessionKey);
4973
+ const { pipeline, stickyCache } = sessions.getOrCreate(sessionKey);
4613
4974
  if (method === "POST" && (url === "/v1/chat/completions" || url === "/chat/completions")) {
4614
4975
  const body = await readBody(req);
4615
4976
  let parsed;
@@ -4621,7 +4982,7 @@ function createRequestHandler(deps) {
4621
4982
  });
4622
4983
  return;
4623
4984
  }
4624
- await handleChatCompletions(req, res, parsed, pipeline, config, logger, semaphore, latencyMonitor, sessionKey.raw);
4985
+ await handleChatCompletions(req, res, parsed, pipeline, config, logger, semaphore, latencyMonitor, sessionKey.raw, stickyCache);
4625
4986
  return;
4626
4987
  }
4627
4988
  if (method === "POST" && (url === "/v1/responses" || url === "/responses")) {
@@ -4650,7 +5011,7 @@ function createRequestHandler(deps) {
4650
5011
  });
4651
5012
  return;
4652
5013
  }
4653
- await handleAnthropicMessages(req, res, parsed, pipeline, config, logger, semaphore, latencyMonitor, sessionKey.raw);
5014
+ await handleAnthropicMessages(req, res, parsed, pipeline, config, logger, semaphore, latencyMonitor, sessionKey.raw, deps.onUsage, stickyCache);
4654
5015
  return;
4655
5016
  }
4656
5017
  await passthroughToUpstream(req, res, fullUrl, config, logger);
@@ -4814,7 +5175,7 @@ var SessionManager = class {
4814
5175
  if (existing) {
4815
5176
  existing.lastAccessedAt = Date.now();
4816
5177
  existing.requestCount++;
4817
- return existing.pipeline;
5178
+ return { pipeline: existing.pipeline, stickyCache: existing.stickyCache };
4818
5179
  }
4819
5180
  if (this.sessions.size >= this.config.maxSessions) {
4820
5181
  this.evictLRU();
@@ -4823,14 +5184,16 @@ var SessionManager = class {
4823
5184
  ...this.config.pipelineConfig,
4824
5185
  sessionId: key.raw
4825
5186
  });
5187
+ const stickyCache = /* @__PURE__ */ new Map();
4826
5188
  this.sessions.set(key.raw, {
4827
5189
  pipeline,
5190
+ stickyCache,
4828
5191
  lastAccessedAt: Date.now(),
4829
5192
  requestCount: 1,
4830
5193
  connector: key.connector
4831
5194
  });
4832
5195
  this.config.onSessionCreated?.(key.raw, pipeline);
4833
- return pipeline;
5196
+ return { pipeline, stickyCache };
4834
5197
  }
4835
5198
  getAllSummaries() {
4836
5199
  const entries = [];
@@ -5409,6 +5772,13 @@ async function startCommand(flags) {
5409
5772
  logger.log(`[LIMINAL] [${key}] Error: ${event.error.message}`);
5410
5773
  statsAggregator.recordFailure(connector);
5411
5774
  });
5775
+ pipeline.events.on("learning", (event) => {
5776
+ if (event.error) {
5777
+ logger.log(`[LIMINAL] [${key}] Learning error: ${event.error}`);
5778
+ } else if (event.tokensIngested > 0) {
5779
+ logger.log(`[LIMINAL] [${key}] Learning: ${event.tokensIngested} tokens ingested (deferred:${event.deferred})`);
5780
+ }
5781
+ });
5412
5782
  pipeline.events.on("degradation", (event) => {
5413
5783
  logger.log(`[LIMINAL] [${key}] Circuit ${event.circuitState}: ${event.reason}`);
5414
5784
  });
@@ -5435,7 +5805,50 @@ async function startCommand(flags) {
5435
5805
  logger.log(`[LATENCY] ${alert.type.toUpperCase()}: ${alert.message} (${alert.activeSessions} sessions) \u2014 ${alert.suggestion}`);
5436
5806
  });
5437
5807
  const mitmStats = new MitmStats();
5438
- const deps = { sessions, semaphore, latencyMonitor, mitmStats, config: resolvedConfig, logger };
5808
+ const deps = {
5809
+ sessions,
5810
+ semaphore,
5811
+ latencyMonitor,
5812
+ mitmStats,
5813
+ config: resolvedConfig,
5814
+ logger,
5815
+ onUsage: (usage2, verifiedSaved, originalInputTokens) => {
5816
+ statsAggregator.recordApiUsage(usage2.inputTokens, usage2.outputTokens, verifiedSaved, originalInputTokens);
5817
+ if (resolvedConfig.rscApiKey && resolvedConfig.rscBaseUrl) {
5818
+ const body = JSON.stringify({
5819
+ model: "cli",
5820
+ actual_input_tokens: usage2.inputTokens,
5821
+ actual_output_tokens: usage2.outputTokens,
5822
+ original_input_bytes: originalInputTokens * 4
5823
+ });
5824
+ fetch(`${resolvedConfig.rscBaseUrl}/api/v1/usage/verified`, {
5825
+ method: "POST",
5826
+ headers: {
5827
+ "Content-Type": "application/json",
5828
+ "Authorization": `Bearer ${resolvedConfig.rscApiKey}`
5829
+ },
5830
+ body,
5831
+ signal: AbortSignal.timeout(5e3)
5832
+ }).catch(() => {
5833
+ });
5834
+ }
5835
+ },
5836
+ getVerifiedTokens: () => {
5837
+ const snap = statsAggregator.snapshot();
5838
+ return {
5839
+ actualInputTokens: snap.actualInputTokens,
5840
+ actualOutputTokens: snap.actualOutputTokens,
5841
+ originalInputEstimate: snap.originalInputEstimate,
5842
+ verifiedInputSaved: snap.verifiedInputSaved,
5843
+ verifiedSavingsRate: snap.verifiedSavingsRate,
5844
+ verifiedRequestCount: snap.verifiedRequestCount
5845
+ };
5846
+ },
5847
+ getCursorMetrics: () => {
5848
+ const { parseCursorHookStats: parseCursorHookStats3 } = (init_stats(), __toCommonJS(stats_exports));
5849
+ return parseCursorHookStats3();
5850
+ }
5851
+ };
5439
5852
  const handler = createRequestHandler(deps);
5440
5853
  let mitmHandler;
5441
5854
  const connectHandler = createConnectHandler({
@@ -5640,8 +6053,8 @@ async function stopCommand() {
5640
6053
  init_lifecycle();
5641
6054
  init_loader();
5642
6055
  init_format();
5643
- import { existsSync as existsSync10, readFileSync as readFileSync8 } from "fs";
5644
- import { join as join7 } from "path";
6056
+ import { existsSync as existsSync11, readFileSync as readFileSync9 } from "fs";
6057
+ import { join as join8 } from "path";
5645
6058
  async function statusCommand() {
5646
6059
  if (!isConfigured()) {
5647
6060
  console.log(error(
@@ -5709,27 +6122,27 @@ async function statusCommand() {
5709
6122
  }
5710
6123
  }
5711
6124
  function printCursorHookStats() {
5712
- const logPath = join7(process.cwd(), ".fabric", "compress.log");
5713
- const hooksPath = join7(process.cwd(), ".cursor", "hooks.json");
6125
+ const logPath = join8(process.cwd(), ".fabric", "compress.log");
6126
+ const hooksPath = join8(process.cwd(), ".cursor", "hooks.json");
5714
6127
  let hooksActive = false;
5715
6128
  try {
5716
- if (existsSync10(hooksPath)) {
5717
- const data = JSON.parse(readFileSync8(hooksPath, "utf-8"));
6129
+ if (existsSync11(hooksPath)) {
6130
+ const data = JSON.parse(readFileSync9(hooksPath, "utf-8"));
5718
6131
  const entries = data?.hooks?.preToolUse ?? [];
5719
6132
  hooksActive = entries.some((e) => e.command?.includes("fabric-compress"));
5720
6133
  }
5721
6134
  } catch {
5722
6135
  }
5723
- if (!hooksActive && !existsSync10(logPath)) return;
6136
+ if (!hooksActive && !existsSync11(logPath)) return;
5724
6137
  console.log(sectionHeader("Cursor Hooks"));
5725
6138
  console.log();
5726
6139
  console.log(label("Status", hooksActive ? `${c.green}active${c.reset}` : `${c.dim}not installed${c.reset}`));
5727
- if (!existsSync10(logPath)) {
6140
+ if (!existsSync11(logPath)) {
5728
6141
  console.log(label("Stats", `${c.dim}no compression data yet${c.reset}`));
5729
6142
  console.log();
5730
6143
  return;
5731
6144
  }
5732
- const lines = readFileSync8(logPath, "utf-8").trim().split("\n");
6145
+ const lines = readFileSync9(logPath, "utf-8").trim().split("\n");
5733
6146
  let compressed = 0;
5734
6147
  let errors = 0;
5735
6148
  let totalInputSize = 0;
@@ -5763,7 +6176,7 @@ function printCursorHookStats() {
5763
6176
  const avgApiMs = compressed > 0 ? Math.round(totalApiMs / compressed) : 0;
5764
6177
  let cacheCount = 0;
5765
6178
  try {
5766
- cacheCount = countFiles(join7(process.cwd(), ".fabric", "cache"));
6179
+ cacheCount = countFiles(join8(process.cwd(), ".fabric", "cache"));
5767
6180
  } catch {
5768
6181
  }
5769
6182
  console.log(label("Files", `${files.size} unique ${c.dim}(${compressed} compressions${errors > 0 ? `, ${c.red}${errors} errors${c.reset}${c.dim}` : ""})${c.reset}`));
@@ -5776,12 +6189,12 @@ function printCursorHookStats() {
5776
6189
  console.log();
5777
6190
  }
5778
6191
  function countFiles(dir) {
5779
- if (!existsSync10(dir)) return 0;
5780
- const { readdirSync: readdirSync2, statSync: statSync4 } = __require("fs");
6192
+ if (!existsSync11(dir)) return 0;
6193
+ const { readdirSync: readdirSync3, statSync: statSync4 } = __require("fs");
5781
6194
  let count = 0;
5782
- for (const entry of readdirSync2(dir, { withFileTypes: true })) {
6195
+ for (const entry of readdirSync3(dir, { withFileTypes: true })) {
5783
6196
  if (entry.isDirectory()) {
5784
- count += countFiles(join7(dir, entry.name));
6197
+ count += countFiles(join8(dir, entry.name));
5785
6198
  } else {
5786
6199
  count++;
5787
6200
  }
@@ -5864,17 +6277,17 @@ async function configCommand(flags) {
5864
6277
 
5865
6278
  // src/commands/logs.ts
5866
6279
  init_paths();
5867
- import { readFileSync as readFileSync9, existsSync as existsSync11, statSync as statSync2, createReadStream } from "fs";
6280
+ import { readFileSync as readFileSync10, existsSync as existsSync12, statSync as statSync2, createReadStream } from "fs";
5868
6281
  import { watchFile, unwatchFile } from "fs";
5869
6282
  async function logsCommand(flags) {
5870
6283
  const follow = flags.has("follow") || flags.has("f");
5871
6284
  const linesFlag = flags.get("lines") ?? flags.get("n");
5872
6285
  const lines = typeof linesFlag === "string" ? parseInt(linesFlag, 10) : 50;
5873
- if (!existsSync11(LOG_FILE)) {
6286
+ if (!existsSync12(LOG_FILE)) {
5874
6287
  console.log('No log file found. Start the daemon with "liminal start" to generate logs.');
5875
6288
  return;
5876
6289
  }
5877
- const content = readFileSync9(LOG_FILE, "utf-8");
6290
+ const content = readFileSync10(LOG_FILE, "utf-8");
5878
6291
  const allLines = content.split("\n");
5879
6292
  const tail = allLines.slice(-lines - 1);
5880
6293
  process.stdout.write(tail.join("\n"));
@@ -5900,7 +6313,7 @@ async function logsCommand(flags) {
5900
6313
  }
5901
6314
 
5902
6315
  // src/commands/uninstall.ts
5903
- import { existsSync as existsSync12, rmSync, readFileSync as readFileSync10 } from "fs";
6316
+ import { existsSync as existsSync13, rmSync, readFileSync as readFileSync11 } from "fs";
5904
6317
  init_paths();
5905
6318
  init_lifecycle();
5906
6319
  init_prompts();
@@ -5910,9 +6323,9 @@ var GREEN = "\x1B[32m";
5910
6323
  var YELLOW = "\x1B[33m";
5911
6324
  var RESET = "\x1B[0m";
5912
6325
  function loadConfiguredTools() {
5913
- if (!existsSync12(CONFIG_FILE)) return [];
6326
+ if (!existsSync13(CONFIG_FILE)) return [];
5914
6327
  try {
5915
- const raw = readFileSync10(CONFIG_FILE, "utf-8");
6328
+ const raw = readFileSync11(CONFIG_FILE, "utf-8");
5916
6329
  const config = JSON.parse(raw);
5917
6330
  if (Array.isArray(config.tools)) return config.tools;
5918
6331
  } catch {
@@ -6000,7 +6413,7 @@ async function uninstallCommand() {
6000
6413
  }
6001
6414
  }
6002
6415
  }
6003
- if (existsSync12(LIMINAL_DIR)) {
6416
+ if (existsSync13(LIMINAL_DIR)) {
6004
6417
  console.log();
6005
6418
  const removeData = await selectPrompt({
6006
6419
  message: "Remove ~/.liminal/ directory? (config, logs, PID file)",
@@ -6120,9 +6533,9 @@ async function untrustCACommand() {
6120
6533
  // src/commands/setup-cursor.ts
6121
6534
  init_loader();
6122
6535
  init_version();
6123
- import { existsSync as existsSync13, readFileSync as readFileSync11, writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, unlinkSync as unlinkSync4, readdirSync, rmdirSync, appendFileSync as appendFileSync3 } from "fs";
6536
+ import { existsSync as existsSync14, readFileSync as readFileSync12, writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, unlinkSync as unlinkSync4, readdirSync as readdirSync2, rmdirSync, appendFileSync as appendFileSync3 } from "fs";
6124
6537
  import { homedir as homedir5 } from "os";
6125
- import { join as join8 } from "path";
6538
+ import { join as join9 } from "path";
6126
6539
 
6127
6540
  // src/cursor/hook-template.ts
6128
6541
  function getHookScript() {
@@ -6375,10 +6788,11 @@ async function main() {
6375
6788
  if (!fs.existsSync(cacheDir)) fs.mkdirSync(cacheDir, { recursive: true });
6376
6789
  fs.writeFileSync(cachePath, result.text, "utf-8");
6377
6790
 
6378
- // Use server-side token counts (actual tokenizer, not byte estimate)
6379
- // Fall back to ~4 chars/token estimate if server doesn't return counts
6380
- const inputTokens = result.inputTokens || Math.ceil(result.inputSize / 4);
6381
- const outputTokens = result.outputTokens || Math.ceil(result.outputSize / 4);
6791
+ // Use server-side token counts from tiktoken-rs (cl100k_base BPE tokenizer)
6792
+ // Fall back to ~3 chars/token estimate if server doesn't return counts
6793
+ const inputTokens = result.inputTokens || Math.ceil(result.inputSize / 3);
6794
+ const outputTokens = result.outputTokens || Math.ceil(result.outputSize / 3);
6795
+ const tokensSaved = Math.max(0, inputTokens - outputTokens);
6382
6796
  logEntry({
6383
6797
  type: "compressed",
6384
6798
  file: relativePath,
@@ -6386,8 +6800,8 @@ async function main() {
6386
6800
  outputSize: result.outputSize,
6387
6801
  inputTokens: inputTokens,
6388
6802
  outputTokens: outputTokens,
6389
- tokensSaved: inputTokens - outputTokens,
6390
- savedPct: +((1 - result.outputSize / result.inputSize) * 100).toFixed(1),
6803
+ tokensSaved: tokensSaved,
6804
+ savedPct: inputTokens > 0 ? +((tokensSaved / inputTokens) * 100).toFixed(1) : 0,
6391
6805
  apiMs: elapsed,
6392
6806
  });
6393
6807
 
@@ -6410,26 +6824,26 @@ var HOOK_SCRIPT_NAME = "fabric-compress.js";
6410
6824
  var HOOK_COMMAND = `node .cursor/hooks/${HOOK_SCRIPT_NAME}`;
6411
6825
  function isCursorInstalled2() {
6412
6826
  if (process.platform === "darwin") {
6413
- return existsSync13("/Applications/Cursor.app");
6827
+ return existsSync14("/Applications/Cursor.app");
6414
6828
  }
6415
6829
  if (process.platform === "win32") {
6416
- const localAppData = process.env.LOCALAPPDATA || join8(homedir5(), "AppData", "Local");
6417
- return existsSync13(join8(localAppData, "Programs", "Cursor", "Cursor.exe"));
6830
+ const localAppData = process.env.LOCALAPPDATA || join9(homedir5(), "AppData", "Local");
6831
+ return existsSync14(join9(localAppData, "Programs", "Cursor", "Cursor.exe"));
6418
6832
  }
6419
- return existsSync13("/usr/bin/cursor");
6833
+ return existsSync14("/usr/bin/cursor");
6420
6834
  }
6421
6835
  function readHooksJson(workspaceRoot) {
6422
- const hooksPath = join8(workspaceRoot, ".cursor", "hooks.json");
6836
+ const hooksPath = join9(workspaceRoot, ".cursor", "hooks.json");
6423
6837
  try {
6424
- return JSON.parse(readFileSync11(hooksPath, "utf-8"));
6838
+ return JSON.parse(readFileSync12(hooksPath, "utf-8"));
6425
6839
  } catch {
6426
6840
  return {};
6427
6841
  }
6428
6842
  }
6429
6843
  function writeHooksJson(workspaceRoot, data) {
6430
- const dir = join8(workspaceRoot, ".cursor");
6431
- if (!existsSync13(dir)) mkdirSync5(dir, { recursive: true });
6432
- writeFileSync6(join8(dir, "hooks.json"), JSON.stringify(data, null, 2) + "\n");
6844
+ const dir = join9(workspaceRoot, ".cursor");
6845
+ if (!existsSync14(dir)) mkdirSync5(dir, { recursive: true });
6846
+ writeFileSync6(join9(dir, "hooks.json"), JSON.stringify(data, null, 2) + "\n");
6433
6847
  }
6434
6848
  function hasOurHook(hooks) {
6435
6849
  const entries = hooks.hooks?.preToolUse ?? [];
@@ -6462,10 +6876,10 @@ function removeOurHook(hooks) {
6462
6876
  return hooks;
6463
6877
  }
6464
6878
  function ensureGitignore(workspaceRoot) {
6465
- const gitignorePath = join8(workspaceRoot, ".gitignore");
6879
+ const gitignorePath = join9(workspaceRoot, ".gitignore");
6466
6880
  const entry = ".fabric/";
6467
- if (existsSync13(gitignorePath)) {
6468
- const content = readFileSync11(gitignorePath, "utf-8");
6881
+ if (existsSync14(gitignorePath)) {
6882
+ const content = readFileSync12(gitignorePath, "utf-8");
6469
6883
  if (content.includes(entry)) return;
6470
6884
  appendFileSync3(gitignorePath, `
6471
6885
  # Liminal compression cache
@@ -6479,7 +6893,7 @@ ${entry}
6479
6893
  }
6480
6894
  function rmdirIfEmpty(dir) {
6481
6895
  try {
6482
- if (existsSync13(dir) && readdirSync(dir).length === 0) {
6896
+ if (existsSync14(dir) && readdirSync2(dir).length === 0) {
6483
6897
  rmdirSync(dir);
6484
6898
  }
6485
6899
  } catch {
@@ -6502,20 +6916,20 @@ async function setupCursorCommand(flags) {
6502
6916
  writeHooksJson(workspaceRoot, updated2);
6503
6917
  console.log(success("Removed hook entry from .cursor/hooks.json"));
6504
6918
  } else {
6505
- const hooksJsonPath = join8(workspaceRoot, ".cursor", "hooks.json");
6919
+ const hooksJsonPath = join9(workspaceRoot, ".cursor", "hooks.json");
6506
6920
  try {
6507
6921
  unlinkSync4(hooksJsonPath);
6508
6922
  } catch {
6509
6923
  }
6510
6924
  console.log(success("Deleted .cursor/hooks.json", "no remaining hooks"));
6511
6925
  }
6512
- const scriptPath2 = join8(workspaceRoot, ".cursor", "hooks", HOOK_SCRIPT_NAME);
6926
+ const scriptPath2 = join9(workspaceRoot, ".cursor", "hooks", HOOK_SCRIPT_NAME);
6513
6927
  try {
6514
6928
  unlinkSync4(scriptPath2);
6515
6929
  console.log(success(`Deleted .cursor/hooks/${HOOK_SCRIPT_NAME}`));
6516
6930
  } catch {
6517
6931
  }
6518
- rmdirIfEmpty(join8(workspaceRoot, ".cursor", "hooks"));
6932
+ rmdirIfEmpty(join9(workspaceRoot, ".cursor", "hooks"));
6519
6933
  console.log();
6520
6934
  console.log(success("Hooks removed. Reload Cursor to deactivate."));
6521
6935
  console.log();
@@ -6538,9 +6952,9 @@ async function setupCursorCommand(flags) {
6538
6952
  console.log(success("API key configured"));
6539
6953
  console.log();
6540
6954
  console.log(` ${c.dim}[3/4]${c.reset} Installing compression hooks...`);
6541
- const hooksDir = join8(workspaceRoot, ".cursor", "hooks");
6542
- if (!existsSync13(hooksDir)) mkdirSync5(hooksDir, { recursive: true });
6543
- const scriptPath = join8(hooksDir, HOOK_SCRIPT_NAME);
6955
+ const hooksDir = join9(workspaceRoot, ".cursor", "hooks");
6956
+ if (!existsSync14(hooksDir)) mkdirSync5(hooksDir, { recursive: true });
6957
+ const scriptPath = join9(hooksDir, HOOK_SCRIPT_NAME);
6544
6958
  writeFileSync6(scriptPath, getHookScript(), { mode: 493 });
6545
6959
  console.log(success(`Wrote .cursor/hooks/${HOOK_SCRIPT_NAME}`));
6546
6960
  const hooks = readHooksJson(workspaceRoot);
@@ -6569,8 +6983,8 @@ init_loader();
6569
6983
  init_store();
6570
6984
  init_aggregator();
6571
6985
  init_format();
6572
- import { existsSync as existsSync14, readFileSync as readFileSync12 } from "fs";
6573
- import { join as join9 } from "path";
6986
+ import { existsSync as existsSync15, readFileSync as readFileSync13 } from "fs";
6987
+ import { join as join10 } from "path";
6574
6988
  async function statsCommand(flags) {
6575
6989
  if (!isConfigured()) {
6576
6990
  console.log(error(
@@ -6606,7 +7020,7 @@ async function statsCommand(flags) {
6606
7020
  agg.recordSkipped(s.connector, 0);
6607
7021
  }
6608
7022
  }
6609
- const cursorMetrics = parseCursorHookStats();
7023
+ const cursorMetrics = parseCursorHookStats2();
6610
7024
  if (cursorMetrics) {
6611
7025
  agg.setCursorMetrics(cursorMetrics);
6612
7026
  }
@@ -6759,14 +7173,14 @@ async function statsCommand(flags) {
6759
7173
  console.log(` ${c.dim}\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500${c.reset}`);
6760
7174
  console.log();
6761
7175
  }
6762
- function parseCursorHookStats() {
6763
- const logPath = join9(process.cwd(), ".fabric", "compress.log");
6764
- if (!existsSync14(logPath)) return null;
7176
+ function parseCursorHookStats2() {
7177
+ const logPath = join10(process.cwd(), ".fabric", "compress.log");
7178
+ if (!existsSync15(logPath)) return null;
6765
7179
  let compressions = 0, errors = 0;
6766
7180
  let tokensProcessed = 0, tokensSaved = 0;
6767
7181
  let apiMsSumMs = 0;
6768
7182
  const files = /* @__PURE__ */ new Set();
6769
- const lines = readFileSync12(logPath, "utf-8").trim().split("\n");
7183
+ const lines = readFileSync13(logPath, "utf-8").trim().split("\n");
6770
7184
  for (const line of lines) {
6771
7185
  try {
6772
7186
  const entry = JSON.parse(line);
@@ -6786,7 +7200,7 @@ function parseCursorHookStats() {
6786
7200
  }
6787
7201
  let cacheCount = 0;
6788
7202
  try {
6789
- cacheCount = countFilesRecursive(join9(process.cwd(), ".fabric", "cache"));
7203
+ cacheCount = countFilesRecursive2(join10(process.cwd(), ".fabric", "cache"));
6790
7204
  } catch {
6791
7205
  }
6792
7206
  return {
@@ -6799,13 +7213,13 @@ function parseCursorHookStats() {
6799
7213
  cacheCount
6800
7214
  };
6801
7215
  }
6802
- function countFilesRecursive(dir) {
6803
- if (!existsSync14(dir)) return 0;
6804
- const { readdirSync: readdirSync2 } = __require("fs");
7216
+ function countFilesRecursive2(dir) {
7217
+ if (!existsSync15(dir)) return 0;
7218
+ const { readdirSync: readdirSync3 } = __require("fs");
6805
7219
  let count = 0;
6806
- for (const entry of readdirSync2(dir, { withFileTypes: true })) {
7220
+ for (const entry of readdirSync3(dir, { withFileTypes: true })) {
6807
7221
  if (entry.isDirectory()) {
6808
- count += countFilesRecursive(join9(dir, entry.name));
7222
+ count += countFilesRecursive2(join10(dir, entry.name));
6809
7223
  } else {
6810
7224
  count++;
6811
7225
  }