data-olympus 0.3.2__tar.gz → 0.3.3__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 (81) hide show
  1. {data_olympus-0.3.2 → data_olympus-0.3.3}/CHANGELOG.md +23 -1
  2. {data_olympus-0.3.2 → data_olympus-0.3.3}/PKG-INFO +1 -1
  3. {data_olympus-0.3.2 → data_olympus-0.3.3}/pyproject.toml +1 -1
  4. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/config.py +11 -2
  5. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/server.py +21 -2
  6. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/session_metrics.py +51 -9
  7. {data_olympus-0.3.2 → data_olympus-0.3.3}/.gitignore +0 -0
  8. {data_olympus-0.3.2 → data_olympus-0.3.3}/LICENSE +0 -0
  9. {data_olympus-0.3.2 → data_olympus-0.3.3}/NOTICE +0 -0
  10. {data_olympus-0.3.2 → data_olympus-0.3.3}/README.md +0 -0
  11. {data_olympus-0.3.2 → data_olympus-0.3.3}/SPEC.md +0 -0
  12. {data_olympus-0.3.2 → data_olympus-0.3.3}/benchmarks/README.md +0 -0
  13. {data_olympus-0.3.2 → data_olympus-0.3.3}/bin/_kb_detect_workspace.sh +0 -0
  14. {data_olympus-0.3.2 → data_olympus-0.3.3}/bin/_kb_enforce.py +0 -0
  15. {data_olympus-0.3.2 → data_olympus-0.3.3}/bin/_kb_fallback.py +0 -0
  16. {data_olympus-0.3.2 → data_olympus-0.3.3}/bin/kb +0 -0
  17. {data_olympus-0.3.2 → data_olympus-0.3.3}/bin/kb-enforce-hook +0 -0
  18. {data_olympus-0.3.2 → data_olympus-0.3.3}/bin/opencode/data-olympus-gate.ts +0 -0
  19. {data_olympus-0.3.2 → data_olympus-0.3.3}/deploy/k8s/README.md +0 -0
  20. {data_olympus-0.3.2 → data_olympus-0.3.3}/deploy/k8s/read-replica/README.md +0 -0
  21. {data_olympus-0.3.2 → data_olympus-0.3.3}/docs/releases/README.md +0 -0
  22. {data_olympus-0.3.2 → data_olympus-0.3.3}/example-bundle/projects/acme-app/README.md +0 -0
  23. {data_olympus-0.3.2 → data_olympus-0.3.3}/example-bundle/projects/acme-app/components/api/README.md +0 -0
  24. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/__init__.py +0 -0
  25. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/audit_log.py +0 -0
  26. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/audit_trailers.py +0 -0
  27. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/auth.py +0 -0
  28. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/cli/__init__.py +0 -0
  29. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/cli/import_cmd.py +0 -0
  30. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/cli/indexgen.py +0 -0
  31. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/cli/main.py +0 -0
  32. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/cli/report_cmd.py +0 -0
  33. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/cooccurrence.py +0 -0
  34. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/dedup.py +0 -0
  35. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/durable.py +0 -0
  36. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/embeddings.py +0 -0
  37. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/enforce_policy.py +0 -0
  38. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/format/__init__.py +0 -0
  39. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/format/document.py +0 -0
  40. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/format/frontmatter.py +0 -0
  41. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/format/lint.py +0 -0
  42. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/format/validate.py +0 -0
  43. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/git_ops.py +0 -0
  44. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/health.py +0 -0
  45. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/importer/__init__.py +0 -0
  46. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/importer/adr.py +0 -0
  47. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/importer/flat.py +0 -0
  48. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/importer/model.py +0 -0
  49. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/importer/okf.py +0 -0
  50. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/importer/run.py +0 -0
  51. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/importer/stamp.py +0 -0
  52. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/index.py +0 -0
  53. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/markdown_parse.py +0 -0
  54. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/models.py +0 -0
  55. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/onboarding.py +0 -0
  56. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/onboarding_inflight.py +0 -0
  57. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/onboarding_playbook.py +0 -0
  58. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/pending.py +0 -0
  59. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/principals.py +0 -0
  60. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/prompts.py +0 -0
  61. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/push_queue.py +0 -0
  62. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/query_expansion.py +0 -0
  63. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/rate_limit.py +0 -0
  64. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/refresh.py +0 -0
  65. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/report.py +0 -0
  66. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/rest_api.py +0 -0
  67. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/safe_id.py +0 -0
  68. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/search_gate.py +0 -0
  69. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/search_shortcut.py +0 -0
  70. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/setup_wizard.py +0 -0
  71. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/thin_pointer.py +0 -0
  72. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/tools_audit.py +0 -0
  73. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/tools_enforce.py +0 -0
  74. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/tools_onboarding.py +0 -0
  75. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/tools_read.py +0 -0
  76. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/tools_write.py +0 -0
  77. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/trigram.py +0 -0
  78. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/viewer/__init__.py +0 -0
  79. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/viewer/generator.py +0 -0
  80. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/worktrees.py +0 -0
  81. {data_olympus-0.3.2 → data_olympus-0.3.3}/src/data_olympus/write_gate.py +0 -0
