madb-mcp-server 0.1.0__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.
@@ -0,0 +1,54 @@
1
+ # Rust build artifacts
2
+ target/
3
+ **/target/
4
+ **/*.rs.bk
5
+
6
+ # Cargo — keep Cargo.lock at workspace root (we're an application, not a library)
7
+ # Individual crate Cargo.lock files should not be committed.
8
+ crates/*/Cargo.lock
9
+
10
+ # Python (maturin develop install side-effects + venvs)
11
+ __pycache__/
12
+ *.py[cod]
13
+ *$py.class
14
+ *.so
15
+ *.egg-info/
16
+ .venv/
17
+ venv/
18
+ .Python
19
+ build/
20
+ dist/
21
+ *.whl
22
+
23
+ # Editor / OS
24
+ .DS_Store
25
+ .idea/
26
+ .vscode/
27
+ *.swp
28
+ *.swo
29
+ *~
30
+ .env
31
+ .env.local
32
+
33
+ # Maturin develop side-effects — the installed wheel lives in the caller's
34
+ # venv; we do not want any artifact under the project tree.
35
+ .maturin/
36
+
37
+ # Test coverage reports
38
+ target/criterion/
39
+ *.profraw
40
+ *.profdata
41
+ coverage/
42
+
43
+ # MADB runtime data directories (should never be in the repo).
44
+ # Intentionally NOT using a global *.lock pattern so the workspace
45
+ # Cargo.lock stays tracked (Rust workspace Cargo.lock must be committed).
46
+ data/
47
+ /tmp/madb*
48
+ **/data/madb*
49
+ **/data/maos_memory*
50
+ **/wal/*.log
51
+ **/hlc.json
52
+ # MADB data-dir lockfile is literally named "LOCK" (no extension)
53
+ # inside the data directory, not at the workspace root.
54
+ **/data/**/LOCK
@@ -0,0 +1,131 @@
1
+ Metadata-Version: 2.4
2
+ Name: madb-mcp-server
3
+ Version: 0.1.0
4
+ Summary: MCP server for Meta-AgentsDB — persistent causal memory for AI agents
5
+ Project-URL: Homepage, https://meta-agents.ai
6
+ Project-URL: Repository, https://github.com/spshkar84/meta-agents-db
7
+ Author-email: Pushkar Singh <01@meta-agents.ai>, Pushkar Singh <spshkar84@gmail.com>, Pushkar Singh <spshkar84@meta-agents.ai>
8
+ License-Expression: Apache-2.0
9
+ Keywords: agents,claude,database,mcp,memory
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: Apache Software License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Database
15
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
16
+ Requires-Python: >=3.10
17
+ Requires-Dist: mcp>=1.0.0
18
+ Requires-Dist: meta-agents-db>=0.1.0
19
+ Description-Content-Type: text/markdown
20
+
21
+ # madb-mcp-server
22
+
23
+ Persistent causal memory for Claude Code — **4.65x recall ROI** measured in production.
24
+
25
+ `madb-mcp-server` gives Claude Code (and any MCP-compatible client) a real database backend for agent memory: causal chains, composite-scored recall, policy-gated access, and built-in analytics.
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ pip install madb-mcp-server
31
+ ```
32
+
33
+ Or run directly with uvx (no install needed):
34
+
35
+ ```bash
36
+ uvx madb-mcp-server
37
+ ```
38
+
39
+ ## Claude Code Setup
40
+
41
+ ```bash
42
+ claude mcp add madb -- uvx madb-mcp-server
43
+ ```
44
+
45
+ Or add to your project's `.mcp.json`:
46
+
47
+ ```json
48
+ {
49
+ "mcpServers": {
50
+ "madb": {
51
+ "command": "uvx",
52
+ "args": ["madb-mcp-server"],
53
+ "env": {
54
+ "MADB_DATA_DIR": "~/.madb/data",
55
+ "MADB_TENANT_ID": "default"
56
+ }
57
+ }
58
+ }
59
+ }
60
+ ```
61
+
62
+ ## Claude Desktop Setup
63
+
64
+ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
65
+
66
+ ```json
67
+ {
68
+ "mcpServers": {
69
+ "madb": {
70
+ "command": "uvx",
71
+ "args": ["madb-mcp-server"],
72
+ "env": {
73
+ "MADB_DATA_DIR": "~/.madb/data",
74
+ "MADB_TENANT_ID": "default"
75
+ }
76
+ }
77
+ }
78
+ }
79
+ ```
80
+
81
+ ## Tools (9)
82
+
83
+ | Tool | Description |
84
+ |------|-------------|
85
+ | `remember` | Store a memory with causal links, tags, and importance scoring |
86
+ | `recall` | Semantic recall — vector similarity + recency + causal proximity + importance |
87
+ | `get_memory` | Point lookup by event_id |
88
+ | `forget` | Soft-delete a memory (tombstone, preserved in causal chain) |
89
+ | `search` | Structured query by tenant + scope |
90
+ | `list_recent` | Last N memories for a tenant |
91
+ | `stats` | Engine metrics snapshot (writes, reads, flushes, compactions, WAL state) |
92
+ | `trace_cause` | Walk the causal DAG forward or backward from any memory |
93
+ | `analytics` | MCP usage analytics — call counts, latencies, recall precision, Memory ROI |
94
+
95
+ ## Memory ROI
96
+
97
+ MADB tracks how much value its memory provides to your Claude sessions:
98
+
99
+ - **Tokens served from recall** — context delivered from memory instead of re-reading files
100
+ - **File reads avoided** — estimated file re-reads saved by recalling from memory
101
+ - **Recall precision** — hit rate and average relevance score
102
+ - **Write-to-read ratio** — tokens recalled per token written (measured 4.65x in production)
103
+
104
+ Run the `analytics` tool at any time to see your session's Memory ROI.
105
+
106
+ ## Resources
107
+
108
+ | URI | Description |
109
+ |-----|-------------|
110
+ | `madb://memories/{event_id}` | Read a single memory record |
111
+
112
+ ## Environment Variables
113
+
114
+ | Variable | Default | Description |
115
+ |----------|---------|-------------|
116
+ | `MADB_DATA_DIR` | `~/.madb/data` | Database storage directory |
117
+ | `MADB_TENANT_ID` | `default` | Default tenant for tool calls |
118
+
119
+ ## How It Works
120
+
121
+ MADB is an agent-native database built in Rust with 5 novel primitives:
122
+
123
+ 1. **Causal DAG** — every memory links to what caused it, forming an evidence chain
124
+ 2. **Composite-scored recall** — vector similarity + recency + causal proximity + importance
125
+ 3. **Storage-layer policy** — scope and retention rules enforced at the engine level
126
+ 4. **Partitioned WAL** — per-tenant write-ahead log for crash safety
127
+ 5. **Time-bucketed HNSW** — vector index organized by time for recency-aware search
128
+
129
+ ## License
130
+
131
+ Apache-2.0 (patent pending)
@@ -0,0 +1,111 @@
1
+ # madb-mcp-server
2
+
3
+ Persistent causal memory for Claude Code — **4.65x recall ROI** measured in production.
4
+
5
+ `madb-mcp-server` gives Claude Code (and any MCP-compatible client) a real database backend for agent memory: causal chains, composite-scored recall, policy-gated access, and built-in analytics.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pip install madb-mcp-server
11
+ ```
12
+
13
+ Or run directly with uvx (no install needed):
14
+
15
+ ```bash
16
+ uvx madb-mcp-server
17
+ ```
18
+
19
+ ## Claude Code Setup
20
+
21
+ ```bash
22
+ claude mcp add madb -- uvx madb-mcp-server
23
+ ```
24
+
25
+ Or add to your project's `.mcp.json`:
26
+
27
+ ```json
28
+ {
29
+ "mcpServers": {
30
+ "madb": {
31
+ "command": "uvx",
32
+ "args": ["madb-mcp-server"],
33
+ "env": {
34
+ "MADB_DATA_DIR": "~/.madb/data",
35
+ "MADB_TENANT_ID": "default"
36
+ }
37
+ }
38
+ }
39
+ }
40
+ ```
41
+
42
+ ## Claude Desktop Setup
43
+
44
+ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
45
+
46
+ ```json
47
+ {
48
+ "mcpServers": {
49
+ "madb": {
50
+ "command": "uvx",
51
+ "args": ["madb-mcp-server"],
52
+ "env": {
53
+ "MADB_DATA_DIR": "~/.madb/data",
54
+ "MADB_TENANT_ID": "default"
55
+ }
56
+ }
57
+ }
58
+ }
59
+ ```
60
+
61
+ ## Tools (9)
62
+
63
+ | Tool | Description |
64
+ |------|-------------|
65
+ | `remember` | Store a memory with causal links, tags, and importance scoring |
66
+ | `recall` | Semantic recall — vector similarity + recency + causal proximity + importance |
67
+ | `get_memory` | Point lookup by event_id |
68
+ | `forget` | Soft-delete a memory (tombstone, preserved in causal chain) |
69
+ | `search` | Structured query by tenant + scope |
70
+ | `list_recent` | Last N memories for a tenant |
71
+ | `stats` | Engine metrics snapshot (writes, reads, flushes, compactions, WAL state) |
72
+ | `trace_cause` | Walk the causal DAG forward or backward from any memory |
73
+ | `analytics` | MCP usage analytics — call counts, latencies, recall precision, Memory ROI |
74
+
75
+ ## Memory ROI
76
+
77
+ MADB tracks how much value its memory provides to your Claude sessions:
78
+
79
+ - **Tokens served from recall** — context delivered from memory instead of re-reading files
80
+ - **File reads avoided** — estimated file re-reads saved by recalling from memory
81
+ - **Recall precision** — hit rate and average relevance score
82
+ - **Write-to-read ratio** — tokens recalled per token written (measured 4.65x in production)
83
+
84
+ Run the `analytics` tool at any time to see your session's Memory ROI.
85
+
86
+ ## Resources
87
+
88
+ | URI | Description |
89
+ |-----|-------------|
90
+ | `madb://memories/{event_id}` | Read a single memory record |
91
+
92
+ ## Environment Variables
93
+
94
+ | Variable | Default | Description |
95
+ |----------|---------|-------------|
96
+ | `MADB_DATA_DIR` | `~/.madb/data` | Database storage directory |
97
+ | `MADB_TENANT_ID` | `default` | Default tenant for tool calls |
98
+
99
+ ## How It Works
100
+
101
+ MADB is an agent-native database built in Rust with 5 novel primitives:
102
+
103
+ 1. **Causal DAG** — every memory links to what caused it, forming an evidence chain
104
+ 2. **Composite-scored recall** — vector similarity + recency + causal proximity + importance
105
+ 3. **Storage-layer policy** — scope and retention rules enforced at the engine level
106
+ 4. **Partitioned WAL** — per-tenant write-ahead log for crash safety
107
+ 5. **Time-bucketed HNSW** — vector index organized by time for recency-aware search
108
+
109
+ ## License
110
+
111
+ Apache-2.0 (patent pending)
@@ -0,0 +1,4 @@
1
+ """Allow `python -m madb_mcp_server` to launch the MCP server."""
2
+ from madb_mcp_server import mcp
3
+
4
+ mcp.run()
@@ -0,0 +1,343 @@
1
+ #!/usr/bin/env python3
2
+ # Copyright 2025-2026 Pushkar Singh / Meta-Agents.AI
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ # Part of Meta-AgentsDB — the first database designed for autonomous AI agents.
5
+ # https://meta-agents.ai
6
+ """
7
+ madb_mcp_analytics — MCP usage instrumentation for Meta-AgentsDB
8
+ ================================================================
9
+
10
+ Lightweight, stdlib-only analytics collector that records every MCP tool
11
+ invocation and produces point-in-time usage snapshots with Memory ROI.
12
+
13
+ Storage: append-only JSONL log at ``$MADB_DATA_DIR/mcp_analytics.jsonl``.
14
+ Hot path: in-memory counters (dict of tool -> count / latency / errors).
15
+ Flush: background thread every 60 s + ``atexit``.
16
+
17
+ Session tracking:
18
+ Each MCP server process lifetime = one Claude Code session.
19
+ A ``session_id`` (UUID4) is generated at init and stamped on every
20
+ JSONL line + snapshot. This lets the report group calls by session
21
+ and compute per-session Memory ROI.
22
+
23
+ Memory ROI model:
24
+ When Claude recalls memories instead of re-reading source files,
25
+ each recalled token represents context served "for free" — without
26
+ a file read round-trip. We estimate:
27
+
28
+ recall_tokens_saved = tokens_served_from_recall
29
+ avg_file_read_tokens = 1500 (conservative median for a source file)
30
+ file_reads_avoided = recall_tokens_saved / avg_file_read_tokens
31
+ memory_roi = recall_tokens_saved / max(memories_written_tokens, 1)
32
+
33
+ The ``recall_precision`` metric tracks the quality of recalled results
34
+ via the composite similarity score (avg top_score across all recalls).
35
+ """
36
+
37
+ from __future__ import annotations
38
+
39
+ import atexit
40
+ import json
41
+ import os
42
+ import platform
43
+ import threading
44
+ import time
45
+ import uuid
46
+ from collections import defaultdict
47
+ from typing import Any
48
+
49
+ # Typical token count when Claude reads a source file via Read tool.
50
+ # Used as the denominator for "file reads avoided" estimation.
51
+ AVG_FILE_READ_TOKENS = 1500
52
+
53
+
54
+ def _resolve_user_fingerprint() -> dict[str, str]:
55
+ """Resolve machine + user identity once at process start.
56
+
57
+ Returns a dict with ``user`` (login name) and ``host`` (hostname).
58
+ Both fall back to ``"unknown"`` if the OS call fails (e.g. inside
59
+ a sandboxed container with no /etc/passwd).
60
+ """
61
+ try:
62
+ user = os.getlogin()
63
+ except OSError:
64
+ user = os.environ.get("USER", os.environ.get("USERNAME", "unknown"))
65
+ host = platform.node() or "unknown"
66
+ return {"user": user, "host": host}
67
+
68
+
69
+ class _ToolStats:
70
+ """Mutable per-tool accumulator (not thread-safe on its own — the
71
+ caller must hold the lock)."""
72
+
73
+ __slots__ = (
74
+ "calls", "errors", "latency_sum", "latency_max",
75
+ "latencies", "result_count_sum", "tokens_served",
76
+ "recall_hits", "top_scores",
77
+ )
78
+
79
+ def __init__(self) -> None:
80
+ self.calls: int = 0
81
+ self.errors: int = 0
82
+ self.latency_sum: float = 0.0
83
+ self.latency_max: float = 0.0
84
+ self.latencies: list[float] = []
85
+ self.result_count_sum: int = 0
86
+ self.tokens_served: int = 0
87
+ self.recall_hits: int = 0 # recalls returning >= 1 result
88
+ self.top_scores: list[float] = [] # best score per recall (precision signal)
89
+
90
+
91
+ class McpAnalytics:
92
+ """In-process analytics collector for the MADB MCP server.
93
+
94
+ Usage::
95
+
96
+ analytics = McpAnalytics("/path/to/mcp_analytics.jsonl")
97
+
98
+ t0 = time.perf_counter()
99
+ # ... tool work ...
100
+ analytics.record_call("recall", (time.perf_counter()-t0)*1000,
101
+ True, {"result_count": 5, "tokens": 420,
102
+ "top_score": 0.87})
103
+
104
+ print(json.dumps(analytics.snapshot(), indent=2))
105
+ """
106
+
107
+ def __init__(self, log_path: str, flush_interval: float = 60.0) -> None:
108
+ self._log_path = log_path
109
+ self._flush_interval = flush_interval
110
+ self._fingerprint = _resolve_user_fingerprint()
111
+ self._session_id = str(uuid.uuid4())
112
+ self._session_seq = 0 # monotonic call counter within session
113
+
114
+ self._lock = threading.Lock()
115
+ self._tools: dict[str, _ToolStats] = defaultdict(_ToolStats)
116
+ self._total_calls: int = 0
117
+ self._total_errors: int = 0
118
+ self._start_ts: float = time.time()
119
+
120
+ # Track writes for ROI denominator (tokens written into memory)
121
+ self._memories_written_tokens: int = 0
122
+
123
+ # Pending JSONL lines waiting to be flushed
124
+ self._pending: list[str] = []
125
+
126
+ # Background flush thread
127
+ self._stop = threading.Event()
128
+ self._thread = threading.Thread(
129
+ target=self._flush_loop, daemon=True, name="madb-mcp-analytics"
130
+ )
131
+ self._thread.start()
132
+ atexit.register(self.flush_to_disk)
133
+
134
+ @property
135
+ def session_id(self) -> str:
136
+ return self._session_id
137
+
138
+ # ------------------------------------------------------------------
139
+ # Public API
140
+ # ------------------------------------------------------------------
141
+
142
+ def record_call(
143
+ self,
144
+ tool: str,
145
+ latency_ms: float,
146
+ success: bool,
147
+ meta: dict[str, Any] | None = None,
148
+ ) -> None:
149
+ """Record a single MCP tool invocation."""
150
+ meta = meta or {}
151
+
152
+ with self._lock:
153
+ self._session_seq += 1
154
+ seq = self._session_seq
155
+
156
+ line = json.dumps({
157
+ "ts": time.time(),
158
+ "session_id": self._session_id,
159
+ "session_seq": seq,
160
+ "tool": tool,
161
+ "latency_ms": round(latency_ms, 2),
162
+ "ok": success,
163
+ "user": self._fingerprint["user"],
164
+ "host": self._fingerprint["host"],
165
+ "tenant": meta.get("tenant", ""),
166
+ "result_count": meta.get("result_count", 0),
167
+ "tokens": meta.get("tokens", 0),
168
+ "top_score": meta.get("top_score", 0.0),
169
+ "payload_tokens": meta.get("payload_tokens", 0),
170
+ })
171
+
172
+ with self._lock:
173
+ ts = self._tools[tool]
174
+ ts.calls += 1
175
+ ts.latency_sum += latency_ms
176
+ ts.latencies.append(latency_ms)
177
+ if latency_ms > ts.latency_max:
178
+ ts.latency_max = latency_ms
179
+ if not success:
180
+ ts.errors += 1
181
+ self._total_errors += 1
182
+ ts.result_count_sum += meta.get("result_count", 0)
183
+ ts.tokens_served += meta.get("tokens", 0)
184
+ self._total_calls += 1
185
+ self._pending.append(line)
186
+
187
+ # Recall-specific tracking
188
+ if tool == "recall":
189
+ rc = meta.get("result_count", 0)
190
+ if rc > 0:
191
+ ts.recall_hits += 1
192
+ top = meta.get("top_score", 0.0)
193
+ if top > 0:
194
+ ts.top_scores.append(top)
195
+
196
+ # Track write payload size for ROI denominator
197
+ if tool == "remember":
198
+ self._memories_written_tokens += meta.get("payload_tokens", 0)
199
+
200
+ def snapshot(self) -> dict[str, Any]:
201
+ """Point-in-time usage summary for the ``analytics`` MCP tool.
202
+
203
+ Includes session context, per-tool stats, recall precision,
204
+ and Memory ROI computation.
205
+ """
206
+ with self._lock:
207
+ tools: dict[str, Any] = {}
208
+ total_recalls = 0
209
+ recall_hits = 0
210
+ total_tokens_served = 0
211
+ total_results = 0
212
+ all_latencies: list[float] = []
213
+ all_top_scores: list[float] = []
214
+ memories_written = 0
215
+
216
+ for name, ts in self._tools.items():
217
+ avg_lat = ts.latency_sum / ts.calls if ts.calls else 0
218
+ lats = sorted(ts.latencies)
219
+ p50 = _percentile(lats, 50)
220
+ p99 = _percentile(lats, 99)
221
+ tools[name] = {
222
+ "calls": ts.calls,
223
+ "errors": ts.errors,
224
+ "avg_latency_ms": round(avg_lat, 1),
225
+ "p50_latency_ms": round(p50, 1),
226
+ "p99_latency_ms": round(p99, 1),
227
+ "max_latency_ms": round(ts.latency_max, 1),
228
+ }
229
+ all_latencies.extend(ts.latencies)
230
+
231
+ if name == "recall":
232
+ total_recalls = ts.calls
233
+ recall_hits = ts.recall_hits
234
+ total_tokens_served = ts.tokens_served
235
+ total_results = ts.result_count_sum
236
+ all_top_scores = ts.top_scores[:]
237
+ tools[name]["tokens_served"] = ts.tokens_served
238
+ tools[name]["total_results"] = ts.result_count_sum
239
+ tools[name]["recall_hits"] = ts.recall_hits
240
+
241
+ if name == "remember":
242
+ memories_written = ts.calls
243
+ tools[name]["memories_written"] = ts.calls
244
+
245
+ all_sorted = sorted(all_latencies)
246
+
247
+ # --- Recall precision ---
248
+ recall_hit_rate = (
249
+ round(recall_hits / max(total_recalls, 1) * 100, 1)
250
+ if total_recalls > 0 else 0.0
251
+ )
252
+ avg_top_score = (
253
+ round(sum(all_top_scores) / len(all_top_scores), 3)
254
+ if all_top_scores else 0.0
255
+ )
256
+ avg_results_per_recall = (
257
+ round(total_results / max(total_recalls, 1), 1)
258
+ if total_recalls > 0 else 0.0
259
+ )
260
+
261
+ # --- Memory ROI ---
262
+ file_reads_avoided = round(
263
+ total_tokens_served / AVG_FILE_READ_TOKENS, 1
264
+ )
265
+ memory_roi = (
266
+ round(total_tokens_served / max(self._memories_written_tokens, 1), 2)
267
+ if self._memories_written_tokens > 0
268
+ else 0.0
269
+ )
270
+ session_duration = round(time.time() - self._start_ts, 1)
271
+ calls_per_minute = (
272
+ round(self._total_calls / max(session_duration / 60, 0.01), 1)
273
+ )
274
+
275
+ return {
276
+ "session_id": self._session_id,
277
+ "user": self._fingerprint["user"],
278
+ "host": self._fingerprint["host"],
279
+ "session_duration_s": session_duration,
280
+ "session_calls": self._total_calls,
281
+ "calls_per_minute": calls_per_minute,
282
+ "total_calls": self._total_calls,
283
+ "total_errors": self._total_errors,
284
+ "error_rate": round(
285
+ self._total_errors / max(self._total_calls, 1) * 100, 2
286
+ ),
287
+ "all_tool_p50_ms": round(_percentile(all_sorted, 50), 1),
288
+ "all_tool_p99_ms": round(_percentile(all_sorted, 99), 1),
289
+ "recall_precision": {
290
+ "hit_rate_pct": recall_hit_rate,
291
+ "avg_top_score": avg_top_score,
292
+ "avg_results_per_recall": avg_results_per_recall,
293
+ "total_recalls": total_recalls,
294
+ "recalls_with_results": recall_hits,
295
+ },
296
+ "memory_roi": {
297
+ "tokens_served_from_recall": total_tokens_served,
298
+ "memories_written": memories_written,
299
+ "memories_written_tokens": self._memories_written_tokens,
300
+ "est_file_reads_avoided": file_reads_avoided,
301
+ "write_to_read_ratio": memory_roi,
302
+ },
303
+ "tools": tools,
304
+ }
305
+
306
+ def flush_to_disk(self) -> None:
307
+ """Write pending JSONL lines to disk."""
308
+ with self._lock:
309
+ if not self._pending:
310
+ return
311
+ lines = self._pending[:]
312
+ self._pending.clear()
313
+
314
+ # Ensure parent directory exists
315
+ os.makedirs(os.path.dirname(self._log_path) or ".", exist_ok=True)
316
+ with open(self._log_path, "a") as f:
317
+ for line in lines:
318
+ f.write(line + "\n")
319
+
320
+ def stop(self) -> None:
321
+ """Stop the background flush thread (for clean shutdown)."""
322
+ self._stop.set()
323
+ self._thread.join(timeout=2)
324
+
325
+ # ------------------------------------------------------------------
326
+ # Internal
327
+ # ------------------------------------------------------------------
328
+
329
+ def _flush_loop(self) -> None:
330
+ while not self._stop.wait(self._flush_interval):
331
+ self.flush_to_disk()
332
+
333
+
334
+ def _percentile(sorted_values: list[float], pct: float) -> float:
335
+ """Compute a percentile from a pre-sorted list."""
336
+ if not sorted_values:
337
+ return 0.0
338
+ k = (len(sorted_values) - 1) * (pct / 100)
339
+ f = int(k)
340
+ c = f + 1
341
+ if c >= len(sorted_values):
342
+ return sorted_values[-1]
343
+ return sorted_values[f] + (k - f) * (sorted_values[c] - sorted_values[f])