@nxuss/lemma 1.7.2 → 1.8.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 (47) hide show
  1. package/README.md +69 -8
  2. package/bin/brain-ingest.js +56 -0
  3. package/dist/cjs/mcp/tools.d.ts.map +1 -1
  4. package/dist/cjs/mcp/tools.js +132 -33
  5. package/dist/cjs/mcp/tools.js.map +1 -1
  6. package/dist/cjs/pr-review/bridge/BrainBridge.d.ts +11 -1
  7. package/dist/cjs/pr-review/bridge/BrainBridge.d.ts.map +1 -1
  8. package/dist/cjs/pr-review/bridge/BrainBridge.js +18 -23
  9. package/dist/cjs/pr-review/bridge/BrainBridge.js.map +1 -1
  10. package/dist/cjs/protocol/utils.d.ts.map +1 -1
  11. package/dist/cjs/protocol/utils.js +6 -5
  12. package/dist/cjs/protocol/utils.js.map +1 -1
  13. package/dist/cjs/subconscious/GitIngest.d.ts +48 -0
  14. package/dist/cjs/subconscious/GitIngest.d.ts.map +1 -0
  15. package/dist/cjs/subconscious/GitIngest.js +175 -0
  16. package/dist/cjs/subconscious/GitIngest.js.map +1 -0
  17. package/dist/cjs/subconscious/TheBrainV2.d.ts +23 -1
  18. package/dist/cjs/subconscious/TheBrainV2.d.ts.map +1 -1
  19. package/dist/cjs/subconscious/TheBrainV2.js +124 -17
  20. package/dist/cjs/subconscious/TheBrainV2.js.map +1 -1
  21. package/dist/cjs/utils/ConversationCheckpoint.d.ts +2 -0
  22. package/dist/cjs/utils/ConversationCheckpoint.d.ts.map +1 -1
  23. package/dist/cjs/utils/ConversationCheckpoint.js +38 -0
  24. package/dist/cjs/utils/ConversationCheckpoint.js.map +1 -1
  25. package/dist/esm/mcp/tools.d.ts.map +1 -1
  26. package/dist/esm/mcp/tools.js +134 -35
  27. package/dist/esm/mcp/tools.js.map +1 -1
  28. package/dist/esm/pr-review/bridge/BrainBridge.d.ts +11 -1
  29. package/dist/esm/pr-review/bridge/BrainBridge.d.ts.map +1 -1
  30. package/dist/esm/pr-review/bridge/BrainBridge.js +18 -20
  31. package/dist/esm/pr-review/bridge/BrainBridge.js.map +1 -1
  32. package/dist/esm/protocol/utils.d.ts.map +1 -1
  33. package/dist/esm/protocol/utils.js +6 -5
  34. package/dist/esm/protocol/utils.js.map +1 -1
  35. package/dist/esm/subconscious/GitIngest.d.ts +48 -0
  36. package/dist/esm/subconscious/GitIngest.d.ts.map +1 -0
  37. package/dist/esm/subconscious/GitIngest.js +166 -0
  38. package/dist/esm/subconscious/GitIngest.js.map +1 -0
  39. package/dist/esm/subconscious/TheBrainV2.d.ts +23 -1
  40. package/dist/esm/subconscious/TheBrainV2.d.ts.map +1 -1
  41. package/dist/esm/subconscious/TheBrainV2.js +124 -17
  42. package/dist/esm/subconscious/TheBrainV2.js.map +1 -1
  43. package/dist/esm/utils/ConversationCheckpoint.d.ts +2 -0
  44. package/dist/esm/utils/ConversationCheckpoint.d.ts.map +1 -1
  45. package/dist/esm/utils/ConversationCheckpoint.js +37 -0
  46. package/dist/esm/utils/ConversationCheckpoint.js.map +1 -1
  47. package/package.json +4 -2