@@ -12,6 +12,27 @@ This project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
12
12
 
13
13
  ## [Unreleased]
14
14
 
15
+ ## [0.3.3] - 2026-07-05
16
+
17
+ ### Fixed
18
+
19
+ - **MCP streamable-HTTP session churn under long-lived clients.** A persistent
20
+ client (notably `opencode serve`) that holds the `GET /mcp` SSE stream open was
21
+ being forced to reconnect on a cycle, piling up abandoned transports on the
22
+ server and leaking event listeners on the client until it degraded. Two causes
23
+ addressed:
24
+ - The activity middleware now keeps a session non-idle for as long as its SSE
25
+ stream is actually open (re-stamping every `KB_SESSION_TOUCH_INTERVAL_SEC`,
26
+ default 30s, clamped to a third of the idle window), so the reaper evicts a
27
+ session only after its stream has genuinely closed. With that safety in
28
+ place, the idle-reap default (`KB_SESSION_IDLE_TIMEOUT_SEC`) drops from 1800s
29
+ to 300s, so an abandoned handshake is cleared in minutes instead of half an
30
+ hour.
31
+ - The shipped ingress manifest (`deploy/k8s/ingress.yaml`) now sets a long
32
+ `proxy-read-timeout`/`proxy-send-timeout` and disables buffering, so a proxy
33
+ with a short default idle timeout (nginx: 60s) no longer closes the SSE
34
+ stream and triggers the reconnect loop. See `docs/serving.md`.
35
+
15
36
  ## [0.3.2] - 2026-07-05
16
37
 
17
38
  ### Fixed
@@ -1029,7 +1050,8 @@ This project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
1029
1050
  - `docs/adoption.md`: bring-your-own-KB guide (author, lint, index, serve, wire an agent).
1030
1051
  - `docs/comparison.md`: how data-olympus relates to OKF, enterprise catalogs, markdown KB tools, agent-context conventions, RAG, and ADR tooling.
1031
1052
 
1032
- [Unreleased]: https://github.com/knaisoma/data-olympus/compare/v0.3.2...HEAD
1053
+ [Unreleased]: https://github.com/knaisoma/data-olympus/compare/v0.3.3...HEAD
1054
+ [0.3.3]: https://github.com/knaisoma/data-olympus/compare/v0.3.2...v0.3.3
1033
1055
  [0.3.2]: https://github.com/knaisoma/data-olympus/compare/v0.3.1...v0.3.2
1034
1056
  [0.3.1]: https://github.com/knaisoma/data-olympus/compare/v0.3.0...v0.3.1
1035
1057
  [0.3.0]: https://github.com/knaisoma/data-olympus/compare/v0.2.0...v0.3.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: data-olympus
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: Governance-grade knowledge-base format (OKF-compatible) plus CLI and single-writer MCP server
5
5
  Project-URL: Homepage, https://github.com/knaisoma/data-olympus
6
6
  Project-URL: Repository, https://github.com/knaisoma/data-olympus
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "data-olympus"
3
- version = "0.3.2"
3
+ version = "0.3.3"
4
4
  description = "Governance-grade knowledge-base format (OKF-compatible) plus CLI and single-writer MCP server"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
@@ -65,8 +65,15 @@ class Config:
65
65
  # Streamable-http session reaping: terminate transports idle beyond this many
66
66
  # seconds to bound _server_instances (see session_metrics). 0 disables the
67
67
  # reaper (observability-only). The scan runs every session_reap_interval_sec.
