@nxuss/lemma 1.10.0 → 1.12.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.
Files changed (114) hide show
  1. package/README.md +92 -819
  2. package/dist/cjs/cli/lemma-proxy.d.ts.map +1 -1
  3. package/dist/cjs/cli/lemma-proxy.js +21 -2
  4. package/dist/cjs/cli/lemma-proxy.js.map +1 -1
  5. package/dist/cjs/infra/mcp-tools.d.ts +7 -0
  6. package/dist/cjs/infra/mcp-tools.d.ts.map +1 -1
  7. package/dist/cjs/infra/mcp-tools.js +22 -0
  8. package/dist/cjs/infra/mcp-tools.js.map +1 -1
  9. package/dist/cjs/mcp/index.js +32 -7
  10. package/dist/cjs/mcp/index.js.map +1 -1
  11. package/dist/cjs/mcp/prompts.d.ts +1 -0
  12. package/dist/cjs/mcp/prompts.d.ts.map +1 -1
  13. package/dist/cjs/mcp/prompts.js +98 -0
  14. package/dist/cjs/mcp/prompts.js.map +1 -1
  15. package/dist/cjs/mcp/resources.d.ts +2 -0
  16. package/dist/cjs/mcp/resources.d.ts.map +1 -1
  17. package/dist/cjs/mcp/resources.js +26 -0
  18. package/dist/cjs/mcp/resources.js.map +1 -1
  19. package/dist/cjs/mcp/tool-profiles.d.ts.map +1 -1
  20. package/dist/cjs/mcp/tool-profiles.js +64 -45
  21. package/dist/cjs/mcp/tool-profiles.js.map +1 -1
  22. package/dist/cjs/mcp/tools.d.ts.map +1 -1
  23. package/dist/cjs/mcp/tools.js +309 -65
  24. package/dist/cjs/mcp/tools.js.map +1 -1
  25. package/dist/cjs/proxy/Gatekeeper.d.ts +29 -0
  26. package/dist/cjs/proxy/Gatekeeper.d.ts.map +1 -1
  27. package/dist/cjs/proxy/Gatekeeper.js +122 -1
  28. package/dist/cjs/proxy/Gatekeeper.js.map +1 -1
  29. package/dist/cjs/utils/AstSymbolCache.d.ts +29 -0
  30. package/dist/cjs/utils/AstSymbolCache.d.ts.map +1 -0
  31. package/dist/cjs/utils/AstSymbolCache.js +66 -0
  32. package/dist/cjs/utils/AstSymbolCache.js.map +1 -0
  33. package/dist/cjs/utils/ContextSqueezer.d.ts +24 -4
  34. package/dist/cjs/utils/ContextSqueezer.d.ts.map +1 -1
  35. package/dist/cjs/utils/ContextSqueezer.js +32 -3
  36. package/dist/cjs/utils/ContextSqueezer.js.map +1 -1
  37. package/dist/cjs/utils/OutputLedger.d.ts.map +1 -1
  38. package/dist/cjs/utils/OutputLedger.js +20 -2
  39. package/dist/cjs/utils/OutputLedger.js.map +1 -1
  40. package/dist/cjs/utils/ReadWorkspaceCache.d.ts.map +1 -1
  41. package/dist/cjs/utils/ReadWorkspaceCache.js +25 -2
  42. package/dist/cjs/utils/ReadWorkspaceCache.js.map +1 -1
  43. package/dist/cjs/utils/RoiLedger.d.ts +31 -0
  44. package/dist/cjs/utils/RoiLedger.d.ts.map +1 -0
  45. package/dist/cjs/utils/RoiLedger.js +113 -0
  46. package/dist/cjs/utils/RoiLedger.js.map +1 -0
  47. package/dist/cjs/utils/SharedLedgerStore.d.ts +26 -0
  48. package/dist/cjs/utils/SharedLedgerStore.d.ts.map +1 -0
  49. package/dist/cjs/utils/SharedLedgerStore.js +130 -0
  50. package/dist/cjs/utils/SharedLedgerStore.js.map +1 -0
  51. package/dist/cjs/utils/SpeculativeWarmer.d.ts +24 -0
  52. package/dist/cjs/utils/SpeculativeWarmer.d.ts.map +1 -0
  53. package/dist/cjs/utils/SpeculativeWarmer.js +73 -0
  54. package/dist/cjs/utils/SpeculativeWarmer.js.map +1 -0
  55. package/dist/cjs/utils/reportSavings.d.ts.map +1 -1
  56. package/dist/cjs/utils/reportSavings.js +11 -0
  57. package/dist/cjs/utils/reportSavings.js.map +1 -1
  58. package/dist/esm/cli/lemma-proxy.d.ts.map +1 -1
  59. package/dist/esm/cli/lemma-proxy.js +21 -2
  60. package/dist/esm/cli/lemma-proxy.js.map +1 -1
  61. package/dist/esm/infra/mcp-tools.d.ts +7 -0
  62. package/dist/esm/infra/mcp-tools.d.ts.map +1 -1
  63. package/dist/esm/infra/mcp-tools.js +22 -0
  64. package/dist/esm/infra/mcp-tools.js.map +1 -1
  65. package/dist/esm/mcp/index.js +34 -9
  66. package/dist/esm/mcp/index.js.map +1 -1
  67. package/dist/esm/mcp/prompts.d.ts +1 -0
  68. package/dist/esm/mcp/prompts.d.ts.map +1 -1
  69. package/dist/esm/mcp/prompts.js +95 -1
  70. package/dist/esm/mcp/prompts.js.map +1 -1
  71. package/dist/esm/mcp/resources.d.ts +2 -0
  72. package/dist/esm/mcp/resources.d.ts.map +1 -1
  73. package/dist/esm/mcp/resources.js +27 -2
  74. package/dist/esm/mcp/resources.js.map +1 -1
  75. package/dist/esm/mcp/tool-profiles.d.ts.map +1 -1
  76. package/dist/esm/mcp/tool-profiles.js +64 -45
  77. package/dist/esm/mcp/tool-profiles.js.map +1 -1
  78. package/dist/esm/mcp/tools.d.ts.map +1 -1
  79. package/dist/esm/mcp/tools.js +309 -65
  80. package/dist/esm/mcp/tools.js.map +1 -1
  81. package/dist/esm/proxy/Gatekeeper.d.ts +29 -0
  82. package/dist/esm/proxy/Gatekeeper.d.ts.map +1 -1
  83. package/dist/esm/proxy/Gatekeeper.js +120 -0
  84. package/dist/esm/proxy/Gatekeeper.js.map +1 -1
  85. package/dist/esm/utils/AstSymbolCache.d.ts +29 -0
  86. package/dist/esm/utils/AstSymbolCache.d.ts.map +1 -0
  87. package/dist/esm/utils/AstSymbolCache.js +58 -0
  88. package/dist/esm/utils/AstSymbolCache.js.map +1 -0
  89. package/dist/esm/utils/ContextSqueezer.d.ts +24 -4
  90. package/dist/esm/utils/ContextSqueezer.d.ts.map +1 -1
  91. package/dist/esm/utils/ContextSqueezer.js +32 -3
  92. package/dist/esm/utils/ContextSqueezer.js.map +1 -1
  93. package/dist/esm/utils/OutputLedger.d.ts.map +1 -1
  94. package/dist/esm/utils/OutputLedger.js +20 -2
  95. package/dist/esm/utils/OutputLedger.js.map +1 -1
  96. package/dist/esm/utils/ReadWorkspaceCache.d.ts.map +1 -1
  97. package/dist/esm/utils/ReadWorkspaceCache.js +25 -2
  98. package/dist/esm/utils/ReadWorkspaceCache.js.map +1 -1
  99. package/dist/esm/utils/RoiLedger.d.ts +31 -0
  100. package/dist/esm/utils/RoiLedger.d.ts.map +1 -0
  101. package/dist/esm/utils/RoiLedger.js +104 -0
  102. package/dist/esm/utils/RoiLedger.js.map +1 -0
  103. package/dist/esm/utils/SharedLedgerStore.d.ts +26 -0
  104. package/dist/esm/utils/SharedLedgerStore.d.ts.map +1 -0
  105. package/dist/esm/utils/SharedLedgerStore.js +122 -0
  106. package/dist/esm/utils/SharedLedgerStore.js.map +1 -0
  107. package/dist/esm/utils/SpeculativeWarmer.d.ts +24 -0
  108. package/dist/esm/utils/SpeculativeWarmer.d.ts.map +1 -0
  109. package/dist/esm/utils/SpeculativeWarmer.js +66 -0
  110. package/dist/esm/utils/SpeculativeWarmer.js.map +1 -0
  111. package/dist/esm/utils/reportSavings.d.ts.map +1 -1
  112. package/dist/esm/utils/reportSavings.js +11 -0
  113. package/dist/esm/utils/reportSavings.js.map +1 -1
  114. package/package.json +2 -2
