tokenmizer 0.2.4__tar.gz → 0.2.6__tar.gz

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 (110) hide show
  1. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/CHANGELOG.md +30 -0
  2. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/PKG-INFO +11 -12
  3. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/README.md +10 -11
  4. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/pyproject.toml +3 -4
  5. tokenmizer-0.2.6/scripts/mcp_e2e_check.py +158 -0
  6. tokenmizer-0.2.6/server.json +29 -0
  7. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tests/unit/test_graph_persistence.py +84 -0
  8. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/__init__.py +1 -1
  9. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/api/app.py +1 -1
  10. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/graph_memory/graph.py +26 -0
  11. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/mcp/server.py +12 -2
  12. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/.claude-plugin/marketplace.json +0 -0
  13. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/.claude-plugin/plugin.json +0 -0
  14. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/.claude-plugin/skills/analyze/SKILL.md +0 -0
  15. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/.claude-plugin/skills/checkpoint/SKILL.md +0 -0
  16. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/.claude-plugin/skills/resume/SKILL.md +0 -0
  17. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/.claude-plugin/skills/stats/SKILL.md +0 -0
  18. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  19. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/.github/ISSUE_TEMPLATE/extraction_miss.md +0 -0
  20. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  21. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/.github/workflows/ci.yml +0 -0
  22. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/.github/workflows/release.yml +0 -0
  23. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/.gitignore +0 -0
  24. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/.mcp.json +0 -0
  25. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/CONTRIBUTING.md +0 -0
  26. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/Dockerfile +0 -0
  27. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/LICENSE +0 -0
  28. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/SECURITY.md +0 -0
  29. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/TESTING.md +0 -0
  30. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/USAGE.md +0 -0
  31. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/benchmarks/__init__.py +0 -0
  32. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/benchmarks/checkpoint_accuracy/__init__.py +0 -0
  33. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/benchmarks/checkpoint_accuracy/runner.py +0 -0
  34. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/benchmarks/checkpoint_accuracy/runner_v2.py +0 -0
  35. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/benchmarks/checkpoint_accuracy/runner_v3.py +0 -0
  36. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/benchmarks/graph_retrieval/__init__.py +0 -0
  37. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/benchmarks/graph_retrieval/runner.py +0 -0
  38. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/benchmarks/latency/__init__.py +0 -0
  39. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/benchmarks/latency/runner.py +0 -0
  40. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/benchmarks/resume_quality/__init__.py +0 -0
  41. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/docker-compose.yml +0 -0
  42. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/docs/assets/architecture.svg +0 -0
  43. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/docs/assets/logo.svg +0 -0
  44. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/examples/basic_usage.py +0 -0
  45. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/scripts/install.sh +0 -0
  46. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/scripts/run_stdlib_tests.py +0 -0
  47. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/scripts/setup.sh +0 -0
  48. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/scripts/static_audit.py +0 -0
  49. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tests/__init__.py +0 -0
  50. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tests/chaos/__init__.py +0 -0
  51. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tests/chaos/test_recovery.py +0 -0
  52. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tests/conftest.py +0 -0
  53. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tests/integration/__init__.py +0 -0
  54. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tests/integration/test_api_endpoint.py +0 -0
  55. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tests/integration/test_checkpoint.py +0 -0
  56. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tests/memory_accuracy/__init__.py +0 -0
  57. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tests/memory_accuracy/test_retention.py +0 -0
  58. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tests/unit/__init__.py +0 -0
  59. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tests/unit/test_cache.py +0 -0
  60. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tests/unit/test_compression_correctness.py +0 -0
  61. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tests/unit/test_decision_cache_async.py +0 -0
  62. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tests/unit/test_file_intelligence.py +0 -0
  63. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tests/unit/test_graph.py +0 -0
  64. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tests/unit/test_hybrid_extractor.py +0 -0
  65. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tests/unit/test_rate_limiter.py +0 -0
  66. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tests/unit/test_security.py +0 -0
  67. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tests/unit/test_tokenizer.py +0 -0
  68. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tests/unit/test_validator.py +0 -0
  69. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/agents/__init__.py +0 -0
  70. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/analytics/__init__.py +0 -0
  71. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/analytics/engine.py +0 -0
  72. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/api/__init__.py +0 -0
  73. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/api/rate_limiter.py +0 -0
  74. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/checkpoints/__init__.py +0 -0
  75. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/checkpoints/manager.py +0 -0
  76. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/cli.py +0 -0
  77. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/compression/__init__.py +0 -0
  78. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/compression/engine.py +0 -0
  79. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/compression/output_trimmer.py +0 -0
  80. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/compression/window.py +0 -0
  81. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/config/__init__.py +0 -0
  82. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/config/settings.py +0 -0
  83. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/core/__init__.py +0 -0
  84. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/core/dto.py +0 -0
  85. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/core/errors.py +0 -0
  86. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/core/tokenizer.py +0 -0
  87. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/dashboard/__init__.py +0 -0
  88. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/dashboard/page.py +0 -0
  89. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/filters/__init__.py +0 -0
  90. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/filters/file_intelligence.py +0 -0
  91. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/graph_memory/__init__.py +0 -0
  92. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/graph_memory/decision_tracker.py +0 -0
  93. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/graph_memory/helpers.py +0 -0
  94. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/graph_memory/hybrid_extractor.py +0 -0
  95. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/graph_memory/types.py +0 -0
  96. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/graph_memory/validator.py +0 -0
  97. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/graph_memory/visualization.py +0 -0
  98. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/mcp/__init__.py +0 -0
  99. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/providers/__init__.py +0 -0
  100. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/providers/providers.py +0 -0
  101. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/security/__init__.py +0 -0
  102. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/security/auth.py +0 -0
  103. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/security/middleware.py +0 -0
  104. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/security/redaction.py +0 -0
  105. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/semantic_cache/__init__.py +0 -0
  106. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/semantic_cache/cache.py +0 -0
  107. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/state/__init__.py +0 -0
  108. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/state/backend.py +0 -0
  109. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/storage/__init__.py +0 -0
  110. {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer.yaml +0 -0
@@ -1,5 +1,35 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.6] — 2026-07-02 — MCP registry readiness
4
+
5
+ - NEW console script `tokenmizer-mcp` — runs the MCP stdio server directly
6
+ (no `python3 -m tokenmizer.mcp.server` incantation needed in client configs).
7
+ - README carries the `mcp-name` ownership marker required by the official
8
+ MCP registry's PyPI package validation.
9
+
10
+ ## [0.2.5] — 2026-07-02 — restart no longer breaks checkpoints
11
+
12
+ ### Critical
13
+ - **`graph_memory/graph.py`:** `_load()` rebuilt nodes/edges from SQLite
14
+ without converting `type`/`status` strings back to their str-Enum types.
15
+ Str-Enum equality kept passing (which hid the bug from every unit test),
16
+ but any `.value` access crashed — concretely, **after a server restart,
17
+ checkpointing any reloaded session returned HTTP 500**, and resume paths
18
+ touching `.value` were equally broken. Enums are now restored on load;
19
+ nodes/edges with unrecognized types (forward-compat) are skipped with a
20
+ warning instead of killing the whole load. Found by the new MCP e2e
21
+ check, not unit tests — reloaded graphs were never checkpoint-ed in tests
22
+ before. Four regression tests added.
23
+
24
+ ### MCP server
25
+ - `serverInfo.version` no longer hardcoded (was stale "0.2.3").
26
+ - `resume_session` no longer claims "No checkpoint found" when a checkpoint
27
+ exists but its graph was empty — the two cases now produce distinct,
28
+ accurate messages.
29
+ - NEW `scripts/mcp_e2e_check.py`: boots the real proxy in-process, spawns
30
+ the real MCP stdio server, and exercises handshake + all 5 tools
31
+ end-to-end. This is the check that caught the restart bug.
32
+
3
33
  ## [0.2.4] — 2026-07-02 — launch-readiness fixes