68
- session_idle_timeout_sec: int = 1800
68
+ # An in-flight GET SSE stream is kept non-idle by the activity middleware, so
69
+ # this window only clears sessions whose stream has actually closed (an
70
+ # abandoned handshake); 5 minutes keeps those from piling up.
71
+ session_idle_timeout_sec: int = 300
69
72
  session_reap_interval_sec: int = 60
73
+ # How often the activity middleware re-stamps a session while its GET SSE
74
+ # stream is open, so a quiet-but-connected client is never reaped. Clamped
75
+ # below session_idle_timeout_sec at wiring time.
76
+ session_touch_interval_sec: int = 30
70
77
  # Optional override for the status-aware reranker's status->weight map
71
78
  # (issue #37). None means "use the Index built-in default map". A negative
72
79
  # weight boosts an in-force status, a positive one penalizes a retired one.
@@ -191,8 +198,9 @@ def load_config() -> Config:
191
198
  auth_principals = parse_principals_env(os.getenv("KB_AUTH_PRINCIPALS", ""))
192
199
  consult_ttl_sec = int(os.getenv("KB_CONSULT_TTL_SEC", "300"))
193
200
  ledger_path = os.getenv("KB_LEDGER_PATH", "/state/ledger.json")
194
- session_idle_timeout_sec = int(os.getenv("KB_SESSION_IDLE_TIMEOUT_SEC", "1800"))
201
+ session_idle_timeout_sec = int(os.getenv("KB_SESSION_IDLE_TIMEOUT_SEC", "300"))
195
202
  session_reap_interval_sec = int(os.getenv("KB_SESSION_REAP_INTERVAL_SEC", "60"))
203
+ session_touch_interval_sec = int(os.getenv("KB_SESSION_TOUCH_INTERVAL_SEC", "30"))
196
204
  status_weights = _load_status_weights(os.getenv("KB_STATUS_WEIGHTS", ""))
197
205
  read_only = _env_bool(os.getenv("KB_READ_ONLY", ""))
