From 9cf52e46e15bc2a26b51c2b7233d9df82883b375 Mon Sep 17 00:00:00 2001 From: cdeust Date: Sun, 2 Aug 2026 11:56:16 +0200 Subject: [PATCH 1/2] ci(release): mirror ci.yml network hardening in the release test gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tag pushes run `release.yml`'s own `test` job, but `ci.yml` triggers on `push: branches: [main]` + `pull_request` (ci.yml:4-8), so no tag has ever reached it. Every hardening pass CI absorbed since 2026-07-27 therefore skipped this file, and the two jobs silently diverged while running the same suite. Release run 30741657854 (v4.17.0) is what that divergence cost: test_spell_alteration.py::test_recall_real_spell_by_name -> pg_recall.py:443 -> reranker.py:109 -> FlashRank's bare `requests.get(..., stream=True)`, which carries no timeout, so a stalled connect hung in `sock.connect` until pytest-timeout killed the suite. HF_HUB_OFFLINE does not reach FlashRank's own fetch path, and reranker.py's `except Exception` cannot engage against a hang that never raises. All five downstream publish jobs were blocked on a tag whose tree had just passed 20 green checks on PR #334; the release shipped zero artifacts. Ported one-for-one from ci.yml:103-115,142-195 — cache + retried prefetch + offline test run, per model: - cache `~/.cache/flashrank`, matching reranker_model.py:104-113's `reranker_cache_dir()` (which honours $XDG_CACHE_HOME); - prefetch the reranker via `ensure_reranker_loaded()` and assert `state == 'loaded'`, so a failed fetch fails the step instead of surfacing later as first-stage-only recall scores (the 2026-07-10 FlashRank incident); - harden the HF prefetch to 5 retries with backoff and drop `continue-on-error`, so a blip cannot leave the cache empty and cascade into a misleading test failure; - run pytest with HF_HUB_OFFLINE / TRANSFORMERS_OFFLINE / CORTEX_RERANKER_OFFLINE, so no model download can happen mid-suite. ci.yml's three tree-sitter steps are deliberately NOT ported: requirements/ release.txt omits tree-sitter and tree-sitter-language-pack (as it omits igraph, leidenalg and texttable), so the AST tests skip in this job and there is no grammar to fetch — porting them would have failed on ImportError. A comment records the corollary: this gate tests a narrower surface than CI, and the three steps must follow if release.txt ever gains that dependency. Verified locally in .venv: `ensure_reranker_loaded()` returns state='loaded'. actionlint is not installed on this machine, so workflow validation was limited to a YAML parse plus review. Co-Authored-By: Claude Opus 5 --- .github/workflows/release.yml | 65 +++++++++++++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9535a579..9bf850de 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -117,23 +117,82 @@ jobs: [ -n "$pg_ready" ] || { echo "postgres failed to become ready after 30 attempts" >&2; exit 1; } PGPASSWORD=cortex psql -h localhost -U cortex -d cortex -c "SELECT version();" + # The network-hardening steps below mirror ci.yml's `test` job + # one-for-one (cache + retried prefetch + offline test run, per model). + # They are NOT optional here: this job runs the same suite on a tag + # push, but `ci.yml` triggers on `push: branches` + `pull_request`, so + # a tag never reaches it and every hardening pass CI received since + # 2026-07-27 skipped this file. Release run 30741657854 (v4.17.0) is + # what that divergence costs: the reranker fetch hung in `sock.connect` + # until pytest-timeout killed the suite, blocking all five downstream + # publish jobs on a tag whose tree had just passed 20 green checks on + # PR #334. Keep the two jobs in step — see ci.yml for the full + # per-incident rationale behind each step. - name: Cache HuggingFace models uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ~/.cache/huggingface key: ${{ runner.os }}-hf-all-MiniLM-L6-v2 + # FlashRank bypasses HF_HUB_OFFLINE entirely: it fetches its ONNX model + # with a bare `requests.get(..., stream=True)` carrying no timeout, so a + # stalled connect blocks the thread instead of raising, and reranker.py's + # `except Exception` cannot engage against a hang. + - name: Cache FlashRank reranker model + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/.cache/flashrank + key: ${{ runner.os }}-flashrank-ms-marco-MiniLM-L-12-v2 + - name: Install dependencies # Hash-pinned from uv.lock (scripts/generate_pip_constraints.py). run: | pip install --require-hashes -r requirements/release.txt pip install --no-deps -e . + # Retry with backoff and fail loudly (no continue-on-error): a transient + # blip must not leave the cache empty and cascade into a misleading test + # failure — or, worse here, into a silently degraded release gate. - name: Pre-download embedding model - run: python -c "from sentence_transformers import SentenceTransformer; SentenceTransformer('all-MiniLM-L6-v2', device='cpu')" - continue-on-error: true - + run: | + for attempt in 1 2 3 4 5; do + python -c "from sentence_transformers import SentenceTransformer; SentenceTransformer('all-MiniLM-L6-v2', device='cpu')" && exit 0 + echo "HF pre-download attempt ${attempt} failed; retrying in $((attempt * 10))s" >&2 + sleep $((attempt * 10)) + done + echo "HF pre-download failed after 5 attempts" >&2 + exit 1 + + # Deliberately runs WITHOUT CORTEX_RERANKER_OFFLINE: this is the one + # place allowed to download the model, so the test step below never can. + # `ensure_reranker_loaded` reports the load state instead of degrading + # silently, so a failed fetch fails this step rather than surfacing later + # as first-stage-only recall scores (the 2026-07-10 FlashRank incident). + - name: Pre-download reranker model + run: | + for attempt in 1 2 3 4 5; do + python -c "from mcp_server.core.reranker import ensure_reranker_loaded as e; s = e(); assert s.state == 'loaded', s" && exit 0 + echo "FlashRank pre-download attempt ${attempt} failed; retrying in $((attempt * 10))s" >&2 + sleep $((attempt * 10)) + done + echo "FlashRank pre-download failed after 5 attempts" >&2 + exit 1 + + # No tree-sitter cache/prefetch pair here, unlike ci.yml: requirements/ + # release.txt deliberately omits tree-sitter + tree-sitter-language-pack + # (as it omits igraph/leidenalg/texttable), so the AST tests skip in this + # job and there is no grammar to fetch. Should release.txt ever gain that + # dependency, port ci.yml's three tree-sitter steps across with it — the + # lazy `get_parser()` download is a real mid-suite DownloadError + # (main-red, CI run 30592244731, 2026-07-31). + # + # Offline: every model is already cached by the steps above, so tests + # must never reach out mid-suite — deterministic and flake-free. - name: Run tests + env: + HF_HUB_OFFLINE: "1" + TRANSFORMERS_OFFLINE: "1" + CORTEX_RERANKER_OFFLINE: "1" run: pytest --tb=short -q github-release: From 407020f035e7ee7c1721a49ebf01779f3bd60227 Mon Sep 17 00:00:00 2001 From: cdeust Date: Sun, 2 Aug 2026 11:59:22 +0200 Subject: [PATCH 2/2] =?UTF-8?q?release:=20v4.17.1=20=E2=80=94=20ship=20v4.?= =?UTF-8?q?17.0's=20tree=20under=20a=20version=20whose=20workflow=20works?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v4.17.0 was tagged but published nothing: its `test` job hung, blocking all five downstream publish jobs, so no GitHub release, no PyPI upload and no .mcpb bundle exist for it. The cause and its fix are the preceding commit. A new patch version rather than a moved tag, because a tag executes the `release.yml` of ITS OWN tree: `v4.17.0` points at 13278df4, which carries the unhardened workflow, so a re-run would replay the same hang; and recreating a published tag would break the "tree bit-identical to ae633a87" property the v4.17.0 release decision rests on. House precedent: v3.15.2 abandoned -> v3.15.3. v4.17.1 therefore carries v4.17.0's tree plus the workflow fix — no source change. Version surfaces moved 4.17.0 -> 4.17.1 across the nine sites that carry the release identity: pyproject.toml, server.json (document + pypi package), manifest.json, .claude-plugin/plugin.json, .claude-plugin/marketplace.json (metadata.version + the hypermnesia-mcp entry), uv.lock's root package, package.json, and the generated assets/badge-version.svg. The deprecated `cortex` marketplace entry stays pinned at 4.15.0 — a migration shim, not a shipped version. README's badge alt text, docs/ROADMAP.md's "where the project is today" line, and .bestpractices.json's three version justifications follow. manifest.json matters beyond bookkeeping here: the mcpb-bundle job refuses to pack when its version does not equal the tag (release.yml:419-421), so this is the file the v4.17.1 tag will be checked against. Gates: generate_repo_badges.py --check (4 badges), check_doc_claims.py, and check_marketplace_pins.py all exit 0; every touched JSON parses. Co-Authored-By: Claude Opus 5 --- .bestpractices.json | 6 +++--- .claude-plugin/marketplace.json | 4 ++-- .claude-plugin/plugin.json | 2 +- CHANGELOG.md | 6 ++++++ README.md | 2 +- assets/badge-version.svg | 8 ++++---- docs/ROADMAP.md | 2 +- manifest.json | 2 +- package.json | 2 +- pyproject.toml | 2 +- server.json | 4 ++-- uv.lock | 2 +- 12 files changed, 24 insertions(+), 18 deletions(-) diff --git a/.bestpractices.json b/.bestpractices.json index 7a49cd7c..48928c70 100644 --- a/.bestpractices.json +++ b/.bestpractices.json @@ -36,7 +36,7 @@ "english_justification": "README, CONTRIBUTING, SECURITY, PRIVACY, CHANGELOG, the docs/ tree and all issue discussion are written in English.", "maintained_status": "Met", - "maintained_justification": "Actively maintained: v4.17.0 was released 2026-08-01, and releases have shipped continuously through the v4.x series: https://github.com/cdeust/Cortex/releases", + "maintained_justification": "Actively maintained: v4.17.1 was released 2026-08-02, and releases have shipped continuously through the v4.x series: https://github.com/cdeust/Cortex/releases", "repo_public_status": "Met", "repo_public_justification": "The repository is public and readable without an account: https://github.com/cdeust/Cortex", @@ -51,10 +51,10 @@ "repo_distributed_justification": "git is a distributed version control system.", "version_unique_status": "Met", - "version_unique_justification": "Every release carries a unique semantic version tag (latest v4.17.0): https://github.com/cdeust/Cortex/releases", + "version_unique_justification": "Every release carries a unique semantic version tag (latest v4.17.1): https://github.com/cdeust/Cortex/releases", "version_semver_status": "Met", - "version_semver_justification": "Versions follow Semantic Versioning, tagged vMAJOR.MINOR.PATCH (v4.15.0, v4.16.0, v4.17.0): https://github.com/cdeust/Cortex/releases", + "version_semver_justification": "Versions follow Semantic Versioning, tagged vMAJOR.MINOR.PATCH (v4.16.0, v4.17.0, v4.17.1): https://github.com/cdeust/Cortex/releases", "version_tags_status": "Met", "version_tags_justification": "Each release has a corresponding git tag: https://github.com/cdeust/Cortex/tags", diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 460b3410..882702b9 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -6,14 +6,14 @@ }, "metadata": { "description": "The Cortex family of Claude Code plugins: home of hypermnesia-mcp (the persistent-memory server formerly named cortex, renamed in v4.15.0 over a directory name collision), zetetic-team-subagents, and cortex-viz. The marketplace name stays cortex-plugins: it is the brand-level umbrella, while hypermnesia-mcp is one package inside it.", - "version": "4.17.0" + "version": "4.17.1" }, "plugins": [ { "name": "hypermnesia-mcp", "source": "./", "description": "Cortex — persistent memory and cognitive profiling for Claude Code (renamed from the 'cortex' plugin in v4.15.0 to match the PyPI/MCP-registry identity hypermnesia-mcp) — thermodynamic memory with heat/decay, intent-aware retrieval, biological plasticity, codebase intelligence, and cognitive profiling. 50 MCP tools (53 with the optional automatised-pipeline + prd-spec-generator integrations) with enriched schemas (visualization extracted to the standalone cortex-viz MCP). PostgreSQL + pgvector in CLI mode; automatic SQLite fallback in Cowork/sandboxed mode. v3.17.0 — autonomous per-project wiki: SessionStart auto-spawns a 6-hour consolidate cycle; a headless `claude -p` worker drains the curation-gap queue, calls codebase-intelligence MCP tools to ground each section in the real call graph, and authors missing anchor pages (architecture / services / api / data-flow / operations / decisions / PRD) per project from the source tree. 15 canonical scopes × 13 file sections; per-project dashboards under `wiki/_dashboards/`. Mermaid diagrams have a 🔍 lens with zoom + pan. Workflow graph with caller-qualified CALLS chains rendering full method-to-method dependencies (native tree-sitter, no AP required). Side panel humanized for non-technical users. Ingests codebase analysis (ai-automatised-pipeline) and PRDs (prd-spec-generator) into wiki + memory + knowledge graph. Docker image available.", - "version": "4.17.0", + "version": "4.17.1", "author": { "name": "Clement Deust", "email": "admin@ai-architect.tools" diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index bcf33493..a1cf8880 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "hypermnesia-mcp", "description": "Cortex — persistent memory for Claude Code that remembers across sessions automatically. Install and forget. Scientific retrieval backed by 97 published references. (Renamed from the 'cortex' plugin to match the PyPI/MCP-registry identity.)", - "version": "4.17.0", + "version": "4.17.1", "author": { "name": "Clement Deust", "email": "admin@ai-architect.tools" diff --git a/CHANGELOG.md b/CHANGELOG.md index 63bc2a93..759252b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ adheres to [Semantic Versioning](https://semver.org/). ## [Unreleased] +## [4.17.1] - 2026-08-02 + +### Fixed + +- **The release workflow's test gate now carries ci.yml's network hardening, and v4.17.0's contents ship under this version** — `.github/workflows/release.yml`. v4.17.0 was tagged but **published nothing**: its `test` job hung and blocked all five downstream publish jobs, so no GitHub release, PyPI upload, or `.mcpb` bundle exists for it. Root cause is a trigger asymmetry, not a flake: `ci.yml` fires on `push: branches: [main]` + `pull_request` (ci.yml:4-8), so **a tag never reaches it** — every network-hardening pass CI absorbed since 2026-07-27 silently skipped `release.yml`, while both files kept running the same suite. Run 30741657854 is what that divergence cost: `test_recall_real_spell_by_name` → `pg_recall.py:443` → `reranker.py:109` → FlashRank's bare `requests.get(..., stream=True)`, which **carries no timeout**, so a stalled connect hung in `sock.connect` until pytest-timeout killed the suite — on a tree that had just passed 20 green checks on PR #334. `HF_HUB_OFFLINE` does not reach FlashRank's own fetch path, and `reranker.py`'s `except Exception` cannot engage against a hang that never raises. The `test` job now mirrors ci.yml:103-115,142-195 one-for-one — cache `~/.cache/flashrank` (matching `reranker_model.py:104-113`'s `reranker_cache_dir()`, which honours `$XDG_CACHE_HOME`), prefetch the reranker via `ensure_reranker_loaded()` asserting `state == 'loaded'` so a failed fetch fails the step instead of resurfacing as first-stage-only recall scores (the 2026-07-10 FlashRank incident), harden the HF prefetch to 5 retries with backoff and **drop `continue-on-error`** so a blip cannot leave the cache empty and cascade into a misleading failure, and run pytest under `HF_HUB_OFFLINE` / `TRANSFORMERS_OFFLINE` / `CORTEX_RERANKER_OFFLINE` so no download can happen mid-suite. ci.yml's three tree-sitter steps are **deliberately not ported**: `requirements/release.txt` omits tree-sitter and tree-sitter-language-pack (as it omits igraph, leidenalg and texttable), so the AST tests skip in this job and there is no grammar to fetch — porting them would have failed on ImportError. The corollary is recorded in the workflow itself: this gate tests a **narrower surface than CI**, and the three steps must follow if `release.txt` ever gains that dependency. Shipped as a new patch version rather than by moving the `v4.17.0` tag, because a tag executes the `release.yml` of **its own tree** — re-running v4.17.0 would replay the unhardened file — and rewriting a published tag would break the "tree bit-identical to ae633a87" property the v4.17.0 release decision rests on (house precedent: v3.15.2 abandoned → v3.15.3). No source change accompanies this fix; v4.17.1 carries the v4.17.0 tree plus the workflow. + ## [4.17.0] - 2026-08-01 ### Added diff --git a/README.md b/README.md index 6d8a74f1..8eecc9e5 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ License: MIT Python 3.10+ tests passing - 97 referenced papers Version 4.17.0 + 97 referenced papers Version 4.17.1 OpenSSF Best Practices MCP Toplist: Top 1.2% of 81,919 tracked MCP servers, July 2026

diff --git a/assets/badge-version.svg b/assets/badge-version.svg index 5d4aecd1..d5786cd8 100644 --- a/assets/badge-version.svg +++ b/assets/badge-version.svg @@ -1,5 +1,5 @@ - - Version 4.17.0 + + Version 4.17.1