4
34
 
5
35
  ### Critical — the endpoint did not work
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tokenmizer
3
- Version: 0.2.4
3
+ Version: 0.2.6
4
4
  Summary: Reduce AI context loss by 2x. Graph-backed checkpoint and resume for any LLM session.
5
5
  Project-URL: Homepage, https://github.com/Shweta-Mishra-ai/tokenmizer
6
6
  Project-URL: Repository, https://github.com/Shweta-Mishra-ai/tokenmizer
@@ -86,6 +86,8 @@ Description-Content-Type: text/markdown
86
86
  </p>
87
87
 
88
88
  <p>
89
+ <a href="https://pypi.org/project/tokenmizer"><img src="https://img.shields.io/pypi/v/tokenmizer?color=7c6af7&style=flat-square" alt="PyPI"/></a>
90
+ <a href="https://pypi.org/project/tokenmizer"><img src="https://img.shields.io/pypi/pyversions/tokenmizer?color=5ee7c8&style=flat-square"/></a>
89
91
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-4ade80?style=flat-square"/></a>
90
92
  <a href="https://github.com/Shweta-Mishra-ai/tokenmizer/actions"><img src="https://img.shields.io/github/actions/workflow/status/Shweta-Mishra-ai/tokenmizer/ci.yml?branch=main&style=flat-square&color=4ade80"/></a>