@@ -8,6 +8,7 @@ exports.getLastEmitted = getLastEmitted;
8
8
  exports.recordSent = recordSent;
9
9
  exports.resetReadCache = resetReadCache;
10
10
  const crypto_1 = __importDefault(require("crypto"));
11
+ const SharedLedgerStore_1 = require("./SharedLedgerStore");
11
12
  // Session-scoped (lives for the MCP server process lifetime, which is normally the
12
13
  // whole conversation). Tracks the content hash we last actually sent back to the
13
14
  // model for a given (filePath, view) pair, so a second identical read doesn't
@@ -42,11 +43,24 @@ function cacheKey(resolvedPath, view) {
42
43
  ];
43
44
  return parts.join("::");
44
45
  }
46
+ function getSharedRead(resolvedPath, view) {
47
+ const raw = (0, SharedLedgerStore_1.getSharedEntry)(process.cwd(), "read-cache", cacheKey(resolvedPath, view));
48
+ if (!raw)
49
+ return null;
50
+ try {
51
+ return JSON.parse(raw);
52
+ }
53
+ catch {
54
+ return null;
55
+ }
56
+ }
45
57
  /** Check whether this exact (path, view) content was already sent this session. */
46
58
  function checkAlreadySent(resolvedPath, view, rawContent) {
47
59
  const hash = sha256(rawContent);
48
60
  const key = cacheKey(resolvedPath, view);
49
- const unchanged = lastSent.get(key) === hash;
61
+ // In-process history wins; a sibling fork's shared entry only counts when this process
62
+ // has never seen the key itself — same "opt-in, scoped, fail-open" contract as OutputLedger.
63
+ const unchanged = lastSent.has(key) ? lastSent.get(key) === hash : getSharedRead(resolvedPath, view)?.hash === hash;
50
64
  return { unchanged, hash };
51
65
  }
52
66
  /**
@@ -55,12 +69,21 @@ function checkAlreadySent(resolvedPath, view, rawContent) {
55
69
  * "send the full file", never "assume it is unchanged".
56
70
  */
57
71
  function getLastEmitted(resolvedPath, view) {
58
- return lastEmitted.get(cacheKey(resolvedPath, view)) ?? null;
72
+ const key = cacheKey(resolvedPath, view);
73
+ const local = lastEmitted.get(key);
74
+ if (local !== undefined)
75
+ return local;
76
+ return getSharedRead(resolvedPath, view)?.emitted ?? null;
59
77
  }
60
78
  /** Record that this content was just sent, so future identical reads short-circuit. */