package/README.md CHANGED
@@ -1,11 +1,11 @@
1
- # 🧠 Lemma v1.7.0
1
+ # 🧠 Lemma v1.8.0
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/@nxuss/lemma.svg?color=orange&style=flat-square)](https://www.npmjs.com/package/@nxuss/lemma)
4
4
  [![License](https://img.shields.io/npm/l/@nxuss/lemma.svg?color=blue&style=flat-square)](https://github.com/Nxusbets/lemma/blob/main/LICENSE)
5
- [![MCP Tools](https://img.shields.io/badge/MCP%20tools-94-ff69b4?style=flat-square)](https://github.com/Nxusbets/lemma)
5
+ [![MCP Tools](https://img.shields.io/badge/MCP%20tools-95-ff69b4?style=flat-square)](https://github.com/Nxusbets/lemma)
6
6
  [![Security](https://img.shields.io/badge/privacy-firewall%20active-blueviolet?style=flat-square)](https://github.com/Nxusbets/lemma)
7
7
  [![Caching](https://img.shields.io/badge/semantic%20memory-enabled-success?style=flat-square)](https://github.com/Nxusbets/lemma)
8
- [![Tests](https://img.shields.io/badge/tests-267%20passing-00c853?style=flat-square)](https://github.com/Nxusbets/lemma)
8
+ [![Tests](https://img.shields.io/badge/tests-293%20passing-00c853?style=flat-square)](https://github.com/Nxusbets/lemma)
9
9
 
10
10
  ```text
11
11
  __
@@ -17,9 +17,9 @@
17
17
 
18
18
  > **The Intelligent local-first AI Gateway — Privacy, Performance, Static Code Intelligence, and Infrastructure Command Center for the Agentic Era.**
19
19
 
20
- Lemma is a high-performance orchestration layer that sits between your development environment and LLM providers. It transforms the way you build with AI by providing **Shared Semantic Memory**, **Autonomous Cost Optimization**, **Privacy Guardrails**, **22 Infrastructure Management Tools**, and a suite of **94 MCP tools** — including zero-token static code intelligence that runs entirely on your machine with no LLM calls required.
20
+ Lemma is a high-performance orchestration layer that sits between your development environment and LLM providers. It transforms the way you build with AI by providing **Shared Semantic Memory**, **Autonomous Cost Optimization**, **Privacy Guardrails**, **22 Infrastructure Management Tools**, and a suite of **95 MCP tools** — including zero-token static code intelligence that runs entirely on your machine with no LLM calls required.
21
21
 
22
- Since v1.7.0 those 94 tools are all registered, but only the ones your project can actually use
22
+ Since v1.7.0 those 95 tools are all registered, but only the ones your project can actually use
23
23
  are advertised to the model each turn. The rest stay one `lemma_toolbox` call away. See
24
24
  [Tool profiles](#-tool-profiles).
25
25
 
@@ -27,7 +27,41 @@ are advertised to the model each turn. The rest stay one `lemma_toolbox` call aw
27
27
 
28
28
  ## 📋 Changelog
29
29
 
30
- ### v1.7.0 — The cost of being connected *(NEW)*
30
+ ### v1.8.0 — The Brain learns from project history, not just what it's told *(NEW)*
31
+
32
+ Every memory in The Brain used to depend on the assistant remembering to call `store_memory`. If
33
+ nobody called it, the Brain knew nothing about work that already happened.
34
+
35
+ | Change | What it does | Why it matters |
36
+ | :--- | :--- | :--- |
37
+ | **`lemma-brain-ingest`** *(new CLI)* | Feeds The Brain from git commits and `CHANGELOG.md` — no assistant call needed. Incremental via a marker file, safe to wire to `post-commit`. | The Brain stops depending on the model remembering to save anything. |
38
+ | **Checkpoints now feed The Brain** | `saveCheckpoint()` indexes any checkpoint with real content into the same Brain `search_memory` reads. | Decisions and bugs recorded in a checkpoint used to be invisible to search. |
39
+ | **`get_project_history`** *(new tool)* | Merges recent commits, checkpoints, and Brain memories for the current project into one call. | Answers "what have we done here" without three separate lookups. |
40
+ | **Outcome tagging** | `store_memory` accepts `outcome: 'failed'`; search surfaces it as a warning, not a suggestion. | A dead end is now as memorable as a fix. |
41
+ | **Symbol-level freshness** | `store_memory` accepts `symbols: [{filePath, symbolName}]` — freshness is judged by that function's own source, not the whole file. | An unrelated edit elsewhere in the file no longer stales the memory out. |
42
+
43
+ **PR review memory was silently going nowhere.** `BrainBridge` talked to the proxy's
44
+ `/api/search`/`/api/memory/store`, both backed by ChromaDB + a local Ollama server on `:11434` —
45
+ the exact stack TheBrainV2 replaced in 1.0.0. Whenever Ollama wasn't running (the common case
46
+ now), every call failed inside a `try/catch` and returned empty. It now reads/writes `TheBrainV2`
47
+ directly, so PR review and interactive coding share one memory instead of two disconnected ones.
48
+
49
+ **Two projects could quietly share — or split — memories.** `deriveProjectId` hashed the
50
+ project path without resolving symlinks; on macOS `process.cwd()` and an `os.tmpdir()`-built path
51
+ can report different strings for the same directory, hashing to two different project scopes. It
52
+ now resolves via `fs.realpathSync` first.
53
+
54
+ **Spanish text was mangled before it reached the index.** `tokenize()` used `\w` to strip
55
+ punctuation, which doesn't match accented letters — "Cómo" became "mo", "diseño" became "dise",
56
+ and the same word typed without its accent tokenized completely differently. `tokenize()` now
57
+ normalizes diacritics first, and gained an additive English/Spanish stemmer and Spanish stopwords.
58
+
59
+ **`token_budget` reported a multiplier no ledger backed.** It fetched a proxy route that doesn't
60
+ exist, silently fell back to an uncosted estimate, and reported a bigger number than the
61
+ session's own auditable ledger would support. It now reads the real `SavingsLedger` net position
62
+ (`/api/savings-breakdown`) or says plainly there's nothing to report if the proxy isn't running.
63
+
64
+ ### v1.7.0 — The cost of being connected
31
65
 
32
66
  Every tool schema Lemma advertises goes into the model's system prompt and is **re-sent on every
33
67
  request of the session** — including schemas for tools the project can never use. On this repo that
@@ -288,7 +322,7 @@ Or use environment variables: `JENKINS_URL`, `JENKINS_USERNAME`, `JENKINS_API_TO
288
322
  ## 🎚️ Tool profiles
289
323
 
290
324
  Every tool schema an MCP server advertises is injected into the model's system prompt and re-sent
291
- on **every request** of the session. Advertising all 94 tools costs ~11,600 tokens per turn — much
325
+ on **every request** of the session. Advertising all 95 tools costs ~11,600 tokens per turn — much
292
326
  of it schemas for tools a given project can never use.
293
327
 
294
328
  Lemma advertises a scoped surface instead. Nothing is removed: every tool stays registered and
@@ -349,9 +383,35 @@ the richer record when one exists.
349
383
 
350
384
  ---
351
385
 
386
+ ## 🧠 Feeding The Brain from project history
387
+
388
+ `store_memory` also depends on the assistant remembering to call it. `lemma-brain-ingest` pulls
389
+ memories from sources that already exist and don't need an assistant in the loop:
390
+
391
+ ```bash
392
+ lemma-brain-ingest [projectRoot] [--max N] [--changelog] [--no-commits]
393
+ ```
394
+
395
+ - Every git commit (message + files touched) becomes one Brain entry, so `search_memory` and
396
+ `get_telepathic_hints` can recall *why* a change happened, not just what the code looks like now.
397
+ - `--changelog` also ingests `CHANGELOG.md` (falls back to `docs/CHANGELOG.md`) — one entry per release, parsed
398
+ directly, no LLM involved.
399
+
400
+ A marker file under `<project>/.lemma/session/` tracks the last commit and changelog version
401
+ ingested, so this is safe to run after every commit and safe to run by hand for a one-time
402
+ backfill of existing history — both take the same incremental path and never double-store.
403
+ Wire it to a post-commit hook to keep it current automatically:
404
+
405
+ ```bash
406
+ echo 'lemma-brain-ingest "$(git rev-parse --show-toplevel)" &' >> .git/hooks/post-commit
407
+ chmod +x .git/hooks/post-commit
408
+ ```
409
+
410
+ ---
411
+
352
412
  ## 💎 Free vs Pro
353
413
 
354
- ### 🆓 Free (33 Tools)
414
+ ### 🆓 Free (34 Tools)
355
415
 
356
416
  These tools work **without a Pro license**. Activate them with just `lemma init`:
357
417
 
@@ -366,6 +426,7 @@ These tools work **without a Pro license**. Activate them with just `lemma init`
366
426
  | `auto_context_bundle` | Full context bundle per file |
367
427
  | `repeat_guard` | Duplicate output detector |
368
428
  | `conversation_checkpoint` | Session state save/load |
429
+ | `get_project_history` | Git log + checkpoints + Brain memories in one call |
369
430
  | `test_autofix_interceptor` | Test failure → Brain auto-fix |
370
431
  | `prompt_pattern_cache` | Reusable prompt templates |
371
432
  | `infra_discover` | Auto-detect Jenkins, OKD, Terraform |
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Feeds The Brain from sources the model never has to remember to store manually.
6
+ *
7
+ * Meant to be wired to a git post-commit hook so every commit becomes a searchable memory
8
+ * without depending on the assistant calling `store_memory`:
9
+ *
10
+ * echo 'lemma-brain-ingest "$(git rev-parse --show-toplevel)" &' >> .git/hooks/post-commit
11
+ *
12
+ * With no flags it's an incremental catch-up: a marker file under .lemma/session/ tracks the
13
+ * last commit ingested, so running this after every commit or by hand both take the same path
14
+ * and never double-store. `--max N` only matters the first time there is no marker yet.
15
+ *
16
+ * Usage: lemma-brain-ingest [projectRoot] [--max N] [--changelog] [--no-commits]
17
+ */
18
+
19
+ const path = require('path');
20
+
21
+ function main() {
22
+ const args = process.argv.slice(2);
23
+ const maxIndex = args.indexOf('--max');
24
+ const maxCommits = maxIndex >= 0 ? parseInt(args[maxIndex + 1], 10) : undefined;
25
+ const includeChangelog = args.includes('--changelog');
26
+ const includeCommits = !args.includes('--no-commits');
27
+ const positional = args.filter((a, i) => !a.startsWith('--') && i !== maxIndex + 1);
28
+ const projectRoot = path.resolve(positional[0] || process.cwd());
29
+
30
+ const distPath = path.join(__dirname, '..', 'dist', 'cjs', 'subconscious', 'GitIngest.js');
31
+ let ingestRecentCommits, ingestChangelog;
32
+ try {
33
+ ({ ingestRecentCommits, ingestChangelog } = require(distPath));
34
+ } catch {
35
+ console.error('[lemma-brain-ingest] Build not found. Run "npm run build" first.');
36
+ return;
37
+ }
38
+
39
+ if (includeCommits) {
40
+ const res = ingestRecentCommits(projectRoot, { maxCommits });
41
+ console.error(`[lemma-brain-ingest] commits: ${res.ingested} stored, ${res.skipped} skipped (up to ${res.lastSha ? res.lastSha.substring(0, 12) : 'none'})`);
42
+ }
43
+
44
+ if (includeChangelog) {
45
+ const res = ingestChangelog(projectRoot);
46
+ console.error(`[lemma-brain-ingest] changelog: ${res.ingested} stored, ${res.skipped} skipped (${res.versionsSeen} version(s) seen)`);
47
+ }
48
+ }
49
+
50
+ try {
51
+ main();
52
+ } catch (err) {
53
+ // A hook that fails must never be the reason a commit or session goes bad.
54
+ console.error('[lemma-brain-ingest] Skipped:', err && err.message);
55
+ }
56
+ process.exit(0);
@@ -1 +1 @@
1
- {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../../../src/mcp/tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAiiCnE,oFAAoF;AACpF,wBAAgB,kBAAkB,IAAI,MAAM,CAM3C;AAED,oEAAoE;AACpE,wBAAgB,sBAAsB,IAAI,MAAM,EAAE,CAEjD;AAED,iEAAiE;AACjE,wBAAgB,sBAAsB,IAAI,MAAM,EAAE,CAEjD;AAED,iFAAiF;AACjF,wBAAgB,qBAAqB,IAAI;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB,CAQA;AAoHD,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC1B,MAAM,EAAE,IAAI,GAAG,OAAO,GAAG,KAAK,GAAG,MAAM,CAAC;IACxC,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAqCD,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,MAAM,EACd,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,GAC1C,IAAI,CAwEN"}
1
+ {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../../../src/mcp/tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAwjCnE,oFAAoF;AACpF,wBAAgB,kBAAkB,IAAI,MAAM,CAM3C;AAED,oEAAoE;AACpE,wBAAgB,sBAAsB,IAAI,MAAM,EAAE,CAEjD;AAED,iEAAiE;AACjE,wBAAgB,sBAAsB,IAAI,MAAM,EAAE,CAEjD;AAED,iFAAiF;AACjF,wBAAgB,qBAAqB,IAAI;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB,CAQA;AAqHD,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC1B,MAAM,EAAE,IAAI,GAAG,OAAO,GAAG,KAAK,GAAG,MAAM,CAAC;IACxC,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAqCD,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,MAAM,EACd,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,GAC1C,IAAI,CAwEN"}
@@ -101,7 +101,7 @@ function isProUser() {
101
101
  }
102
102
  const FREE_TOOLS = new Set([
103
103
  // Cache & Memory — the hook that shows instant value
104
- "smarter_cache", "state_hash_cache", "token_receipt", "search_memory", "store_memory",
104
+ "smarter_cache", "state_hash_cache", "token_receipt", "search_memory", "store_memory", "get_project_history",
105
105
  // Token optimization — shows what they're saving
106
106
  "token_budget", "squeeze_prompt", "turbosqueeze",
107
107
  // Utility — just enough to function
@@ -157,7 +157,7 @@ const toolDefinitions = [
157
157
  },
158
158
  {
159
159
  name: "store_memory",
160
- description: "Persist a technical solution, bug fix, architecture decision, or key fact into Lemma's Brain — so future questions on the same topic (even phrased differently) don't require re-investigating the repo. Pass filePaths for anything derived from specific files (e.g. 'where is X implemented', 'how does Y work') so the memory auto-invalidates the moment those files change, instead of risking a stale answer being reused.",
160
+ description: "Persist a technical solution, bug fix, architecture decision, or key fact into Lemma's Brain — so future questions on the same topic (even phrased differently) don't require re-investigating the repo. Pass filePaths for anything derived from specific files (e.g. 'where is X implemented', 'how does Y work') so the memory auto-invalidates the moment those files change, instead of risking a stale answer being reused. If the answer is really about one function/class rather than the whole file, pass `symbols` instead (or in addition) so an unrelated edit elsewhere in that file doesn't stale it out. Pass outcome='failed' for an approach that was tried and did NOT work — that's just as worth remembering as a fix, so the Brain can warn 'already tried, didn't work' instead of only ever suggesting reuse.",
161
161
  inputSchema: {
162
162
  type: "object",
163
163
  properties: {
@@ -165,6 +165,19 @@ const toolDefinitions = [
165
165
  response: { type: "string", description: "The complete technical content to memorize" },
166
166
  provider: { type: "string", description: "Optional model provider name", default: "generic" },
167
167
  filePaths: { type: "array", items: { type: "string" }, description: "Paths (relative to project root) this answer depends on. If any changes later, this memory is marked stale instead of being silently reused." },
168
+ symbols: {
169
+ type: "array",
170
+ items: {
171
+ type: "object",
172
+ properties: {
173
+ filePath: { type: "string", description: "Path (relative to project root) containing the symbol" },
174
+ symbolName: { type: "string", description: "Function/class/interface/type/enum/const name" },
175
+ },
176
+ required: ["filePath", "symbolName"],
177
+ },
178
+ description: "Symbols (not whole files) this answer depends on. Freshness is then judged by that symbol's own source, so edits elsewhere in the same file don't stale this memory out.",
179
+ },
180
+ outcome: { type: "string", enum: ["confirmed", "failed"], description: "Default 'confirmed' (a verified working solution). Set 'failed' to record a dead end — an approach ruled out, not one to recommend." },
168
181
  },
169
182
  required: ["query", "response"],
170
183
  },
@@ -316,6 +329,16 @@ const toolDefinitions = [
316
329
  properties: {},
317
330
  },
318
331
  },
332
+ {
333
+ name: "get_project_history",
334
+ description: "Answers 'what have we done in this project': merges recent git commits, session checkpoints, and The Brain's memories (decisions, fixes, prior PR reviews) for the current project into one narrative. Use this instead of piecing the same picture together from search_memory + git log + reading checkpoint files separately.",
335
+ inputSchema: {
336
+ type: "object",
337
+ properties: {
338
+ limit: { type: "number", description: "Max items per section (commits, checkpoints, Brain memories)", default: 10 },
339
+ },
340
+ },
341
+ },
319
342
  {
320
343
  name: "get_ast_hologram",
321
344
  description: "Generate a dense, token-efficient Holographic AST Map of the workspace using the TypeScript compiler. Returns structured JSON of all exported symbols with file paths and line numbers.",
@@ -1164,6 +1187,7 @@ const toolHandlers = {
1164
1187
  auto_context_bundle: handleAutoContextBundle,
1165
1188
  repeat_guard: handleRepeatGuard,
1166
1189
  conversation_checkpoint: handleConversationCheckpoint,
1190
+ get_project_history: handleGetProjectHistory,
1167
1191
  test_autofix_interceptor: handleTestAutofixInterceptor,
1168
1192
  prompt_pattern_cache: handlePromptPatternCache,
1169
1193
  lemma_toolbox: handleToolbox,
@@ -1349,7 +1373,7 @@ async function handleSearchMemory(args) {
1349
1373
  }
1350
1374
  const fresh = results.filter((r) => r.fresh);
1351
1375
  const stale = results.filter((r) => !r.fresh);
1352
- const formatResult = (r, i) => `Result ${i + 1} (Similarity: ${(r.similarity * 100).toFixed(1)}%)\nPrompt: ${r.query.substring(0, 300)}...\nResponse: ${typeof r.response === "string" ? r.response : JSON.stringify(r.response, null, 2)}`;
1376
+ const formatResult = (r, i) => `Result ${i + 1} (Similarity: ${(r.similarity * 100).toFixed(1)}%)${r.outcome === "failed" ? "\n⚠️ TAGGED AS A FAILED ATTEMPT — this was tried before and did NOT work. Do not repeat it; treat this as a warning, not a suggestion." : ""}\nPrompt: ${r.query.substring(0, 300)}...\nResponse: ${typeof r.response === "string" ? r.response : JSON.stringify(r.response, null, 2)}`;
1353
1377
  if (fresh.length > 0) {
1354
1378
  // Only a fresh hit (or an untracked, purely conceptual entry) counts as a real
1355
1379
  // avoided re-investigation — this is the only case worth crediting as savings.
@@ -1390,11 +1414,15 @@ async function handleStoreMemory(args) {
1390
1414
  const responseText = args?.response;
1391
1415
  const provider = args?.provider || "generic";
1392
1416
  const filePaths = Array.isArray(args?.filePaths) ? args.filePaths : undefined;
1417
+ const outcome = args?.outcome === "failed" ? "failed" : undefined;
1418
+ const symbols = Array.isArray(args?.symbols)
1419
+ ? args.symbols.filter((s) => s?.filePath && s?.symbolName)
1420
+ : undefined;
1393
1421
  if (!query || !responseText)
1394
1422
  throw new Error("Query and response are required");
1395
1423
  try {
1396
1424
  const brain = (0, TheBrainV2_1.getBrain)();
1397
- const storeRes = brain.store(query, responseText, provider, 0.92, filePaths);
1425
+ const storeRes = brain.store(query, responseText, provider, 0.92, filePaths, undefined, outcome, symbols);
1398
1426
  // Storing a memory saves nothing — it only creates the chance of a saving later.
1399
1427
  // Crediting tokens here inflated the ledger on write and then credited the same
1400
1428
  // answer again on every read. The saving is booked by search_memory on a fresh hit.
@@ -1404,7 +1432,9 @@ async function handleStoreMemory(args) {
1404
1432
  note: "memory written — no tokens saved yet",
1405
1433
  });
1406
1434
  const trackingNote = filePaths && filePaths.length > 0 ? ` Tracking freshness against ${filePaths.length} file(s) — this memory auto-invalidates if they change.` : "";
1407
- return { content: [{ type: "text", text: `Success: Memory stored. ${storeRes.reason}${trackingNote}` }] };
1435
+ const symbolNote = symbols && symbols.length > 0 ? ` Tracking freshness against ${symbols.length} symbol(s) specifically — unrelated edits elsewhere in those files won't stale this out.` : "";
1436
+ const outcomeNote = outcome === "failed" ? " Tagged as a FAILED attempt — future searches will surface it as a warning, not a suggestion." : "";
1437
+ return { content: [{ type: "text", text: `Success: Memory stored. ${storeRes.reason}${trackingNote}${symbolNote}${outcomeNote}` }] };
1408
1438
  }
1409
1439
  catch (e) {
1410
1440
  (0, utils_1.logError)("store_memory", e);
@@ -1923,6 +1953,59 @@ async function handleGetProjectOnboarding(_args) {
1923
1953
  ],
1924
1954
  };
1925
1955
  }
1956
+ // ── Project History (aggregate: git log + checkpoints + Brain) ─────
1957
+ /**
1958
+ * "What have we done in this project" today means piecing together search_memory, git log,
1959
+ * and manually reading .lemma/session/ checkpoint files by hand — three separate calls whose
1960
+ * results the model has to merge itself. This does that merge in one call.
1961
+ */
1962
+ async function handleGetProjectHistory(args) {
1963
+ const cwd = process.cwd();
1964
+ const limit = typeof args?.limit === "number" ? args.limit : 10;
1965
+ try {
1966
+ const projectId = (0, TheBrainV2_1.deriveProjectId)(cwd);
1967
+ const brainEntries = (0, TheBrainV2_1.getBrain)()
1968
+ .getEntriesForProject(projectId)
1969
+ .sort((a, b) => (b.hits - a.hits) || (new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()))
1970
+ .slice(0, limit);
1971
+ const checkpoints = (0, ConversationCheckpoint_1.loadRecentCheckpoints)(cwd, Math.min(limit, 5));
1972
+ let recentCommits = [];
1973
+ try {
1974
+ const log = (0, child_process_1.execFileSync)("git", ["-C", cwd, "log", `-${limit}`, "--pretty=format:%h %s (%ar)"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
1975
+ recentCommits = log.split("\n").filter(Boolean);
1976
+ }
1977
+ catch {
1978
+ // Not a git repo, or no commits yet — sections below just won't include this one.
1979
+ }
1980
+ const parts = [`# Project history — ${path_1.default.basename(cwd)}`];
1981
+ if (recentCommits.length > 0) {
1982
+ parts.push(`## Recent commits\n${recentCommits.map((c) => `- ${c}`).join("\n")}`);
1983
+ }
1984
+ if (checkpoints.length > 0) {
1985
+ const checkpointLines = checkpoints.map((cp) => {
1986
+ const date = new Date(cp.timestamp).toISOString().replace("T", " ").slice(0, 16);
1987
+ return `- **${date}** — ${cp.summary.replace(/\n+/g, " ")}`;
1988
+ });
1989
+ parts.push(`## Session checkpoints\n${checkpointLines.join("\n")}`);
1990
+ }
1991
+ if (brainEntries.length > 0) {
1992
+ const brainLines = brainEntries.map((e) => {
1993
+ const tag = e.outcome === "failed" ? " ⚠️ FAILED ATTEMPT" : "";
1994
+ const snippet = e.response.length > 300 ? `${e.response.substring(0, 300)}...` : e.response;
1995
+ return `- **${e.query}**${tag} _(${e.provider}, ${e.hits} hit(s))_\n ${snippet}`;
1996
+ });
1997
+ parts.push(`## Brain memories (decisions, fixes, prior reviews)\n${brainLines.join("\n")}`);
1998
+ }
1999
+ if (parts.length === 1) {
2000
+ parts.push("Nothing recorded yet — no git history, checkpoints, or Brain memories found for this project.");
2001
+ }
2002
+ return { content: [{ type: "text", text: parts.join("\n\n") }] };
2003
+ }
2004
+ catch (e) {
2005
+ (0, utils_1.logError)("get_project_history", e);
2006
+ return { content: [{ type: "text", text: `get_project_history failed: ${e.message}` }] };
2007
+ }
2008
+ }
1926
2009
  function extractSymbolsWithTsCompiler(filePath, relPath, parseErrors) {
1927
2010
  try {
1928
2011
  const src = fs_1.default.readFileSync(filePath, "utf8");
@@ -2243,7 +2326,7 @@ async function handleGetTelepathicHints(args) {
2243
2326
  const similarity = (r.similarity * 100).toFixed(1);
2244
2327
  const prompt = r.query || "Unknown";
2245
2328
  const responseContent = r.response || "";
2246
- hintsText += `### Hint ${i + 1} (${similarity}% match)\n`;
2329
+ hintsText += `### Hint ${i + 1} (${similarity}% match)${r.outcome === "failed" ? " — ⚠️ FAILED ATTEMPT, do not repeat" : ""}\n`;
2247
2330
  hintsText += `**Memory:** ${prompt}\n\n`;
2248
2331
  hintsText += `${responseContent.substring(0, 400)}${responseContent.length > 400 ? "..." : ""}\n\n---\n\n`;
2249
2332
  });
@@ -2592,48 +2675,64 @@ async function handleTokenReceipt(args) {
2592
2675
  }
2593
2676
  // ── Token Budget ─────────────────────────────────────────────────
2594
2677
  async function handleTokenBudget(_args) {
2595
- const statsFile = path_1.default.join(os_1.default.homedir(), '.lemma-cache/stats.json');
2596
2678
  const port = (0, utils_1.getProxyPort)();
2597
- let stats = {};
2679
+ const schemaTokens = Math.round(getToolSchemaChars() / 4);
2680
+ // /api/savings-breakdown serves SavingsLedger.getSnapshot() — the same ledger that
2681
+ // already nets tokensSaved against what Lemma itself spent (MCP schema injection,
2682
+ // tool-result bytes, cache misses). There is no separate "membership multiplier" to
2683
+ // compute: reusing that one number keeps this tool from reporting a bigger win than
2684
+ // the ledger the user can audit via `token_receipt` actually shows.
2685
+ let snapshot = null;
2598
2686
  try {
2599
- stats = JSON.parse(fs_1.default.readFileSync(statsFile, 'utf8'));
2687
+ const resp = await axios_1.default.get(`http://localhost:${port}/api/savings-breakdown`);
2688
+ snapshot = resp.data;
2600
2689
  }
2601
2690
  catch { }
2602
- let ledger = [];
2603
- try {
2604
- const resp = await axios_1.default.get(`http://localhost:${port}/api/ledger`);
2605
- ledger = resp.data.events || [];
2691
+ if (!snapshot) {
2692
+ return {
2693
+ content: [{
2694
+ type: "text",
2695
+ text: JSON.stringify({
2696
+ note: "El proxy de Lemma no está corriendo, así que no hay bitácora de ahorro/costo que leer — no se inventa un número aquí. Arranca el proxy para que este reporte sea real.",
2697
+ perTurnOverhead: { ...getToolSurfaceSummary(), schemaTokens },
2698
+ }, null, 2),
2699
+ }],
2700
+ };
2606
2701
  }
2607
- catch { }
2608
- const totalTokensSaved = stats.totalTokens || ledger.reduce((sum, e) => sum + (e.tokensImpact || 0), 0);
2609
- const monthlyTokens = stats.monthlyTokens || stats.totalTokens || 100000;
2610
- const multiplier = monthlyTokens > 0 ? ((monthlyTokens + totalTokensSaved) / monthlyTokens).toFixed(1) : "N/A";
2611
- const toolCallCount = ledger.length || stats.toolCalls || 0;
2702
+ const { total, totalCost, net } = snapshot;
2703
+ const ratioStr = net.ratio === Infinity ? "n/a (sin costo registrado todavía)" : `${net.ratio.toFixed(2)}x`;
2612
2704
  return {
2613
2705
  content: [{
2614
2706
  type: "text",
2615
2707
  text: JSON.stringify({
2616
- membershipMultiplier: `${multiplier}x`,
2617
- totalTokensSaved: totalTokensSaved.toLocaleString(),
2618
- estimatedTokensPerMonth: monthlyTokens.toLocaleString(),
2619
- effectiveBudget: (monthlyTokens + totalTokensSaved).toLocaleString(),
2620
- toolCallsOptimized: toolCallCount,
2621
- activeStrategies: {
2622
- turboSqueeze: true,
2623
- contextCompression: true,
2624
- semanticCaching: true,
2625
- historyPruning: true,
2626
- astNavigation: true,
2627
- privacyScrubbing: true,
2708
+ netRatio: ratioStr,
2709
+ note: "netRatio = tokensSaved / tokensSpent, ambos de la misma bitácora auditable que expone token_receipt. Por debajo de 1.0x, Lemma está costando más de lo que ahorra en esta sesión.",
2710
+ tokensSaved: total.tokensSaved.toLocaleString(),
2711
+ tokensSpent: totalCost.tokensSpent.toLocaleString(),
2712
+ netTokens: net.netTokens.toLocaleString(),
2713
+ netCostUSD: net.netCost.toFixed(4),
2714
+ breakdown: {
2715
+ saved: {
2716
+ cache: snapshot.cache?.tokensSaved ?? 0,
2717
+ contextSqueeze: snapshot.contextSqueeze?.tokensSaved ?? 0,
2718
+ historyPrune: snapshot.historyPrune?.tokensSaved ?? 0,
2719
+ complexityRouting: snapshot.complexityRouting?.tokensSaved ?? 0,
2720
+ clipboard: snapshot.clipboard?.tokensSaved ?? 0,
2721
+ },
2722
+ spent: {
2723
+ mcpInstructions: snapshot.costs?.mcpInstructions?.tokensSpent ?? 0,
2724
+ toolResult: snapshot.costs?.toolResult?.tokensSpent ?? 0,
2725
+ cacheMiss: snapshot.costs?.cacheMiss?.tokensSpent ?? 0,
2726
+ },
2628
2727
  },
2629
2728
  // The fixed cost of being connected, re-sent on every request of the session.
2630
2729
  perTurnOverhead: {
2631
2730
  ...getToolSurfaceSummary(),
2632
- schemaTokens: Math.round(getToolSchemaChars() / 4),
2731
+ schemaTokens,
2633
2732
  note: "Hidden tools stay callable via lemma_toolbox. Set mcp.toolProfile='full' in lemma.config.json to advertise all of them.",
2634
2733
  },
2635
- recommendations: toolCallCount === 0
2636
- ? ["No se han usado tools de Lemma todavía. Asegúrate de que el agente esté usando el prompt lemma-turbomode."]
2734
+ recommendations: net.ratio !== Infinity && net.ratio < 1
2735
+ ? ["netRatio < 1.0x: revisa si search_memory/smarter_cache se están llamando antes de razonar, o si el toolProfile anuncia más tools de las que este proyecto necesita cada schema extra es costo fijo por turno."]
2637
2736
  : ["Sigue usando turbosqueeze antes de cada code block grande.", "Usa smarter_cache antes de razonar."],
2638
2737
  }, null, 2),
2639
2738
  }],