91
93
  <img src="https://img.shields.io/badge/Claude%20Code-Plugin-7c6af7?style=flat-square&logo=anthropic"/>
@@ -146,22 +148,16 @@ History is **never deleted**. "Why did we switch from React to Next.js?" — alw
146
148
 
147
149
  ### 1. Install
148
150
 
149
- > Not yet published to PyPI — install from source (PyPI release is on the
150
- > roadmap; the `pip install tokenmizer` short form will work after that).
151
-
152
151
  ```bash
153
- git clone https://github.com/Shweta-Mishra-ai/tokenmizer
154
- cd tokenmizer
155
-
156
152
  # Recommended
157
- pip install -e ".[anthropic,cache]"
153
+ pip install "tokenmizer[anthropic,cache]"
158
154
 
159
155
  # All providers
160
- pip install -e ".[anthropic,openai,gemini,cohere,cache]"
156
+ pip install "tokenmizer[anthropic,openai,gemini,cohere,cache]"
161
157
 
162
158
  # No key? Use Ollama (free, local)
163
159
  brew install ollama && ollama pull llama3
164
- pip install -e .
160
+ pip install tokenmizer
165
161
  ```
166
162
 
167
163
  ### 2. Set your API key
@@ -231,20 +227,23 @@ Then use skills directly:
231
227
 
232
228
  ### Option B — MCP server
233
229
 
230
+ mcp-name: io.github.Shweta-Mishra-ai/tokenmizer
231
+
234
232
  Add to `~/.claude/settings.json`:
235
233
 
236
234
  ```json
237
235
  {
238
236
  "mcpServers": {
239
237
  "tokenmizer": {
240
- "command": "python3",
241
- "args": ["-m", "tokenmizer.mcp.server"],
238
+ "command": "tokenmizer-mcp",
242
239
  "env": { "TOKENMIZER_URL": "http://localhost:8000" }
243
240
  }
244
241
  }
245
242
  }
246
243
  ```
247
244
 
245
+ (`tokenmizer-mcp` is installed with the package; `python3 -m tokenmizer.mcp.server` also works.)
246
+
248
247
  ---
249
248
 
250
249
  ## Other Tools
@@ -11,6 +11,8 @@
11
11
  </p>
12
12
 
13
13
  <p>
14
+ <a href="https://pypi.org/project/tokenmizer"><img src="https://img.shields.io/pypi/v/tokenmizer?color=7c6af7&style=flat-square" alt="PyPI"/></a>
15
+ <a href="https://pypi.org/project/tokenmizer"><img src="https://img.shields.io/pypi/pyversions/tokenmizer?color=5ee7c8&style=flat-square"/></a>
14
16
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-4ade80?style=flat-square"/></a>
15
17
  <a href="https://github.com/Shweta-Mishra-ai/tokenmizer/actions"><img src="https://img.shields.io/github/actions/workflow/status/Shweta-Mishra-ai/tokenmizer/ci.yml?branch=main&style=flat-square&color=4ade80"/></a>