61
79
  function recordSent(resolvedPath, view, hash, emitted) {
62
80
  const key = cacheKey(resolvedPath, view);
63
81
  lastSent.set(key, hash);
82
+ // Mirrors the local size cap: an oversized entry is skipped locally too, so keeping it
83
+ // out of the shared file avoids bloating something every sibling process reads on every hit.
84
+ if (emitted === undefined || emitted.length <= MAX_ENTRY_CHARS) {
85
+ (0, SharedLedgerStore_1.setSharedEntry)(process.cwd(), "read-cache", key, JSON.stringify({ hash, emitted }));
86
+ }
64
87
  if (emitted === undefined)
65
88
  return;
66
89
  const previous = lastEmitted.get(key);
@@ -1 +1 @@
1
- {"version":3,"file":"ReadWorkspaceCache.js","sourceRoot":"","sources":["../../../src/utils/ReadWorkspaceCache.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ReadWorkspaceCache.js","sourceRoot":"","sources":["../../../src/utils/ReadWorkspaceCache.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * RoiLedger — per-(repo, tool) net token history, persisted across sessions.
3
+ *
4
+ * `SavingsLedger` already tracks saved/spent totals, but only by *source* (cache,
5
+ * contextSqueeze, ...) and only in `~/.lemma-cache/ledger.json` — one file, no notion of
6
+ * which repo or which specific tool earned its keep. That is not enough to answer "does
7
+ * `affected_tests` pay for its schema in THIS repo", which is what `tool-profiles.ts` needs
8
+ * to auto-tune the advertised surface instead of relying purely on the hand-curated
9
+ * `CORE_TOOL_NAMES` list.
10
+ *
11
+ * Scope: one small JSON file per repo, keyed by a hash of the repo path, holding cumulative
12
+ * (calls, tokensSaved, tokensSpent) per tool name. Fail-open throughout — a lookup or write
13
+ * that errors must never break the caller; it just means no history this time.
14
+ *
15
+ * Hard rule inherited from `tool-profiles.ts`: the tool surface resolves once at startup and
16
+ * never mutates mid-session. This ledger is written continuously, but only ever *read* by
17
+ * `resolveToolSurface` once, at process start — so a promotion always reflects PRIOR
18
+ * sessions, never the one currently running.
19
+ */
20
+ export interface ToolRoiStats {
21
+ calls: number;
22
+ tokensSaved: number;
23
+ tokensSpent: number;
24
+ }
25
+ export declare function recordRoiSaved(repoRoot: string, tool: string, tokensSaved: number): void;
26
+ export declare function recordRoiSpent(repoRoot: string, tool: string, tokensSpent: number): void;
27
+ /** All per-tool history for this repo, or an empty map if none exists yet or the read fails. */
28
+ export declare function getRoiStats(repoRoot: string): Record<string, ToolRoiStats>;
29
+ /** Test seam only — production code should never need to wipe another repo's history. */
30
+ export declare function resetRoiLedger(repoRoot: string): void;
31
+ //# sourceMappingURL=RoiLedger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RoiLedger.d.ts","sourceRoot":"","sources":["../../../src/utils/RoiLedger.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAMH,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;CACrB;AA8DD,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI,CAExF;AAED,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI,CAExF;AAED,gGAAgG;AAChG,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAM1E;AAED,yFAAyF;AACzF,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAMrD"}
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ /**
3
+ * RoiLedger — per-(repo, tool) net token history, persisted across sessions.
4
+ *
5
+ * `SavingsLedger` already tracks saved/spent totals, but only by *source* (cache,
6
+ * contextSqueeze, ...) and only in `~/.lemma-cache/ledger.json` — one file, no notion of
7
+ * which repo or which specific tool earned its keep. That is not enough to answer "does
8
+ * `affected_tests` pay for its schema in THIS repo", which is what `tool-profiles.ts` needs
9
+ * to auto-tune the advertised surface instead of relying purely on the hand-curated
10
+ * `CORE_TOOL_NAMES` list.
11
+ *
12
+ * Scope: one small JSON file per repo, keyed by a hash of the repo path, holding cumulative
13
+ * (calls, tokensSaved, tokensSpent) per tool name. Fail-open throughout — a lookup or write
14
+ * that errors must never break the caller; it just means no history this time.
15
+ *
16
+ * Hard rule inherited from `tool-profiles.ts`: the tool surface resolves once at startup and
17
+ * never mutates mid-session. This ledger is written continuously, but only ever *read* by
18
+ * `resolveToolSurface` once, at process start — so a promotion always reflects PRIOR
19
+ * sessions, never the one currently running.
20
+ */
21
+ var __importDefault = (this && this.__importDefault) || function (mod) {
22
+ return (mod && mod.__esModule) ? mod : { "default": mod };
23
+ };
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.recordRoiSaved = recordRoiSaved;
26
+ exports.recordRoiSpent = recordRoiSpent;
27
+ exports.getRoiStats = getRoiStats;
28
+ exports.resetRoiLedger = resetRoiLedger;
29
+ const fs_1 = __importDefault(require("fs"));
30
+ const path_1 = __importDefault(require("path"));
31
+ const crypto_1 = __importDefault(require("crypto"));
32
+ const HOME = process.env.HOME || process.env.USERPROFILE || "~";
33
+ const ROI_DIR = path_1.default.join(HOME, ".lemma-cache", "roi");
34
+ function repoHash(repoRoot) {
35
+ return crypto_1.default.createHash("sha256").update(path_1.default.resolve(repoRoot)).digest("hex").slice(0, 16);
36
+ }
37
+ function roiFilePath(repoRoot) {
38
+ return path_1.default.join(ROI_DIR, `${repoHash(repoRoot)}.json`);
39
+ }
40
+ function emptyStats() {
41
+ return { calls: 0, tokensSaved: 0, tokensSpent: 0 };
42
+ }
43
+ function load(repoRoot) {
44
+ try {
45
+ const p = roiFilePath(repoRoot);
46
+ if (fs_1.default.existsSync(p)) {
47
+ const parsed = JSON.parse(fs_1.default.readFileSync(p, "utf8"));
48
+ if (parsed && typeof parsed === "object" && parsed.tools)
49
+ return parsed;
50
+ }
51
+ }
52
+ catch {
53
+ // Corrupt or unreadable file must never break the caller — start clean for this write.
54
+ }
55
+ return { tools: {}, updatedAt: Date.now() };
56
+ }
57
+ function save(repoRoot, data) {
58
+ try {
59
+ if (!fs_1.default.existsSync(ROI_DIR))
60
+ fs_1.default.mkdirSync(ROI_DIR, { recursive: true });
61
+ data.updatedAt = Date.now();
62
+ fs_1.default.writeFileSync(roiFilePath(repoRoot), JSON.stringify(data, null, 2), "utf8");
63
+ }
64
+ catch {
65
+ // Non-critical: losing one write just means the next read sees slightly stale history.
66
+ }
67
+ }
68
+ function bump(repoRoot, tool, field, amount) {
69
+ if (!tool || !(amount > 0))
70
+ return;
71
+ try {
72
+ const data = load(repoRoot);
73
+ const stats = data.tools[tool] || emptyStats();
74
+ if (field === "tokensSaved")
75
+ stats.tokensSaved += amount;
76
+ else
77
+ stats.tokensSpent += amount;
78
+ // `calls` counts either kind of event once — it is "how many times this tool did
79
+ // something billable", not a sum of both fields.
80
+ stats.calls += 1;
81
+ data.tools[tool] = stats;
82
+ save(repoRoot, data);
83
+ }
84
+ catch {
85
+ // Fail-open: ROI bookkeeping must never be why a tool call fails.
86
+ }
87
+ }
88
+ function recordRoiSaved(repoRoot, tool, tokensSaved) {
89
+ bump(repoRoot, tool, "tokensSaved", tokensSaved);
90
+ }
91
+ function recordRoiSpent(repoRoot, tool, tokensSpent) {
92
+ bump(repoRoot, tool, "tokensSpent", tokensSpent);
93
+ }
94
+ /** All per-tool history for this repo, or an empty map if none exists yet or the read fails. */
95
+ function getRoiStats(repoRoot) {
96
+ try {
97
+ return load(repoRoot).tools;
98
+ }
99
+ catch {
100
+ return {};
101
+ }
102
+ }
103
+ /** Test seam only — production code should never need to wipe another repo's history. */
104
+ function resetRoiLedger(repoRoot) {
105
+ try {
106
+ if (fs_1.default.existsSync(roiFilePath(repoRoot)))
107
+ fs_1.default.unlinkSync(roiFilePath(repoRoot));
108
+ }
109
+ catch {
110
+ // ignore
111
+ }
112
+ }
113
+ //# sourceMappingURL=RoiLedger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RoiLedger.js","sourceRoot":"","sources":["../../../src/utils/RoiLedger.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * SharedLedgerStore — file-backed cross-process backing for `OutputLedger` and
3
+ * `ReadWorkspaceCache`, so subagents that genuinely share a conversation (a `fork`, which
4
+ * inherits the parent's full context) don't independently re-read the same files or re-run
5
+ * the same commands the parent already put in that shared context.
6
+ *
7
+ * OFF BY DEFAULT — opt in with `LEMMA_SHARED_LEDGER=1` (or `mcp.sharedLedger: true` in
8
+ * lemma.config.json) AND a `LEMMA_AGENT_TREE_ID` env var identifying the fork tree. Both are
9
+ * required on purpose:
10
+ *
11
+ * - Without a tree id, every process on the machine would share one ledger, and a plain
12
+ * fresh subagent (no inherited context) would get told a file is "UNCHANGED" or a command
13
+ * "IDENTICAL" for content it never actually received — suppressing content the model
14
+ * never saw, which is the one thing these caches must never do.
15
+ * - The tree id scopes sharing to processes an orchestrator has explicitly declared share a
16
+ * context (i.e. `fork` subagents of the same parent), which is the only case where "another
17
+ * process already has this" is actually true for THIS process too.
18
+ *
19
+ * TTL is short (30 min): a fork tree is a single task, not a persistent cache.
20
+ */
21
+ export declare function isSharedLedgerEnabled(cwd?: string): boolean;
22
+ /** Get a shared value for `key` (namespaced), or null if disabled, missing, or expired. */
23
+ export declare function getSharedEntry(cwd: string, namespace: "output-ledger" | "read-cache", key: string): string | null;
24
+ /** Record a value under `key` (namespaced) for other trees-scoped processes to read. */
25
+ export declare function setSharedEntry(cwd: string, namespace: "output-ledger" | "read-cache", key: string, value: string): void;
26
+ //# sourceMappingURL=SharedLedgerStore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SharedLedgerStore.d.ts","sourceRoot":"","sources":["../../../src/utils/SharedLedgerStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AASH,wBAAgB,qBAAqB,CAAC,GAAG,GAAE,MAAsB,GAAG,OAAO,CAa1E;AAgDD,2FAA2F;AAC3F,wBAAgB,cAAc,CAC5B,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,eAAe,GAAG,YAAY,EACzC,GAAG,EAAE,MAAM,GACV,MAAM,GAAG,IAAI,CAWf;AAED,wFAAwF;AACxF,wBAAgB,cAAc,CAC5B,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,eAAe,GAAG,YAAY,EACzC,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,GACZ,IAAI,CAcN"}
@@ -0,0 +1,130 @@
1
+ "use strict";
2
+ /**
3
+ * SharedLedgerStore — file-backed cross-process backing for `OutputLedger` and
4
+ * `ReadWorkspaceCache`, so subagents that genuinely share a conversation (a `fork`, which
5
+ * inherits the parent's full context) don't independently re-read the same files or re-run
6
+ * the same commands the parent already put in that shared context.
7
+ *
8
+ * OFF BY DEFAULT — opt in with `LEMMA_SHARED_LEDGER=1` (or `mcp.sharedLedger: true` in
9
+ * lemma.config.json) AND a `LEMMA_AGENT_TREE_ID` env var identifying the fork tree. Both are
10
+ * required on purpose:
11
+ *
12
+ * - Without a tree id, every process on the machine would share one ledger, and a plain
13
+ * fresh subagent (no inherited context) would get told a file is "UNCHANGED" or a command
14
+ * "IDENTICAL" for content it never actually received — suppressing content the model
15
+ * never saw, which is the one thing these caches must never do.
16
+ * - The tree id scopes sharing to processes an orchestrator has explicitly declared share a
17
+ * context (i.e. `fork` subagents of the same parent), which is the only case where "another
18
+ * process already has this" is actually true for THIS process too.
19
+ *
20
+ * TTL is short (30 min): a fork tree is a single task, not a persistent cache.
21
+ */
22
+ var __importDefault = (this && this.__importDefault) || function (mod) {
23
+ return (mod && mod.__esModule) ? mod : { "default": mod };
24
+ };
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.isSharedLedgerEnabled = isSharedLedgerEnabled;
27
+ exports.getSharedEntry = getSharedEntry;
28
+ exports.setSharedEntry = setSharedEntry;
29
+ const fs_1 = __importDefault(require("fs"));
30
+ const path_1 = __importDefault(require("path"));
31
+ const crypto_1 = __importDefault(require("crypto"));
32
+ const HOME = process.env.HOME || process.env.USERPROFILE || "~";
33
+ const TTL_MS = 30 * 60 * 1000;
34
+ function isSharedLedgerEnabled(cwd = process.cwd()) {
35
+ if (!process.env.LEMMA_AGENT_TREE_ID)
36
+ return false;
37
+ if (process.env.LEMMA_SHARED_LEDGER === "1")
38
+ return true;
39
+ try {
40
+ const configPath = path_1.default.join(cwd, "lemma.config.json");
41
+ if (fs_1.default.existsSync(configPath)) {
42
+ const config = JSON.parse(fs_1.default.readFileSync(configPath, "utf8"));
43
+ return config?.mcp?.sharedLedger === true;
44
+ }
45
+ }
46
+ catch {
47
+ // Config errors must never silently turn ON cross-process sharing.
48
+ }
49
+ return false;
50
+ }
51
+ function workspaceHash(cwd) {
52
+ return crypto_1.default.createHash("sha256").update(path_1.default.resolve(cwd)).digest("hex").slice(0, 16);
53
+ }
54
+ function storeDir(cwd) {
55
+ const treeId = process.env.LEMMA_AGENT_TREE_ID;
56
+ if (!treeId)
57
+ return null;
58
+ // Sanitize: the tree id lands in a path segment.
59
+ const safeTreeId = treeId.replace(/[^a-zA-Z0-9_-]/g, "").slice(0, 64) || "default";
60
+ return path_1.default.join(HOME, ".lemma-cache", "session", workspaceHash(cwd), safeTreeId);
61
+ }
62
+ function storeFile(cwd, namespace) {
63
+ const dir = storeDir(cwd);
64
+ if (!dir)
65
+ return null;
66
+ return path_1.default.join(dir, `${namespace}.json`);
67
+ }
68
+ function loadStore(cwd, namespace) {
69
+ try {
70
+ const file = storeFile(cwd, namespace);
71
+ if (!file || !fs_1.default.existsSync(file))
72
+ return {};
73
+ const parsed = JSON.parse(fs_1.default.readFileSync(file, "utf8"));
74
+ return parsed && typeof parsed === "object" ? parsed : {};
75
+ }
76
+ catch {
77
+ return {};
78
+ }
79
+ }
80
+ function saveStore(cwd, namespace, data) {
81
+ try {
82
+ const dir = storeDir(cwd);
83
+ const file = storeFile(cwd, namespace);
84
+ if (!dir || !file)
85
+ return;
86
+ if (!fs_1.default.existsSync(dir))
87
+ fs_1.default.mkdirSync(dir, { recursive: true });
88
+ fs_1.default.writeFileSync(file, JSON.stringify(data), "utf8");
89
+ }
90
+ catch {
91
+ // Fail-open: the caller falls back to in-process-only behavior.
92
+ }
93
+ }
94
+ /** Get a shared value for `key` (namespaced), or null if disabled, missing, or expired. */
95
+ function getSharedEntry(cwd, namespace, key) {
96
+ if (!isSharedLedgerEnabled(cwd))
97
+ return null;
98
+ try {
99
+ const data = loadStore(cwd, namespace);
100
+ const entry = data[key];
101
+ if (!entry)
102
+ return null;
103
+ if (Date.now() - entry.at > TTL_MS)
104
+ return null;
105
+ return entry.value;
106
+ }
107
+ catch {
108
+ return null;
109
+ }
110
+ }
111
+ /** Record a value under `key` (namespaced) for other trees-scoped processes to read. */
112
+ function setSharedEntry(cwd, namespace, key, value) {
113
+ if (!isSharedLedgerEnabled(cwd))
114
+ return;
115
+ try {
116
+ const data = loadStore(cwd, namespace);
117
+ data[key] = { value, at: Date.now() };
118
+ // Bound the file: drop expired entries whenever we touch it, rather than growing forever.
119
+ const now = Date.now();
120
+ for (const k of Object.keys(data)) {
121
+ if (now - data[k].at > TTL_MS)
122
+ delete data[k];
123
+ }
124
+ saveStore(cwd, namespace, data);
125
+ }
126
+ catch {
127
+ // Fail-open.
128
+ }
129
+ }
130
+ //# sourceMappingURL=SharedLedgerStore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SharedLedgerStore.js","sourceRoot":"","sources":["../../../src/utils/SharedLedgerStore.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * SpeculativeWarmer — precomputes local caches for a file's import neighbors while the
3
+ * model is off reasoning about what it just read, so a follow-up `get_ast_hologram` or
4
+ * `depgraph`/`affected_tests` call on a neighbor is a cache hit instead of a cold parse.
5
+ *
6
+ * HARD RULE: this warms local caches only (`DepGraph`'s graph cache, `AstSymbolCache`) and
7
+ * must never touch anything that reaches the model's context. It runs after the response for
8
+ * the triggering tool call has already been returned, off the event loop's current tick, and
9
+ * every failure is swallowed — a warming run that throws or runs slow must never be visible
10
+ * to whoever is waiting on the actual tool call.
11
+ */
12
+ import { AstSymbol } from "./AstSymbolCache";
13
+ /**
14
+ * `extractSymbolsWithTsCompiler` lives in `mcp/tools.ts`, which already imports this module —
15
+ * importing it back here would be circular. The dispatcher registers it once at startup instead.
16
+ */
17
+ export declare function registerSymbolExtractor(extractor: (absPath: string, relPath: string, parseErrors?: string[]) => AstSymbol[]): void;
18
+ /**
19
+ * Fire-and-forget: warm the import-neighborhood of `filePath` after a real read. Safe to call
20
+ * unconditionally — it is a no-op (not just cheap, actually skipped) whenever the extractor
21
+ * hasn't been registered yet, since that means startup isn't finished.
22
+ */
23
+ export declare function warmNeighbors(workspaceRoot: string, filePath: string): void;
24
+ //# sourceMappingURL=SpeculativeWarmer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SpeculativeWarmer.d.ts","sourceRoot":"","sources":["../../../src/utils/SpeculativeWarmer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAKH,OAAO,EAAsC,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAQjF;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,EAAE,KAAK,SAAS,EAAE,GACnF,IAAI,CAEN;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,aAAa,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CA4B3E"}
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ /**
3
+ * SpeculativeWarmer — precomputes local caches for a file's import neighbors while the
4
+ * model is off reasoning about what it just read, so a follow-up `get_ast_hologram` or
5
+ * `depgraph`/`affected_tests` call on a neighbor is a cache hit instead of a cold parse.
6
+ *
7
+ * HARD RULE: this warms local caches only (`DepGraph`'s graph cache, `AstSymbolCache`) and
8
+ * must never touch anything that reaches the model's context. It runs after the response for
9
+ * the triggering tool call has already been returned, off the event loop's current tick, and
10
+ * every failure is swallowed — a warming run that throws or runs slow must never be visible
11
+ * to whoever is waiting on the actual tool call.
12
+ */
13
+ var __importDefault = (this && this.__importDefault) || function (mod) {
14
+ return (mod && mod.__esModule) ? mod : { "default": mod };
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.registerSymbolExtractor = registerSymbolExtractor;
18
+ exports.warmNeighbors = warmNeighbors;
19
+ const fs_1 = __importDefault(require("fs"));
20
+ const path_1 = __importDefault(require("path"));
21
+ const DepGraph_1 = require("./DepGraph");
22
+ const AstSymbolCache_1 = require("./AstSymbolCache");
23
+ /** How many neighbor files a single warm pass will touch — bounded so this stays cheap. */
24
+ const MAX_NEIGHBORS = 8;
25
+ const MAX_DEPTH = 1;
26
+ let extractorRef = null;
27
+ /**
28
+ * `extractSymbolsWithTsCompiler` lives in `mcp/tools.ts`, which already imports this module —
29
+ * importing it back here would be circular. The dispatcher registers it once at startup instead.
30
+ */
31
+ function registerSymbolExtractor(extractor) {
32
+ extractorRef = extractor;
33
+ }
34
+ /**
35
+ * Fire-and-forget: warm the import-neighborhood of `filePath` after a real read. Safe to call
36
+ * unconditionally — it is a no-op (not just cheap, actually skipped) whenever the extractor
37
+ * hasn't been registered yet, since that means startup isn't finished.
38
+ */
39
+ function warmNeighbors(workspaceRoot, filePath) {
40
+ if (!extractorRef)
41
+ return;
42
+ const extractor = extractorRef;
43
+ // Deferred and unref'd so it can never keep the process alive or delay the response that
44
+ // triggered it — this must cost the triggering call nothing.
45
+ const timer = setTimeout(() => {
46
+ try {
47
+ const rel = path_1.default.relative(workspaceRoot, filePath).split(path_1.default.sep).join("/");
48
+ const graph = (0, DepGraph_1.buildDepGraphMap)(workspaceRoot);
49
+ const node = graph.get(rel);
50
+ if (!node)
51
+ return;
52
+ const neighbors = (0, DepGraph_1.bfsDepGraph)(graph, rel, "forward", MAX_DEPTH);
53
+ let warmed = 0;
54
+ for (const [neighborRel, { node: neighborNode }] of neighbors) {
55
+ if (warmed >= MAX_NEIGHBORS)
56
+ break;
57
+ if (neighborRel === rel)
58
+ continue;
59
+ if ((0, AstSymbolCache_1.hasFreshEntry)(neighborNode.abs))
60
+ continue; // already warm — nothing to do
61
+ if (!fs_1.default.existsSync(neighborNode.abs))
62
+ continue;
63
+ (0, AstSymbolCache_1.getOrExtractSymbols)(neighborNode.abs, neighborRel, extractor);
64
+ warmed++;
65
+ }
66
+ }
67
+ catch {
68
+ // Warming must never surface an error anywhere the model or the user can see it.
69
+ }
70
+ }, 0);
71
+ timer.unref?.();
72
+ }
73
+ //# sourceMappingURL=SpeculativeWarmer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SpeculativeWarmer.js","sourceRoot":"","sources":["../../../src/utils/SpeculativeWarmer.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;GAUG;;;;;AAiBH,0DAIC;AAOD,sCA4BC;AAtDD,4CAAoB;AACpB,gDAAwB;AACxB,yCAA2D;AAC3D,qDAAiF;AAEjF,2FAA2F;AAC3F,MAAM,aAAa,GAAG,CAAC,CAAC;AACxB,MAAM,SAAS,GAAG,CAAC,CAAC;AAEpB,IAAI,YAAY,GAAuF,IAAI,CAAC;AAE5G;;;GAGG;AACH,SAAgB,uBAAuB,CACrC,SAAoF;IAEpF,YAAY,GAAG,SAAS,CAAC;AAC3B,CAAC;AAED;;;;GAIG;AACH,SAAgB,aAAa,CAAC,aAAqB,EAAE,QAAgB;IACnE,IAAI,CAAC,YAAY;QAAE,OAAO;IAC1B,MAAM,SAAS,GAAG,YAAY,CAAC;IAE/B,yFAAyF;IACzF,6DAA6D;IAC7D,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;QAC5B,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,cAAI,CAAC,QAAQ,CAAC,aAAa,EAAE,QAAQ,CAAC,CAAC,KAAK,CAAC,cAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YAC7E,MAAM,KAAK,GAAG,IAAA,2BAAgB,EAAC,aAAa,CAAC,CAAC;YAC9C,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YAC5B,IAAI,CAAC,IAAI;gBAAE,OAAO;YAElB,MAAM,SAAS,GAAG,IAAA,sBAAW,EAAC,KAAK,EAAE,GAAG,EAAE,SAAS,EAAE,SAAS,CAAC,CAAC;YAChE,IAAI,MAAM,GAAG,CAAC,CAAC;YACf,KAAK,MAAM,CAAC,WAAW,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC,IAAI,SAAS,EAAE,CAAC;gBAC9D,IAAI,MAAM,IAAI,aAAa;oBAAE,MAAM;gBACnC,IAAI,WAAW,KAAK,GAAG;oBAAE,SAAS;gBAClC,IAAI,IAAA,8BAAa,EAAC,YAAY,CAAC,GAAG,CAAC;oBAAE,SAAS,CAAC,+BAA+B;gBAC9E,IAAI,CAAC,YAAE,CAAC,UAAU,CAAC,YAAY,CAAC,GAAG,CAAC;oBAAE,SAAS;gBAC/C,IAAA,oCAAmB,EAAC,YAAY,CAAC,GAAG,EAAE,WAAW,EAAE,SAAS,CAAC,CAAC;gBAC9D,MAAM,EAAE,CAAC;YACX,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,iFAAiF;QACnF,CAAC;IACH,CAAC,EAAE,CAAC,CAAC,CAAC;IACN,KAAK,CAAC,KAAK,EAAE,EAAE,CAAC;AAClB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"reportSavings.d.ts","sourceRoot":"","sources":["../../../src/utils/reportSavings.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAOH,KAAK,aAAa,GAAG,WAAW,GAAG,gBAAgB,GAAG,cAAc,GAAG,mBAAmB,GAAG,OAAO,CAAC;AAErG,UAAU,aAAa;IACrB,MAAM,EAAE,aAAa,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,UAAU,cAAc;IACtB,MAAM,EAAE,aAAa,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,KAAK,aAAa,GAAG,aAAa,GAAG,cAAc,CAAC;AAYpD;;;GAGG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI,CAE1D;AAED,KAAK,UAAU,GAAG,iBAAiB,GAAG,YAAY,GAAG,WAAW,CAAC;AAEjE,UAAU,WAAW;IACnB,MAAM,EAAE,UAAU,CAAC;IACnB,mDAAmD;IACnD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,0EAA0E;IAC1E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,CAErD"}
1
+ {"version":3,"file":"reportSavings.d.ts","sourceRoot":"","sources":["../../../src/utils/reportSavings.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAQH,KAAK,aAAa,GAAG,WAAW,GAAG,gBAAgB,GAAG,cAAc,GAAG,mBAAmB,GAAG,OAAO,CAAC;AAErG,UAAU,aAAa;IACrB,MAAM,EAAE,aAAa,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,UAAU,cAAc;IACtB,MAAM,EAAE,aAAa,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,KAAK,aAAa,GAAG,aAAa,GAAG,cAAc,CAAC;AAYpD;;;GAGG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI,CAQ1D;AAED,KAAK,UAAU,GAAG,iBAAiB,GAAG,YAAY,GAAG,WAAW,CAAC;AAEjE,UAAU,WAAW;IACnB,MAAM,EAAE,UAAU,CAAC;IACnB,mDAAmD;IACnD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,0EAA0E;IAC1E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,CAMrD"}
@@ -15,6 +15,7 @@ const fs_1 = __importDefault(require("fs"));
15
15
  const path_1 = __importDefault(require("path"));
16
16
  const os_1 = __importDefault(require("os"));
17
17
  const http_1 = __importDefault(require("http"));
18
+ const RoiLedger_1 = require("./RoiLedger");
18
19
  function getProxyPort() {
19
20
  try {
20
21
  const portFile = path_1.default.join(os_1.default.homedir(), '.lemma-cache/proxy.port');
@@ -31,6 +32,12 @@ function getProxyPort() {
31
32
  */
32
33
  function reportSavings(payload) {
33
34
  post('/api/savings-record', payload);
35
+ if (payload.toolName) {
36
+ const tokens = 'tokens' in payload ? payload.tokens : Math.floor(Math.max(0, payload.charsBefore - payload.charsAfter) / 4);
37
+ // Best-effort, repo-scoped: process.cwd() is the workspace root for every MCP tool
38
+ // handler, same assumption `tools.ts` already makes throughout.
39
+ (0, RoiLedger_1.recordRoiSaved)(process.cwd(), payload.toolName, tokens);
40
+ }
34
41
  }
35
42
  /**
36
43
  * Report tokens Lemma *spent* to the proxy's ledger. Same fire-and-forget contract
@@ -39,6 +46,10 @@ function reportSavings(payload) {
39
46
  */
40
47
  function reportCost(payload) {
41
48
  post('/api/cost-record', payload);
49
+ if (payload.toolName) {
50
+ const tokens = typeof payload.tokens === 'number' ? payload.tokens : Math.floor(Math.max(0, payload.chars || 0) / 4);
51
+ (0, RoiLedger_1.recordRoiSpent)(process.cwd(), payload.toolName, tokens);
52
+ }
42
53
  }
43
54
  function post(pathname, payload) {
44
55
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"reportSavings.js","sourceRoot":"","sources":["../../../src/utils/reportSavings.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;;;;AA0CH,sCAEC;AAkBD,gCAEC;AA9DD,4CAAoB;AACpB,gDAAwB;AACxB,4CAAoB;AACpB,gDAAwB;AAuBxB,SAAS,YAAY;IACnB,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,cAAI,CAAC,IAAI,CAAC,YAAE,CAAC,OAAO,EAAE,EAAE,yBAAyB,CAAC,CAAC;QACpE,IAAI,YAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC5B,OAAO,YAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;QAClD,CAAC;IACH,CAAC;IAAC,MAAM,CAAC,CAAA,CAAC;IACV,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;GAGG;AACH,SAAgB,aAAa,CAAC,OAAsB;IAClD,IAAI,CAAC,qBAAqB,EAAE,OAAO,CAAC,CAAC;AACvC,CAAC;AAaD;;;;GAIG;AACH,SAAgB,UAAU,CAAC,OAAoB;IAC7C,IAAI,CAAC,kBAAkB,EAAE,OAAO,CAAC,CAAC;AACpC,CAAC;AAED,SAAS,IAAI,CAAC,QAAgB,EAAE,OAAgB;IAC9C,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,YAAY,EAAE,CAAC;QAC5B,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;QAErC,MAAM,GAAG,GAAG,cAAI,CAAC,OAAO,CAAC;YACvB,QAAQ,EAAE,WAAW;YACrB,IAAI,EAAE,QAAQ,CAAC,IAAI,CAAC;YACpB,IAAI,EAAE,QAAQ;YACd,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,cAAc,EAAE,kBAAkB;gBAClC,gBAAgB,EAAE,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC;aAC1C;SACF,CAAC,CAAC;QAEH,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC,CAAC,yCAAyC;QACpE,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAChB,GAAG,CAAC,GAAG,EAAE,CAAC;IACZ,CAAC;IAAC,MAAM,CAAC;QACP,kDAAkD;IACpD,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"reportSavings.js","sourceRoot":"","sources":["../../../src/utils/reportSavings.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;;;;AA2CH,sCAQC;AAkBD,gCAMC;AAzED,4CAAoB;AACpB,gDAAwB;AACxB,4CAAoB;AACpB,gDAAwB;AACxB,2CAA6D;AAuB7D,SAAS,YAAY;IACnB,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,cAAI,CAAC,IAAI,CAAC,YAAE,CAAC,OAAO,EAAE,EAAE,yBAAyB,CAAC,CAAC;QACpE,IAAI,YAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC5B,OAAO,YAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;QAClD,CAAC;IACH,CAAC;IAAC,MAAM,CAAC,CAAA,CAAC;IACV,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;GAGG;AACH,SAAgB,aAAa,CAAC,OAAsB;IAClD,IAAI,CAAC,qBAAqB,EAAE,OAAO,CAAC,CAAC;IACrC,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC;QACrB,MAAM,MAAM,GAAG,QAAQ,IAAI,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC;QAC5H,mFAAmF;QACnF,gEAAgE;QAChE,IAAA,0BAAc,EAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IAC1D,CAAC;AACH,CAAC;AAaD;;;;GAIG;AACH,SAAgB,UAAU,CAAC,OAAoB;IAC7C,IAAI,CAAC,kBAAkB,EAAE,OAAO,CAAC,CAAC;IAClC,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC;QACrB,MAAM,MAAM,GAAG,OAAO,OAAO,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QACrH,IAAA,0BAAc,EAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IAC1D,CAAC;AACH,CAAC;AAED,SAAS,IAAI,CAAC,QAAgB,EAAE,OAAgB;IAC9C,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,YAAY,EAAE,CAAC;QAC5B,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;QAErC,MAAM,GAAG,GAAG,cAAI,CAAC,OAAO,CAAC;YACvB,QAAQ,EAAE,WAAW;YACrB,IAAI,EAAE,QAAQ,CAAC,IAAI,CAAC;YACpB,IAAI,EAAE,QAAQ;YACd,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,cAAc,EAAE,kBAAkB;gBAClC,gBAAgB,EAAE,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC;aAC1C;SACF,CAAC,CAAC;QAEH,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC,CAAC,yCAAyC;QACpE,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAChB,GAAG,CAAC,GAAG,EAAE,CAAC;IACZ,CAAC;IAAC,MAAM,CAAC;QACP,kDAAkD;IACpD,CAAC;AACH,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"lemma-proxy.d.ts","sourceRoot":"","sources":["../../../src/cli/lemma-proxy.ts"],"names":[],"mappings":";AA4cA,wBAAgB,+BAA+B,CAAC,OAAO,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CA4DvF;AA8lED,wBAAsB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CA6KhL"}
1
+ {"version":3,"file":"lemma-proxy.d.ts","sourceRoot":"","sources":["../../../src/cli/lemma-proxy.ts"],"names":[],"mappings":";AA4cA,wBAAgB,+BAA+B,CAAC,OAAO,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CA4DvF;AA+mED,wBAAsB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CA8KhL"}
@@ -916,6 +916,14 @@ async function relayMissStream(res, body, provider, tier) {
916
916
  return;
917
917
  }
918
918
  const hostname = provider === 'openai' ? 'api.openai.com' : 'api.anthropic.com';
919
+ try {
920
+ gatekeeper.assertEgressAllowed(hostname);
921
+ }
922
+ catch (err) {
923
+ res.write(`data: ${JSON.stringify({ error: err.message, code: err.code })}\n\ndata: [DONE]\n\n`);
924
+ res.end();
925
+ return;
926
+ }
919
927
  const upPath = provider === 'openai' ? '/v1/chat/completions' : '/v1/messages';
920
928
  const headers = provider === 'openai'
921
929
  ? { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' }
@@ -1914,8 +1922,13 @@ Adjusted Answer:`;
1914
1922
  if (provider === 'openai' || provider === 'anthropic') {
1915
1923
  if (req.body.messages && req.body.messages.length > 0) {
1916
1924
  injectSqueezedContent(req.body.messages[req.body.messages.length - 1], squeezedPrompt);
1917
- // Apply conversational history pruning
1918
- const pruneResult = pruneHistoryMessages(req.body.messages);
1925
+ // Apply conversational history pruning — off by default (T7 guard): pruning
1926
+ // invalidates the cached prompt prefix, which is usually a net loss. Set
1927
+ // LEMMA_FORCE_HISTORY_PRUNE=1 to opt back into the old unconditional behavior
1928
+ // (e.g. for a provider with no prompt caching).
1929
+ const pruneResult = pruneHistoryMessages(req.body.messages, {
1930
+ force: process.env.LEMMA_FORCE_HISTORY_PRUNE === '1',
1931
+ });
1919
1932
  req.body.messages = pruneResult.messages;
1920
1933
  historyTokensSaved = pruneResult.tokensSavedEstimate + Math.floor(compressionSavings / 4);
1921
1934
  if (pruneResult.tokensSavedEstimate > 0) {
@@ -1960,6 +1973,9 @@ Adjusted Answer:`;
1960
1973
  return res.json(semanticScrubber.unmask(data, tokenMap));
1961
1974
  }
1962
1975
  catch (err) {
1976
+ if (err.code === 'EGRESS_BLOCKED') {
1977
+ return res.status(403).json({ error: err.message, code: err.code, decision: err.decision });
1978
+ }
1963
1979
  return res.status(500).json({ error: err.message });
1964
1980
  }
1965
1981
  }
@@ -1976,6 +1992,7 @@ Adjusted Answer:`;
1976
1992
  stream: false // keep stream false for simulation simplicity
1977
1993
  };
1978
1994
  const baseURL = 'https://api.openai.com/v1/chat/completions';
1995
+ gatekeeper.assertEgressAllowed(baseURL);
1979
1996
  const resp = await axios.post(baseURL, payload, {
1980
1997
  headers: {
1981
1998
  Authorization: `Bearer ${apiKey}`,
@@ -2028,6 +2045,7 @@ Adjusted Answer:`;
2028
2045
  console.log(`\x1b[36m🔀 [Gateway Model Translation] Translating futuristic model ${body.model} to gpt-4o for key compatibility.\x1b[0m`);
2029
2046
  body.model = 'gpt-4o';
2030
2047
  }
2048
+ gatekeeper.assertEgressAllowed(baseURL);
2031
2049
  const resp = await axios.post(baseURL, body, { headers: { ...headers, 'Content-Type': 'application/json' } });
2032
2050
  return resp.data;
2033
2051
  }
@@ -2584,6 +2602,7 @@ Ensure you return strictly JSON. Do not return any other text outside the JSON b
2584
2602
  }
2585
2603
  else {
2586
2604
  // Use OpenAI/Anthropic
2605
+ gatekeeper.assertEgressAllowed('https://api.openai.com/v1/chat/completions');
2587
2606
  const resp = await axios.post('https://api.openai.com/v1/chat/completions', {
2588
2607
  model: 'gpt-4o',
2589
2608
  messages: [{ role: 'user', content: prompt }],