198
206
  from data_olympus.cooccurrence import (
@@ -254,6 +262,7 @@ def load_config() -> Config:
254
262
  ledger_path=ledger_path,
255
263
  session_idle_timeout_sec=session_idle_timeout_sec,
256
264
  session_reap_interval_sec=session_reap_interval_sec,
265
+ session_touch_interval_sec=session_touch_interval_sec,
257
266
  status_weights=status_weights,
258
267
  read_only=read_only,
259
268
  cooccurrence_enabled=cooc_enabled,
@@ -251,8 +251,9 @@ def build_app(
251
251
  audit_hmac_key: str = "",
252
252
  audit_max_bytes: int = 0,
253
253
  ledger_path: str | None = None,
254
- session_idle_timeout_sec: int = 1800,
254
+ session_idle_timeout_sec: int = 300,
255
255
  session_reap_interval_sec: int = 60,
256
+ session_touch_interval_sec: int = 30,
256
257
  status_weights: dict[str, float] | None = None,
257
258
  read_only: bool = False,
258
259
  embeddings_enabled: bool = False,
@@ -300,6 +301,7 @@ def build_app(
300
301
  audit_max_bytes=audit_max_bytes,
301
302
  session_idle_timeout_sec=session_idle_timeout_sec,
302
303
  session_reap_interval_sec=session_reap_interval_sec,
304
+ session_touch_interval_sec=session_touch_interval_sec,
303
305
  status_weights=status_weights,
304
306
  read_only=read_only,
305
307
  embeddings_enabled=embeddings_enabled,
@@ -888,6 +890,7 @@ def build_app_from_config(config: Config, *, bootstrap_now: bool = True) -> Fast
888
890
  ledger_path=config.ledger_path,
889
891
  session_idle_timeout_sec=config.session_idle_timeout_sec,
890
892
  session_reap_interval_sec=config.session_reap_interval_sec,
893
+ session_touch_interval_sec=config.session_touch_interval_sec,
891
894
  status_weights=config.status_weights,
892
895
  read_only=config.read_only,
893
896
  embeddings_enabled=config.embeddings_enabled,
@@ -983,9 +986,25 @@ def main() -> None:
983
986
  )
984
987
 
985
988
  tracker = SessionActivityTracker()
989
+ # Re-stamp an in-flight SSE session at least 3x per idle window, so a
990
+ # quiet-but-connected client is never reaped even if an operator sets a short
991
+ # idle timeout. Falls back to the configured interval when the idle window is
992
+ # generous.
993
+ _idle = config.session_idle_timeout_sec
994
+ _touch_interval = float(config.session_touch_interval_sec)
995
+ if _idle > 0:
996
+ # Always strictly below the idle window (>= 3 touches per window) even for
997
+ # a tiny idle value; a small positive floor avoids a zero/negative sleep.
998
+ _touch_interval = max(0.1, min(_touch_interval, _idle / 3))
986
999
  http_app = app.http_app(
987
1000
  transport="streamable-http",
988
- middleware=[StarletteMiddleware(SessionActivityMiddleware, tracker=tracker)],
1001
+ middleware=[
1002
+ StarletteMiddleware(
1003
+ SessionActivityMiddleware,
1004
+ tracker=tracker,
1005
+ touch_interval_sec=_touch_interval,
1006
+ )
1007
+ ],
989
1008
  )
990
1009
  # Health surfaces the live session count; the manager is only reachable once
991
1010
  # the lifespan has started, so this returns None before then (never raises).
@@ -45,6 +45,7 @@ these internals, discovery returns ``None`` and the count is reported as
45
45
  from __future__ import annotations
46
46
 
47
47
  import asyncio
48
+ import contextlib
48
49
  import logging
49
50
  import time
50
51
  from typing import TYPE_CHECKING, Any, Protocol
@@ -195,20 +196,61 @@ class SessionActivityTracker:
195
196
 
196
197
  class SessionActivityMiddleware:
197
198
  """ASGI middleware that stamps :class:`SessionActivityTracker` on each request
198
- carrying an ``mcp-session-id`` header. Pure pass-through otherwise; adds no
199
- latency beyond a header scan."""
199
+ carrying an ``mcp-session-id`` header.
200
+
201
+ The ``GET /mcp`` Server-Sent-Events stream is a *single* long-lived request
202
+ that emits no further ASGI events while the session is quiet. Stamping only
203
+ at request start would let the idle reaper evict a still-connected client
204
+ after the idle window (forcing a reconnect, the exact churn that piles up
205
+ transports server-side and leaks listeners in a persistent client like
206
+ ``opencode serve``). So for an in-flight GET stream this middleware re-stamps
207
+ the session every ``touch_interval_sec`` for as long as the stream stays open;
208
+ when the client disconnects the request completes, re-stamping stops, and only
209
+ then does the session go idle and become reap-eligible. Short GET/POST
210
+ requests finish before the first tick, so they pay only the initial stamp.
211
+ """
200
212
 
201
- def __init__(self, app: ASGIApp, tracker: SessionActivityTracker) -> None:
213
+ def __init__(
214
+ self,
215
+ app: ASGIApp,
216
+ tracker: SessionActivityTracker,
217
+ *,
218
+ touch_interval_sec: float = 30.0,
219
+ ) -> None:
202
220
  self.app = app
203
221
  self._tracker = tracker
222
+ self._touch_interval = touch_interval_sec
204
223
 
205
224
  async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
206
- if scope.get("type") == "http":
207
- for key, value in scope.get("headers", []):
208
- if key.lower() == _MCP_SESSION_ID_HEADER and value:
209
- self._tracker.touch(value.decode("latin-1"))
210
- break
211
- await self.app(scope, receive, send)
225
+ if scope.get("type") != "http":
226
+ await self.app(scope, receive, send)
227
+ return
228
+ session_id = ""
229
+ for key, value in scope.get("headers", []):
230
+ if key.lower() == _MCP_SESSION_ID_HEADER and value:
231
+ session_id = value.decode("latin-1")
232
+ break
233
+ if not session_id:
234
+ await self.app(scope, receive, send)
235
+ return
236
+ self._tracker.touch(session_id)
237
+ # Only the long-lived GET SSE stream needs keep-alive re-stamping.
238
+ if scope.get("method") != "GET":
239
+ await self.app(scope, receive, send)
240
+ return
241
+
242
+ async def _keep_active() -> None:
243
+ while True:
244
+ await asyncio.sleep(self._touch_interval)
245
+ self._tracker.touch(session_id)
246
+
247
+ task = asyncio.create_task(_keep_active())
248
+ try:
249
+ await self.app(scope, receive, send)
250
+ finally:
251
+ task.cancel()
252
+ with contextlib.suppress(asyncio.CancelledError, Exception):
253
+ await task
212
254
 
213
255
 
214
256
  async def reap_idle_sessions(
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes