tokenmizer 0.2.4__tar.gz → 0.2.5__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 (109) hide show
  1. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/CHANGELOG.md +23 -0
  2. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/PKG-INFO +6 -10
  3. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/README.md +5 -9
  4. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/pyproject.toml +2 -4
  5. tokenmizer-0.2.5/scripts/mcp_e2e_check.py +158 -0
  6. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tests/unit/test_graph_persistence.py +84 -0
  7. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tokenmizer/__init__.py +1 -1
  8. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tokenmizer/api/app.py +1 -1
  9. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tokenmizer/graph_memory/graph.py +26 -0
  10. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tokenmizer/mcp/server.py +12 -2
  11. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/.claude-plugin/marketplace.json +0 -0
  12. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/.claude-plugin/plugin.json +0 -0
  13. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/.claude-plugin/skills/analyze/SKILL.md +0 -0
  14. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/.claude-plugin/skills/checkpoint/SKILL.md +0 -0
  15. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/.claude-plugin/skills/resume/SKILL.md +0 -0
  16. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/.claude-plugin/skills/stats/SKILL.md +0 -0
  17. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  18. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/.github/ISSUE_TEMPLATE/extraction_miss.md +0 -0
  19. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  20. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/.github/workflows/ci.yml +0 -0
  21. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/.github/workflows/release.yml +0 -0
  22. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/.gitignore +0 -0
  23. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/.mcp.json +0 -0
  24. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/CONTRIBUTING.md +0 -0
  25. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/Dockerfile +0 -0
  26. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/LICENSE +0 -0
  27. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/SECURITY.md +0 -0
  28. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/TESTING.md +0 -0
  29. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/USAGE.md +0 -0
  30. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/benchmarks/__init__.py +0 -0
  31. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/benchmarks/checkpoint_accuracy/__init__.py +0 -0
  32. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/benchmarks/checkpoint_accuracy/runner.py +0 -0
  33. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/benchmarks/checkpoint_accuracy/runner_v2.py +0 -0
  34. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/benchmarks/checkpoint_accuracy/runner_v3.py +0 -0
  35. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/benchmarks/graph_retrieval/__init__.py +0 -0
  36. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/benchmarks/graph_retrieval/runner.py +0 -0
  37. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/benchmarks/latency/__init__.py +0 -0
  38. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/benchmarks/latency/runner.py +0 -0
  39. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/benchmarks/resume_quality/__init__.py +0 -0
  40. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/docker-compose.yml +0 -0
  41. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/docs/assets/architecture.svg +0 -0
  42. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/docs/assets/logo.svg +0 -0
  43. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/examples/basic_usage.py +0 -0
  44. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/scripts/install.sh +0 -0
  45. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/scripts/run_stdlib_tests.py +0 -0
  46. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/scripts/setup.sh +0 -0
  47. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/scripts/static_audit.py +0 -0
  48. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tests/__init__.py +0 -0
  49. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tests/chaos/__init__.py +0 -0
  50. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tests/chaos/test_recovery.py +0 -0
  51. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tests/conftest.py +0 -0
  52. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tests/integration/__init__.py +0 -0
  53. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tests/integration/test_api_endpoint.py +0 -0
  54. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tests/integration/test_checkpoint.py +0 -0
  55. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tests/memory_accuracy/__init__.py +0 -0
  56. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tests/memory_accuracy/test_retention.py +0 -0
  57. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tests/unit/__init__.py +0 -0
  58. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tests/unit/test_cache.py +0 -0
  59. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tests/unit/test_compression_correctness.py +0 -0
  60. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tests/unit/test_decision_cache_async.py +0 -0
  61. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tests/unit/test_file_intelligence.py +0 -0
  62. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tests/unit/test_graph.py +0 -0
  63. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tests/unit/test_hybrid_extractor.py +0 -0
  64. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tests/unit/test_rate_limiter.py +0 -0
  65. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tests/unit/test_security.py +0 -0
  66. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tests/unit/test_tokenizer.py +0 -0
  67. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tests/unit/test_validator.py +0 -0
  68. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tokenmizer/agents/__init__.py +0 -0
  69. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tokenmizer/analytics/__init__.py +0 -0
  70. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tokenmizer/analytics/engine.py +0 -0
  71. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tokenmizer/api/__init__.py +0 -0
  72. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tokenmizer/api/rate_limiter.py +0 -0
  73. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tokenmizer/checkpoints/__init__.py +0 -0
  74. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tokenmizer/checkpoints/manager.py +0 -0
  75. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tokenmizer/cli.py +0 -0
  76. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tokenmizer/compression/__init__.py +0 -0
  77. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tokenmizer/compression/engine.py +0 -0
  78. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tokenmizer/compression/output_trimmer.py +0 -0
  79. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tokenmizer/compression/window.py +0 -0
  80. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tokenmizer/config/__init__.py +0 -0
  81. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tokenmizer/config/settings.py +0 -0
  82. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tokenmizer/core/__init__.py +0 -0
  83. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tokenmizer/core/dto.py +0 -0
  84. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tokenmizer/core/errors.py +0 -0
  85. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tokenmizer/core/tokenizer.py +0 -0
  86. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tokenmizer/dashboard/__init__.py +0 -0
  87. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tokenmizer/dashboard/page.py +0 -0
  88. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tokenmizer/filters/__init__.py +0 -0
  89. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tokenmizer/filters/file_intelligence.py +0 -0
  90. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tokenmizer/graph_memory/__init__.py +0 -0
  91. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tokenmizer/graph_memory/decision_tracker.py +0 -0
  92. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tokenmizer/graph_memory/helpers.py +0 -0
  93. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tokenmizer/graph_memory/hybrid_extractor.py +0 -0
  94. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tokenmizer/graph_memory/types.py +0 -0
  95. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tokenmizer/graph_memory/validator.py +0 -0
  96. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tokenmizer/graph_memory/visualization.py +0 -0
  97. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tokenmizer/mcp/__init__.py +0 -0
  98. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tokenmizer/providers/__init__.py +0 -0
  99. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tokenmizer/providers/providers.py +0 -0
  100. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tokenmizer/security/__init__.py +0 -0
  101. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tokenmizer/security/auth.py +0 -0
  102. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tokenmizer/security/middleware.py +0 -0
  103. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tokenmizer/security/redaction.py +0 -0
  104. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tokenmizer/semantic_cache/__init__.py +0 -0
  105. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tokenmizer/semantic_cache/cache.py +0 -0
  106. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tokenmizer/state/__init__.py +0 -0
  107. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tokenmizer/state/backend.py +0 -0
  108. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tokenmizer/storage/__init__.py +0 -0
  109. {tokenmizer-0.2.4 → tokenmizer-0.2.5}/tokenmizer.yaml +0 -0
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.5] — 2026-07-02 — restart no longer breaks checkpoints
4
+
5
+ ### Critical
6
+ - **`graph_memory/graph.py`:** `_load()` rebuilt nodes/edges from SQLite
7
+ without converting `type`/`status` strings back to their str-Enum types.
8
+ Str-Enum equality kept passing (which hid the bug from every unit test),
9
+ but any `.value` access crashed — concretely, **after a server restart,
10
+ checkpointing any reloaded session returned HTTP 500**, and resume paths
11
+ touching `.value` were equally broken. Enums are now restored on load;
12
+ nodes/edges with unrecognized types (forward-compat) are skipped with a
13
+ warning instead of killing the whole load. Found by the new MCP e2e
14
+ check, not unit tests — reloaded graphs were never checkpoint-ed in tests
15
+ before. Four regression tests added.
16
+
17
+ ### MCP server
18
+ - `serverInfo.version` no longer hardcoded (was stale "0.2.3").
19
+ - `resume_session` no longer claims "No checkpoint found" when a checkpoint
20
+ exists but its graph was empty — the two cases now produce distinct,
21
+ accurate messages.
22
+ - NEW `scripts/mcp_e2e_check.py`: boots the real proxy in-process, spawns
23
+ the real MCP stdio server, and exercises handshake + all 5 tools
24
+ end-to-end. This is the check that caught the restart bug.
25
+
3
26
  ## [0.2.4] — 2026-07-02 — launch-readiness fixes
4
27
 
5
28
  ### 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.5
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
@@ -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
@@ -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.5"
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"
@@ -108,6 +108,4 @@ omit = [
108
108
  [tool.coverage.report]
109
109
  # Honest floor over ALL product code (previously 70, but with api/app.py,
110
110
  # 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
111
+ # 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())
@@ -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.5"
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.5",
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