16
18
  <img src="https://img.shields.io/badge/Claude%20Code-Plugin-7c6af7?style=flat-square&logo=anthropic"/>
@@ -71,22 +73,16 @@ History is **never deleted**. "Why did we switch from React to Next.js?" — alw
71
73
 
72
74
  ### 1. Install
73
75
 
74
- > Not yet published to PyPI — install from source (PyPI release is on the
75
- > roadmap; the `pip install tokenmizer` short form will work after that).
76
-
77
76
  ```bash
78
- git clone https://github.com/Shweta-Mishra-ai/tokenmizer
79
- cd tokenmizer
80
-
81
77
  # Recommended
82
- pip install -e ".[anthropic,cache]"
78
+ pip install "tokenmizer[anthropic,cache]"
83
79
 
84
80
  # All providers
85
- pip install -e ".[anthropic,openai,gemini,cohere,cache]"
81
+ pip install "tokenmizer[anthropic,openai,gemini,cohere,cache]"
86
82
 
87
83
  # No key? Use Ollama (free, local)
88
84
  brew install ollama && ollama pull llama3
89
- pip install -e .
85
+ pip install tokenmizer
90
86
  ```
91
87
 
92
88
  ### 2. Set your API key
@@ -156,20 +152,23 @@ Then use skills directly:
156
152
 
157
153
  ### Option B — MCP server
158
154
 
155
+ mcp-name: io.github.Shweta-Mishra-ai/tokenmizer
156
+
159
157
  Add to `~/.claude/settings.json`:
160
158
 
161
159
  ```json
162
160
  {
163
161
  "mcpServers": {
164
162
  "tokenmizer": {
165
- "command": "python3",
166
- "args": ["-m", "tokenmizer.mcp.server"],
163
+ "command": "tokenmizer-mcp",
167
164
  "env": { "TOKENMIZER_URL": "http://localhost:8000" }
168
165
  }
169
166
  }
170
167
  }
171
168
  ```
172
169
 
170
+ (`tokenmizer-mcp` is installed with the package; `python3 -m tokenmizer.mcp.server` also works.)
171
+
173
172
  ---
174
173
 
