data-olympus 0.3.2__tar.gz → 0.3.4__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.
- {data_olympus-0.3.2 → data_olympus-0.3.4}/CHANGELOG.md +54 -1
- {data_olympus-0.3.2 → data_olympus-0.3.4}/PKG-INFO +1 -1
- {data_olympus-0.3.2 → data_olympus-0.3.4}/pyproject.toml +1 -1
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/config.py +11 -2
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/enforce_policy.py +15 -1
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/server.py +21 -2
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/session_metrics.py +51 -9
- {data_olympus-0.3.2 → data_olympus-0.3.4}/.gitignore +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/LICENSE +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/NOTICE +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/README.md +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/SPEC.md +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/benchmarks/README.md +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/bin/_kb_detect_workspace.sh +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/bin/_kb_enforce.py +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/bin/_kb_fallback.py +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/bin/kb +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/bin/kb-enforce-hook +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/bin/opencode/data-olympus-gate.ts +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/deploy/k8s/README.md +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/deploy/k8s/read-replica/README.md +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/docs/releases/README.md +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/example-bundle/projects/acme-app/README.md +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/example-bundle/projects/acme-app/components/api/README.md +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/__init__.py +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/audit_log.py +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/audit_trailers.py +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/auth.py +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/cli/__init__.py +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/cli/import_cmd.py +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/cli/indexgen.py +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/cli/main.py +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/cli/report_cmd.py +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/cooccurrence.py +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/dedup.py +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/durable.py +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/embeddings.py +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/format/__init__.py +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/format/document.py +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/format/frontmatter.py +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/format/lint.py +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/format/validate.py +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/git_ops.py +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/health.py +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/importer/__init__.py +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/importer/adr.py +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/importer/flat.py +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/importer/model.py +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/importer/okf.py +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/importer/run.py +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/importer/stamp.py +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/index.py +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/markdown_parse.py +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/models.py +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/onboarding.py +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/onboarding_inflight.py +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/onboarding_playbook.py +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/pending.py +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/principals.py +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/prompts.py +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/push_queue.py +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/query_expansion.py +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/rate_limit.py +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/refresh.py +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/report.py +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/rest_api.py +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/safe_id.py +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/search_gate.py +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/search_shortcut.py +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/setup_wizard.py +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/thin_pointer.py +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/tools_audit.py +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/tools_enforce.py +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/tools_onboarding.py +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/tools_read.py +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/tools_write.py +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/trigram.py +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/viewer/__init__.py +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/viewer/generator.py +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/worktrees.py +0 -0
- {data_olympus-0.3.2 → data_olympus-0.3.4}/src/data_olympus/write_gate.py +0 -0
|
@@ -12,6 +12,58 @@ This project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
12
12
|
|
|
13
13
|
## [Unreleased]
|
|
14
14
|
|
|
15
|
+
## [0.3.4] - 2026-07-05
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- **Enforcement gate false-positives on prose that merely mentions a governed
|
|
20
|
+
keyword, for tool calls with a known file path.** `IntentClassifier.classify`
|
|
21
|
+
scanned `action_diff` (the raw Write/Edit content or Bash command text, up
|
|
22
|
+
to 4000 chars) for free-text keywords like `auth`, `rls`, `tenant`, `secret`
|
|
23
|
+
in addition to `intent`, for every tool call. A scratch write whose text
|
|
24
|
+
simply discussed one of those words as a topic - not an actual governed
|
|
25
|
+
action - tripped the pre-tool gate regardless of the real (ungoverned)
|
|
26
|
+
write target, and a fresh `kb_consult` call did not clear it because the
|
|
27
|
+
consult's own classification (`intent`-only) and the gate's classification
|
|
28
|
+
(`intent + action_diff`) disagreed on whether the action was governed.
|
|
29
|
+
Keyword matching on `action_diff` now only applies when the tool call
|
|
30
|
+
carries no `action_path` (Bash commands and OpenCode's `patch`, whose
|
|
31
|
+
content is the only signal available); when an `action_path` is present
|
|
32
|
+
(Write/Edit/MultiEdit), classification uses the path globs and dependency
|
|
33
|
+
command patterns, unchanged, so this keeps full detection for real governed
|
|
34
|
+
edits (a patch touching `package.json`) without flagging unrelated prose in
|
|
35
|
+
files the path globs don't consider governed. Found via a company-knowledge
|
|
36
|
+
KB audit session that reproduced it live (a scratch `.txt` file containing
|
|
37
|
+
"RLS tenant boundaries auth trust model" was blocked with no governed path
|
|
38
|
+
or command in sight); independently reviewed via `codex exec` before
|
|
39
|
+
release.
|
|
40
|
+
- The audit's other finding (CX1: the pre-tool gate and the pre-commit
|
|
41
|
+
`report --staged` gate resolving two different workspace keys) was
|
|
42
|
+
already fixed in v0.2.0 (#64, worktree-invariant workspace key); no
|
|
43
|
+
further change needed. It was still visible in that audit only because
|
|
44
|
+
it predated the deployed kn-dev redeploy.
|
|
45
|
+
|
|
46
|
+
## [0.3.3] - 2026-07-05
|
|
47
|
+
|
|
48
|
+
### Fixed
|
|
49
|
+
|
|
50
|
+
- **MCP streamable-HTTP session churn under long-lived clients.** A persistent
|
|
51
|
+
client (notably `opencode serve`) that holds the `GET /mcp` SSE stream open was
|
|
52
|
+
being forced to reconnect on a cycle, piling up abandoned transports on the
|
|
53
|
+
server and leaking event listeners on the client until it degraded. Two causes
|
|
54
|
+
addressed:
|
|
55
|
+
- The activity middleware now keeps a session non-idle for as long as its SSE
|
|
56
|
+
stream is actually open (re-stamping every `KB_SESSION_TOUCH_INTERVAL_SEC`,
|
|
57
|
+
default 30s, clamped to a third of the idle window), so the reaper evicts a
|
|
58
|
+
session only after its stream has genuinely closed. With that safety in
|
|
59
|
+
place, the idle-reap default (`KB_SESSION_IDLE_TIMEOUT_SEC`) drops from 1800s
|
|
60
|
+
to 300s, so an abandoned handshake is cleared in minutes instead of half an
|
|
61
|
+
hour.
|
|
62
|
+
- The shipped ingress manifest (`deploy/k8s/ingress.yaml`) now sets a long
|
|
63
|
+
`proxy-read-timeout`/`proxy-send-timeout` and disables buffering, so a proxy
|
|
64
|
+
with a short default idle timeout (nginx: 60s) no longer closes the SSE
|
|
65
|
+
stream and triggers the reconnect loop. See `docs/serving.md`.
|
|
66
|
+
|
|
15
67
|
## [0.3.2] - 2026-07-05
|
|
16
68
|
|
|
17
69
|
### Fixed
|
|
@@ -1029,7 +1081,8 @@ This project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
1029
1081
|
- `docs/adoption.md`: bring-your-own-KB guide (author, lint, index, serve, wire an agent).
|
|
1030
1082
|
- `docs/comparison.md`: how data-olympus relates to OKF, enterprise catalogs, markdown KB tools, agent-context conventions, RAG, and ADR tooling.
|
|
1031
1083
|
|
|
1032
|
-
[Unreleased]: https://github.com/knaisoma/data-olympus/compare/v0.3.
|
|
1084
|
+
[Unreleased]: https://github.com/knaisoma/data-olympus/compare/v0.3.3...HEAD
|
|
1085
|
+
[0.3.3]: https://github.com/knaisoma/data-olympus/compare/v0.3.2...v0.3.3
|
|
1033
1086
|
[0.3.2]: https://github.com/knaisoma/data-olympus/compare/v0.3.1...v0.3.2
|
|
1034
1087
|
[0.3.1]: https://github.com/knaisoma/data-olympus/compare/v0.3.0...v0.3.1
|
|
1035
1088
|
[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.
|
|
3
|
+
Version: 0.3.4
|
|
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
|
|
@@ -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
|
-
|
|
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", "
|
|
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,
|
|
@@ -81,7 +81,21 @@ class IntentClassifier:
|
|
|
81
81
|
action_diff: str = "",
|
|
82
82
|
) -> ClassifyResult:
|
|
83
83
|
signals: list[str] = []
|
|
84
|
-
text
|
|
84
|
+
# Free-text keyword signals always scan `intent` (a short, deliberate
|
|
85
|
+
# statement of what the agent is about to do - kb_consult's argument -
|
|
86
|
+
# so a keyword there is a real signal). They scan `action_diff` too,
|
|
87
|
+
# but ONLY when there is no `action_path`: `action_diff` is arbitrary
|
|
88
|
+
# write/command CONTENT up to 4000 chars (Write.content, Edit.new_string,
|
|
89
|
+
# Bash.command, OpenCode patch text), and when a concrete action_path is
|
|
90
|
+
# already given, path-glob matching below is the intended signal for
|
|
91
|
+
# that action - layering the free-text net on top blocked benign writes
|
|
92
|
+
# to ungoverned files that merely discussed a keyword as a topic (a
|
|
93
|
+
# scratch file quoting "RLS"/"auth" from a security standard). Bash
|
|
94
|
+
# commands and OpenCode's `patch` tool carry no action_path at all
|
|
95
|
+
# (their action_diff IS the only content available), so the free-text
|
|
96
|
+
# net still applies there - dropping it entirely would silently lose
|
|
97
|
+
# the only governed-decision signal for those two tool shapes.
|
|
98
|
+
text = intent.lower() if action_path else f"{intent} {action_diff}".lower()
|
|
85
99
|
for kw, rx in self._keyword_res:
|
|
86
100
|
if rx.search(text):
|
|
87
101
|
signals.append(f"keyword:{kw}")
|
|
@@ -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 =
|
|
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=[
|
|
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.
|
|
199
|
-
|
|
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__(
|
|
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")
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
|
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
|
{data_olympus-0.3.2 → data_olympus-0.3.4}/example-bundle/projects/acme-app/components/api/README.md
RENAMED
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|