Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"email": "admin@ai-architect.tools"
},
"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.",
"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 hypermnesia-mcp-viz. The marketplace name stays cortex-plugins: it is the brand-level umbrella, while hypermnesia-mcp is one package inside it.",
"version": "4.17.2"
},
"plugins": [
Expand Down Expand Up @@ -58,7 +58,7 @@
"category": "productivity"
},
{
"name": "cortex-viz",
"name": "hypermnesia-mcp-viz",
"source": {
"source": "github",
"repo": "cdeust/cortex-viz"
Expand Down
23 changes: 22 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,22 @@ jobs:
# silently fall back. Raise the floor when coverage rises.
run: pytest --cov=mcp_server --cov-report=xml --cov-report=term-missing --cov-fail-under=82

# Match the Codex manifest's auto policy against a real, explicitly
# configured PostgreSQL instance. Explicit targets cannot fall back, so
# a successful memory_stats call proves PostgreSQL was selected.
- name: Verify Codex PostgreSQL-first MCP contract
if: matrix.python-version == '3.12'
env:
CORTEX_RUNTIME: cowork
HF_HUB_OFFLINE: "1"
TRANSFORMERS_OFFLINE: "1"
CORTEX_RERANKER_OFFLINE: "1"
run: >-
python scripts/verify_mcp_hosts.py
--clients codex-cli --profiles lean
--storage-selection auto
-- hypermnesia-mcp

# The advertised test count is the one claim the static gate cannot check
# without collecting the suite, so it is checked in the job that already
# has the suite installed. `--collect-only` re-collects (~seconds) rather
Expand Down Expand Up @@ -432,20 +448,25 @@ jobs:
"$cli_bin/codex" plugin list --json \
| python -c 'import json,sys; d=json.load(sys.stdin); p=[x for x in d.get("installed",[]) if x.get("pluginId")=="hypermnesia-mcp-codex@cortex-codex-plugins"]; assert len(p)==1, p; assert p[0]["installed"] and p[0]["enabled"]'
"$cli_bin/codex" mcp list --json \
| python -c 'import json,sys; ss=[s for s in json.load(sys.stdin) if s.get("name")=="cortex"]; assert len(ss)==1, ss; s=ss[0]; assert s["startup_timeout_sec"]==180.0, s; assert s["transport"]["command"]=="uvx", s'
| python -c 'import json,sys; ss=[s for s in json.load(sys.stdin) if s.get("name")=="cortex"]; assert len(ss)==1, ss; s=ss[0]; assert s["startup_timeout_sec"]==180.0, s; assert s["transport"]["command"]=="uvx", s; assert s["transport"]["env"]["CORTEX_RUNTIME"]=="cowork", s'
mapfile -t plugin_command < <(
python -c 'import json; s=json.load(open("plugins/hypermnesia-mcp-codex/.mcp.json"))["mcpServers"]["cortex"]; print(s["command"]); print(*s["args"], sep="\n")'
)
plugin_timeout="$(
python -c 'import json; print(json.load(open("plugins/hypermnesia-mcp-codex/.mcp.json"))["mcpServers"]["cortex"]["startup_timeout_sec"])'
)"
plugin_runtime="$(
python -c 'import json; print(json.load(open("plugins/hypermnesia-mcp-codex/.mcp.json"))["mcpServers"]["cortex"]["env"]["CORTEX_RUNTIME"])'
)"
CORTEX_RUNTIME="$plugin_runtime" \
UV_CACHE_DIR="$RUNNER_TEMP/cortex-codex-cold-uv-cache" \
UV_TOOL_DIR="$RUNNER_TEMP/cortex-codex-cold-uv-tools" \
PYTHONPATH="$GITHUB_WORKSPACE" \
python scripts/verify_mcp_hosts.py \
--timeout "$plugin_timeout" --clients codex-cli --profiles lean \
--command-includes-profile \
--allow-bootstrap-network \
--storage-selection auto \
-- "${plugin_command[@]}"

test-windows:
Expand Down
38 changes: 26 additions & 12 deletions docs/codex-plugin.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,33 +22,47 @@ the active working directory.
Add the Cortex repository marketplace and install the plugin:

```bash
uv tool install "hypermnesia-mcp[sqlite]"
codex plugin marketplace add cdeust/Cortex
codex plugin add hypermnesia-mcp-codex@cortex-codex-plugins
```

Restart the ChatGPT desktop app and start a new task so Codex loads the new
plugin components. The plugin uses `uvx`, so `uv` must be available on `PATH`.
The preliminary `uv tool install` is deliberate: it downloads the published
package before Codex's startup window, allowing the first plugin handshake to
reuse uv's local artifact cache.
The first launch installs both storage drivers. Cortex tries PostgreSQL first
at the configured `DATABASE_URL` (or its local `cortex` default), then falls
back to SQLite only when no explicit PostgreSQL target was supplied and the
default server is unavailable. An explicitly configured but unreachable
`DATABASE_URL` remains an error rather than silently redirecting writes.

An optional prewarm can download the package before restarting Codex; it is a
startup optimization, not an installation prerequisite:

```bash
uv tool install "hypermnesia-mcp[postgresql,sqlite]"
```

The bundled server declares `startup_timeout_sec: 180`. This is a bounded
startup ceiling, not a delay. On 2026-08-02, a local macOS 26.5.1 arm64 run
with uv 0.8.19 and clean `UV_CACHE_DIR` and `UV_TOOL_DIR` completed
`initialize`, `tools/list`, and `memory_stats` in 110.46 seconds with exactly
ten lean tools. A follow-up on the same machine after the tool installation
completed the same contract in 28.19 seconds. The clean `ubuntu-latest` CI
runner completed it in 23.87 seconds; CI reads the command and timeout from the
manifest itself.
startup ceiling, not a delay. On 2026-08-03, the exact two-driver command below
completed `initialize`, `tools/list`, and a real PostgreSQL-backed
`memory_stats` call in 103.84 seconds from clean `UV_CACHE_DIR` and
`UV_TOOL_DIR` directories on macOS 26.5.1 arm64 with uv 0.8.19. It exposed
exactly ten lean tools. The next offline run from that cache completed the same
contract in 2.75 seconds. CI reads the command, runtime policy, and timeout from
the manifest itself and repeats the clean-cache contract.

The bundled MCP command is equivalent to:

```bash
uvx --from "hypermnesia-mcp[sqlite]" \
env CORTEX_RUNTIME=cowork \
uvx --from "hypermnesia-mcp[postgresql,sqlite]" \
hypermnesia-mcp --profile lean
```

`CORTEX_RUNTIME=cowork` selects Cortex's existing DB-optional local-runtime
policy; it does not install or invoke the Cowork plugin. Claude Code remains
the primary integration and its manifest, hooks, agents, and full tool profile
are unchanged.

This is a local plugin. It does not make Cortex available to ChatGPT web and
does not expose the local memory database over the internet.

Expand Down
5 changes: 4 additions & 1 deletion plugins/hypermnesia-mcp-codex/.mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@
"command": "uvx",
"args": [
"--from",
"hypermnesia-mcp[sqlite]",
"hypermnesia-mcp[postgresql,sqlite]",
"hypermnesia-mcp",
"--profile",
"lean"
],
"env": {
"CORTEX_RUNTIME": "cowork"
},
"startup_timeout_sec": 180
}
}
Expand Down
29 changes: 27 additions & 2 deletions scripts/verify_mcp_hosts.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

CLIENTS = ("claude-code", "gemini-cli", "codex-cli")
PROFILES: tuple[Literal["full", "lean"], ...] = ("full", "lean")
STORAGE_SELECTIONS: tuple[Literal["sqlite", "auto"], ...] = ("sqlite", "auto")
# source: MCP protocol revision implemented by fastmcp==3.4.5.
PROTOCOL_VERSION = "2025-06-18"
# source: tests_py/test_main.py standalone baseline plus its three documented
Expand All @@ -48,6 +49,7 @@ class ContractCase:
data_root: Path
timeout: int
socks_proxy_regression: bool
storage_selection: Literal["sqlite", "auto"]

@property
def label(self) -> str:
Expand Down Expand Up @@ -82,15 +84,27 @@ def _frames(client_name: str) -> str:
)


def _environment(data_root: Path, *, socks_proxy_regression: bool) -> dict[str, str]:
def _environment(
data_root: Path,
*,
socks_proxy_regression: bool,
storage_selection: Literal["sqlite", "auto"] = "sqlite",
) -> dict[str, str]:
env = os.environ.copy()
env.update(
{
"CORTEX_CLAUDE_DIR": str(data_root),
"CORTEX_MEMORY_STORE_BACKEND": "sqlite",
"CORTEX_MEMORY_AP_ENABLED": "0",
}
)
if storage_selection == "sqlite":
env["CORTEX_MEMORY_STORE_BACKEND"] = "sqlite"
else:
# Exercise production auto-selection. In particular, do not inherit a
# repository- or runner-level SQLite override that would make a
# PostgreSQL-first smoke pass without ever attempting PostgreSQL.
env.pop("CORTEX_MEMORY_STORE_BACKEND", None)
env.pop("CORTEX_ALLOW_SQLITE_FALLBACK", None)
if socks_proxy_regression:
# Regression environment: FastMCP's banner-time update check used to
# import SOCKS support and abort before initialize. The MCP runtime
Expand Down Expand Up @@ -144,6 +158,7 @@ def _run_client(case: ContractCase) -> dict[int, dict[str, object]]:
env=_environment(
case.data_root / case.client_name / case.profile,
socks_proxy_regression=case.socks_proxy_regression,
storage_selection=case.storage_selection,
),
timeout=case.timeout,
check=False,
Expand Down Expand Up @@ -269,6 +284,15 @@ def main() -> int:
action="store_true",
help="do not inject the SOCKS regression fixture; intended for cold uvx",
)
parser.add_argument(
"--storage-selection",
choices=STORAGE_SELECTIONS,
default="sqlite",
help=(
"storage policy to exercise: explicit sqlite (default), or auto "
"for PostgreSQL-first selection with SQLite fallback"
),
)
parser.add_argument(
"command",
nargs=argparse.REMAINDER,
Expand Down Expand Up @@ -298,6 +322,7 @@ def main() -> int:
data_root=Path(temp_dir),
timeout=args.timeout,
socks_proxy_regression=not args.allow_bootstrap_network,
storage_selection=args.storage_selection,
)
count, elapsed_seconds = _verify(case)
print(
Expand Down
32 changes: 32 additions & 0 deletions tests_py/infrastructure/test_sqlite_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,38 @@ def test_no_explicit_url_cowork_falls_back_silently(self, monkeypatch, tmp_path)
store.close()
get_memory_settings.cache_clear()

def test_reachable_postgresql_is_preferred_over_sqlite(self, monkeypatch, tmp_path):
"""A reachable default PostgreSQL target wins before SQLite fallback."""
import mcp_server.infrastructure.memory_store as memory_store_module
from mcp_server.infrastructure.memory_config import get_memory_settings

pg_store = object()
monkeypatch.setenv("CORTEX_RUNTIME", "cowork")
monkeypatch.setenv("CORTEX_MEMORY_STORE_BACKEND", "auto")
monkeypatch.delenv("DATABASE_URL", raising=False)
monkeypatch.delenv("CORTEX_ALLOW_SQLITE_FALLBACK", raising=False)
monkeypatch.setattr(
memory_store_module,
"_try_pg_verbose",
lambda url: (pg_store, None),
)
monkeypatch.setattr(
memory_store_module,
"_make_sqlite",
lambda *args, **kwargs: pytest.fail("SQLite fallback was selected"),
)
get_memory_settings.cache_clear()

try:
assert (
memory_store_module._construct_store(
db_path=str(tmp_path / "unused.db")
)
is pg_store
)
finally:
get_memory_settings.cache_clear()

def test_postgresql_mode_unreachable_still_raises_unchanged(self, monkeypatch):
"""(d) STORE_BACKEND=postgresql (the CLI/explicit-postgresql path) with
PG unreachable must keep raising exactly as before this fix — this
Expand Down
20 changes: 16 additions & 4 deletions tests_py/scripts/test_codex_plugin_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,18 @@ def test_codex_plugin_is_mcp_only_and_uses_the_exact_lean_profile() -> None:
"command": "uvx",
"args": [
"--from",
"hypermnesia-mcp[sqlite]",
"hypermnesia-mcp[postgresql,sqlite]",
"hypermnesia-mcp",
"--profile",
"lean",
],
# Measured clean-cache startup on 2026-08-02: 110.46s on macOS 26.5.1
# arm64 with uv 0.8.19. This bounded ceiling leaves startup headroom
# without inventing a sleep or retry.
# Codex is additive, but its local storage selection has the same
# auto contract as the DB-optional sandbox surface: try PostgreSQL
# first and fall back only when no explicit DATABASE_URL was supplied.
"env": {"CORTEX_RUNTIME": "cowork"},
# Measured clean-cache startup with both extras on 2026-08-03: 103.84s
# on macOS 26.5.1 arm64 with uv 0.8.19. This bounded ceiling leaves
# startup headroom without inventing a sleep or retry.
"startup_timeout_sec": 180,
}

Expand All @@ -91,3 +95,11 @@ def test_codex_package_does_not_weaken_the_primary_claude_plugin() -> None:
"mcp_server",
]
assert "--profile" not in claude_server["args"]


def test_claude_marketplace_uses_the_canonical_viz_publication_identity() -> None:
marketplace = _json(REPO_ROOT / ".claude-plugin/marketplace.json")
names = {entry["name"] for entry in marketplace["plugins"]}

assert "hypermnesia-mcp-viz" in names
assert "cortex-viz" not in names
24 changes: 23 additions & 1 deletion tests_py/scripts/test_verify_mcp_hosts.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import pytest

from scripts.verify_mcp_hosts import ContractError, _responses
from scripts.verify_mcp_hosts import ContractError, _environment, _responses


def test_malformed_stdout_frame_is_reported_at_the_parse_boundary() -> None:
Expand All @@ -28,3 +28,25 @@ def test_valid_notification_is_ignored_but_response_is_retained() -> None:
assert _responses(stdout) == {
2: {"jsonrpc": "2.0", "id": 2, "result": {"tools": []}}
}


def test_default_environment_forces_the_isolated_sqlite_fixture(tmp_path) -> None:
env = _environment(tmp_path, socks_proxy_regression=False)

assert env["CORTEX_MEMORY_STORE_BACKEND"] == "sqlite"


def test_auto_environment_cannot_inherit_a_forced_sqlite_backend(
monkeypatch, tmp_path
) -> None:
monkeypatch.setenv("CORTEX_MEMORY_STORE_BACKEND", "sqlite")
monkeypatch.setenv("CORTEX_ALLOW_SQLITE_FALLBACK", "1")

env = _environment(
tmp_path,
socks_proxy_regression=False,
storage_selection="auto",
)

assert "CORTEX_MEMORY_STORE_BACKEND" not in env
assert "CORTEX_ALLOW_SQLITE_FALLBACK" not in env