175
174
  ## Other Tools
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "tokenmizer"
7
- version = "0.2.4"
7
+ version = "0.2.6"
8
8
  description = "Reduce AI context loss by 2x. Graph-backed checkpoint and resume for any LLM session."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -73,6 +73,7 @@ all = [
73
73
 
74
74
  [project.scripts]
75
75
  tokenmizer = "tokenmizer.cli:app"
76
+ tokenmizer-mcp = "tokenmizer.mcp.server:run_stdio_server"
76
77
 
77
78
  [project.urls]
78
79
  Homepage = "https://github.com/Shweta-Mishra-ai/tokenmizer"
@@ -108,6 +109,4 @@ omit = [
108
109
  [tool.coverage.report]
109
110
  # Honest floor over ALL product code (previously 70, but with api/app.py,
110
111
  # auth.py, providers and other core modules excluded — coverage theater).
111
- # Single source of truth: CI must not override this number.
112
- fail_under = 50
113
- show_missing = true
112
+ # Single source of truth: CI m
@@ -0,0 +1,158 @@
1
+ """
2
+ MCP server end-to-end check.
3
+
4
+ Spawns the real MCP stdio server as a subprocess, performs the MCP
5
+ handshake (initialize → initialized → tools/list), then exercises real
6
+ tool calls against a running TokenMizer proxy (TOKENMIZER_URL) and a
7
+ local file analysis (no proxy needed).
8
+
9
+ Run: python scripts/mcp_e2e_check.py
10
+ Exit code 0 = all checks passed.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import os
16
+ import subprocess
17
+ import sys
18
+ import tempfile
19
+
20
+ if hasattr(sys.stdout, "reconfigure"):
21
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
22
+
23
+ FAILURES: list[str] = []
24
+
25
+
26
+ def check(name: str, cond: bool, detail: str = ""):
27
+ status = "PASS" if cond else "FAIL"
28
+ print(f"[{status}] {name}" + (f" — {detail}" if detail and not cond else ""))
29
+ if not cond:
30
+ FAILURES.append(name)
31
+
32
+
33
+ def _start_proxy_in_thread(port: int) -> None:
34
+ """Run the real FastAPI app via uvicorn in a daemon thread."""
35
+ import threading
36
+ import time
37
+
38
+ import uvicorn
39
+
40
+ from tokenmizer.api.app import app
41
+
42
+ config = uvicorn.Config(app, host="127.0.0.1", port=port, log_level="error")
43
+ server = uvicorn.Server(config)
44
+ t = threading.Thread(target=server.run, daemon=True)
45
+ t.start()
46
+ for _ in range(50): # wait up to 5s for startup
47
+ if server.started:
48
+ return
49
+ time.sleep(0.1)
50
+ raise RuntimeError("proxy did not start within 5s")
51
+
52
+
53
+ def _seed_session_graph(session_id: str) -> None:
54
+ """Put real nodes into the session graph BEFORE the proxy starts, using
55
+ the same storage dir the proxy will read — so checkpoint/resume exercise
56
+ the full real pipeline instead of an empty graph."""
57
+ from tokenmizer.config.settings import get_settings
58
+ from tokenmizer.graph_memory.graph import GraphMemory
59
+
60
+ storage = get_settings().graph_checkpoint.storage_dir
61
+ g = GraphMemory(session_id, storage_dir=storage)
62
+ g.extract_from_messages([
63
+ {"role": "user", "content": "Let's build a FastAPI auth service with JWT and PostgreSQL"},
64
+ {"role": "assistant", "content": "Decided: PostgreSQL for storage. Completed: project setup. Working on: login endpoint in api/auth.py"},
65
+ ], incremental=False)
66
+ g._persist(force=True)
67
+
68
+
69
+ def main() -> int:
70
+ port = 8765
71
+ _seed_session_graph("mcp-e2e-test")
72
+ _start_proxy_in_thread(port)
73
+ os.environ["TOKENMIZER_URL"] = f"http://127.0.0.1:{port}"
74
+ print(f"proxy up on :{port}")
75
+
76
+ proc = subprocess.Popen(
77
+ [sys.executable, "-m", "tokenmizer.mcp.server"],
78
+ stdin=subprocess.PIPE, stdout=subprocess.PIPE,
79
+ text=True, encoding="utf-8",
80
+ env={**os.environ},
81
+ )
82
+
83
+ def rpc(method: str, params: dict | None = None, req_id: int | None = 1):
84
+ msg: dict = {"jsonrpc": "2.0", "method": method}
85
+ if req_id is not None:
86
+ msg["id"] = req_id
87
+ if params is not None:
88
+ msg["params"] = params
89
+ proc.stdin.write(json.dumps(msg) + "\n")
90
+ proc.stdin.flush()
91
+ if req_id is None:
92
+ return None
93
+ line = proc.stdout.readline()
94
+ return json.loads(line)
95
+
96
+ try:
97
+ # 1. Handshake
98
+ r = rpc("initialize", {"protocolVersion": "2024-11-05",
99
+ "capabilities": {}, "clientInfo": {"name": "e2e", "version": "0"}})
100
+ check("initialize returns serverInfo",
101
+ r and r.get("result", {}).get("serverInfo", {}).get("name") == "tokenmizer")
102
+ check("initialize reports current version",
103
+ r.get("result", {}).get("serverInfo", {}).get("version") not in ("", "0.2.3"),
104
+ str(r.get("result", {}).get("serverInfo")))
105
+ rpc("notifications/initialized", req_id=None)
106
+
107
+ # 2. tools/list
108
+ r = rpc("tools/list", req_id=2)
109
+ tools = {t["name"] for t in r.get("result", {}).get("tools", [])}
110
+ expected = {"checkpoint_session", "resume_session", "get_graph_stats",
111
+ "analyze_file", "get_savings_stats"}
112
+ check("tools/list exposes all 5 tools", tools == expected, str(tools))
113
+
114
+ # 3. Local tool (no proxy): analyze_file on a real CSV
115
+ with tempfile.NamedTemporaryFile("w", suffix=".csv", delete=False,
116
+ encoding="utf-8") as f:
117
+ f.write("region,revenue\nnorth,100\nsouth,250\neast,90\nwest,410\n")
118
+ csv_path = f.name
119
+ r = rpc("tools/call", {"name": "analyze_file",
120
+ "arguments": {"file_path": csv_path}}, req_id=3)
121
+ text = r.get("result", {}).get("content", [{}])[0].get("text", "")
122
+ check("analyze_file returns analysis", "File Analysis" in text, text[:120])
123
+ os.unlink(csv_path)
124
+
125
+ # 4. Proxy-backed tools (require TOKENMIZER_URL server running)
126
+ r = rpc("tools/call", {"name": "get_savings_stats", "arguments": {}}, req_id=4)
127
+ text = r.get("result", {}).get("content", [{}])[0].get("text", "")
128
+ check("get_savings_stats reaches proxy", "Savings Report" in text, text[:120])
129
+
130
+ r = rpc("tools/call", {"name": "checkpoint_session",
131
+ "arguments": {"session_id": "mcp-e2e-test"}}, req_id=5)
132
+ text = r.get("result", {}).get("content", [{}])[0].get("text", "")
133
+ check("checkpoint_session creates checkpoint", "checkpointed" in text, text[:160])
134
+
135
+ r = rpc("tools/call", {"name": "resume_session",
136
+ "arguments": {"session_id": "mcp-e2e-test"}}, req_id=6)
137
+ text = r.get("result", {}).get("content", [{}])[0].get("text", "")
138
+ check("resume_session returns context", "TokenMizer Resume" in text, text[:160])
139
+
140
+ # 5. Unknown method → JSON-RPC error, not crash
141
+ r = rpc("bogus/method", {}, req_id=7)
142
+ check("unknown method returns -32601 error",
143
+ r.get("error", {}).get("code") == -32601)
144
+
145
+ finally:
146
+ proc.stdin.close()
147
+ proc.terminate()
148
+
149
+ print()
150
+ if FAILURES:
151
+ print(f"E2E RESULT: {len(FAILURES)} FAILURE(S): {FAILURES}")
152
+ return 1
153
+ print("E2E RESULT: ALL PASS")
154
+ return 0
155
+
156
+
157
+ if __name__ == "__main__":
158
+ sys.exit(main())
@@ -0,0 +1,29 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.Shweta-Mishra-ai/tokenmizer",
4
+ "description": "An MCP server that provides [describe what your server does]",
5
+ "repository": {
6
+ "url": "https://github.com/Shweta-Mishra-ai/tokenmizer",
7
+ "source": "github"
8
+ },
9
+ "version": "1.0.0",
10
+ "packages": [
11
+ {
12
+ "registryType": "pypi",
13
+ "identifier": "tokenmizer\"\r",
14
+ "version": "1.0.0",
15
+ "transport": {
16
+ "type": "stdio"
17
+ },
18
+ "environmentVariables": [
19
+ {
20
+ "description": "Your API key for the service",
21
+ "isRequired": true,
22
+ "format": "string",
23
+ "isSecret": true,
24
+ "name": "YOUR_API_KEY"
25
+ }
26
+ ]
27
+ }
28
+ ]
29
+ }
@@ -198,3 +198,87 @@ class TestDirectMutationRequiresForce:
198
198
  "force=True is required after any direct node mutation, or the "
199
199
  "dirty-flag optimization silently drops the write"
200
200
  )
201
+
202
+
203
+ class TestEnumRestorationOnLoad:
204
+ """
205
+ Regression tests for the restart-breaks-everything bug (v0.2.4 e2e find):
206
+
207
+ asdict() serializes NodeType/NodeStatus/EdgeType (str-Enums) to plain
208
+ strings; _load() rebuilt nodes WITHOUT converting back. str-Enum equality
209
+ (`n.type == NodeType.TASK`) still passed on reloaded graphs — hiding the
210
+ bug from every existing test — but `.type.value` crashed with
211
+ "'str' object has no attribute 'value'". Net effect: after any server
212
+ restart, checkpointing a reloaded session returned HTTP 500.
213
+ """
214
+
215
+ def test_reloaded_node_types_are_real_enums(self, graph, tmp_path):
216
+ graph.add_node(NodeType.DECISION, "Use PostgreSQL", status=NodeStatus.COMPLETED)
217
+ graph._persist(force=True)
218
+
219
+ reloaded = GraphMemory("dirty-flag-test-session", storage_dir=str(tmp_path))
220
+ node = next(iter(reloaded._nodes.values()))
221
+ assert isinstance(node.type, NodeType), type(node.type)
222
+ assert isinstance(node.status, NodeStatus), type(node.status)
223
+ # The exact access pattern that crashed checkpoint creation:
224
+ assert node.type.value == "decision"
225
+ assert node.status.value == "completed"
226
+
227
+ def test_reloaded_edge_types_are_real_enums(self, graph, tmp_path):
228
+ n1 = graph.add_node(NodeType.TASK, "Implement login")
229
+ n2 = graph.add_node(NodeType.FILE, "src/auth.py")
230
+ graph.add_edge(n1, n2, EdgeType.IMPLEMENTS)
231
+ graph._persist(force=True)
232
+
233
+ reloaded = GraphMemory("dirty-flag-test-session", storage_dir=str(tmp_path))
234
+ assert reloaded._edges, "edge did not survive reload"
235
+ assert isinstance(reloaded._edges[0].type, EdgeType)
236
+ assert reloaded._edges[0].type.value == "implements"
237
+
238
+ def test_checkpoint_of_reloaded_graph_does_not_crash(self, graph, tmp_path):
239
+ """Full restart simulation: persist -> fresh GraphMemory ->
240
+ CheckpointManager.create() — the exact path that 500'd."""
241
+ from tokenmizer.checkpoints.manager import CheckpointManager
242
+
243
+ graph.add_node(NodeType.GOAL, "Build auth service")
244
+ graph.add_node(NodeType.DECISION, "Use PostgreSQL",
245
+ status=NodeStatus.COMPLETED)
246
+ graph._persist(force=True)
247
+
248
+ reloaded = GraphMemory("dirty-flag-test-session", storage_dir=str(tmp_path))
249
+ mgr = CheckpointManager(storage_dir=str(tmp_path))
250
+ ckpt = mgr.create(
251
+ session_id="dirty-flag-test-session",
252
+ messages=[], graph=reloaded, context_pct=0.5, trigger="manual",
253
+ )
254
+ assert ckpt.checkpoint_id
255
+ snapshot_types = {n["type"] for n in ckpt.graph_snapshot["nodes"]}
256
+ assert "decision" in snapshot_types
257
+
258
+ def test_unknown_node_type_is_skipped_not_fatal(self, graph, tmp_path):
259
+ """Forward-compat: a node with an unrecognized type (e.g. written by
260
+ a newer version) must not take down the whole graph load."""
261
+ import json as _json
262
+ import sqlite3 as _sqlite3
263
+
264
+ graph.add_node(NodeType.TASK, "Implement login")
265
+ graph._persist(force=True)
266
+
267
+ # Corrupt one node's type directly in SQLite
268
+ conn = _sqlite3.connect(str(graph._db_path))
269
+ row = conn.execute(
270
+ "SELECT nodes_json FROM graphs WHERE session_id=?",
271
+ ("dirty-flag-test-session",),
272
+ ).fetchone()
273
+ nodes = _json.loads(row[0])
274
+ nodes.append({**nodes[0], "id": "corrupt01", "type": "from_the_future"})
275
+ conn.execute(
276
+ "UPDATE graphs SET nodes_json=? WHERE session_id=?",
277
+ (_json.dumps(nodes), "dirty-flag-test-session"),
278
+ )
279
+ conn.commit()
280
+ conn.close()
281
+
282
+ reloaded = GraphMemory("dirty-flag-test-session", storage_dir=str(tmp_path))
283
+ assert "corrupt01" not in reloaded._nodes # bad node skipped
284
+ assert len(reloaded._nodes) == 1 # good node survived
@@ -1,6 +1,6 @@
1
1
  """TokenMizer — Never lose your AI context again."""
2
2
 
3
- __version__ = "0.2.4"
3
+ __version__ = "0.2.6"
4
4
  __all__ = ["GraphMemory", "CheckpointManager", "get_settings"]
5
5
 
6
6
 
@@ -261,7 +261,7 @@ async def lifespan(app: FastAPI):
261
261
  app = FastAPI(
262
262
  title="TokenMizer",
263
263
  description="Never lose your AI context again.",
264
- version="0.2.4",
264
+ version="0.2.6",
265
265
  lifespan=lifespan,
266
266
  docs_url="/docs",
267
267
  redoc_url="/redoc",
@@ -315,11 +315,37 @@ class GraphMemory:
315
315
  )
316
316
  self._processed_hashes = set()
317
317
 
318
+ # CRITICAL: restore enum types on load. asdict() serializes
319
+ # NodeType/NodeStatus (str-Enums) to plain strings; without
320
+ # converting back, every reloaded node has type/status as `str`.
321
+ # Because they're str-Enums, equality checks still pass — which
322
+ # HID this bug — but any `.value` access crashes ('str' object
323
+ # has no attribute 'value'). Concretely: after a server restart,
324
+ # every checkpoint of a reloaded session returned HTTP 500.
325
+ # Found by the MCP e2e check, not by unit tests, because unit
326
+ # tests reloaded graphs but never then called `.type.value`.
318
327
  for nd in nodes_data:
319
328
  nd.pop("_evicted", None)
329
+ try:
330
+ nd["type"] = NodeType(nd["type"])
331
+ nd["status"] = NodeStatus(nd["status"])
332
+ except (ValueError, KeyError) as conv_err:
333
+ logger.warning(
334
+ f"Skipping node with unknown type/status during load "
335
+ f"for {self.session_id}: {conv_err} — {nd.get('id')}"
336
+ )
337
+ continue
320
338
  n = MemoryNode(**{k: v for k, v in nd.items() if k != "_evicted"})
321
339
  self._nodes[n.id] = n
322
340
  for ed in edges_data:
341
+ try:
342
+ ed["type"] = EdgeType(ed["type"])
343
+ except (ValueError, KeyError) as conv_err:
344
+ logger.warning(
345
+ f"Skipping edge with unknown type during load "
346
+ f"for {self.session_id}: {conv_err}"
347
+ )
348
+ continue
323
349
  self._edges.append(MemoryEdge(**ed))
324
350
  except (sqlite3.DatabaseError, sqlite3.OperationalError) as e:
325
351
  logger.warning(f"Corrupted DB for {self.session_id} — starting fresh: {e}")
@@ -207,7 +207,16 @@ def handle_resume_session(args: dict) -> str:
207
207
  ctx = result.get("resume_context", "")
208
208
  tokens = result.get("token_count", 0)
209
209
  if not ctx:
210
- return f"No checkpoint found for session '{session_id}'. Start a session and checkpoint it first."
210
+ # A checkpoint EXISTS (the API 404s via "error" above when none does)
211
+ # its graph was just empty at checkpoint time. Saying "no checkpoint
212
+ # found" here would be wrong and send the user debugging the wrong
213
+ # thing.
214
+ return (
215
+ f"Checkpoint {result.get('checkpoint_id', '?')} exists for "
216
+ f"'{session_id}' but its graph was empty (no session activity "
217
+ f"had been recorded when it was created). Chat through the proxy "
218
+ f"with this session_id, then checkpoint again."
219
+ )
211
220
  return (
212
221
  f"[TokenMizer Resume — session: {session_id} — {tokens} tokens]\n\n"
213
222
  f"{ctx}\n\n"
@@ -333,12 +342,13 @@ def run_stdio_server():
333
342
  params = req.get("params", {})
334
343
 
335
344
  if method == "initialize":
345
+ from tokenmizer import __version__
336
346
  send({
337
347
  "jsonrpc": "2.0", "id": req_id,
338
348
  "result": {
339
349
  "protocolVersion": "2024-11-05",
340
350
  "capabilities": {"tools": {}},
341
- "serverInfo": {"name": "tokenmizer", "version": "0.2.3"},
351
+ "serverInfo": {"name": "tokenmizer", "version": __version__},
342
352
  },
343
353
  })
344
354
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes