Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
48 changes: 31 additions & 17 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,6 @@ jobs:
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]

services:
postgres:
image: pgvector/pgvector:pg17
env:
POSTGRES_USER: cortex
POSTGRES_PASSWORD: cortex
POSTGRES_DB: cortex
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U cortex"
--health-interval 10s
--health-timeout 5s
--health-retries 5

env:
DATABASE_URL: postgresql://cortex:cortex@localhost:5432/cortex

Expand All @@ -49,8 +34,37 @@ jobs:
with:
python-version: ${{ matrix.python-version }}

- name: Enable pg_trgm extension
run: PGPASSWORD=cortex psql -h localhost -U cortex -d cortex -c "CREATE EXTENSION IF NOT EXISTS pg_trgm;"
# Provision PostgreSQL + pgvector on the runner itself, instead of a
# Docker-Hub service container. Anonymous `pgvector/pgvector:pg17` pulls
# from registry-1.docker.io are rate-limited and outage-prone (observed:
# "context deadline exceeded" failing container init before any test ran,
# CI run 27190427877). The runner ships PostgreSQL preinstalled; pgvector
# comes from the PGDG apt repo (apt.postgresql.org) — no registry, no pull
# rate limit. source: runner image (actions/runner-images) + PGDG.
- name: Set up PostgreSQL + pgvector (runner-local, no registry pull)
run: |
set -euxo pipefail
sudo systemctl start postgresql
PG_VER="$(pg_lsclusters -h | awk 'NR==1 {print $1}')"
# Ensure the PGDG apt repo (canonical pgvector source); idempotent.
sudo install -d /usr/share/postgresql-common/pgdg
sudo curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc \
-o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc
echo "deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" \
| sudo tee /etc/apt/sources.list.d/pgdg.list
for i in 1 2 3; do sudo apt-get update && break || sleep 5; done
for i in 1 2 3; do sudo apt-get install -y "postgresql-${PG_VER}-pgvector" && break || sleep 5; done
# Role + DB expected by DATABASE_URL.
sudo -u postgres psql -v ON_ERROR_STOP=1 \
-c "CREATE ROLE cortex LOGIN SUPERUSER PASSWORD 'cortex';"
sudo -u postgres createdb -O cortex cortex
sudo -u postgres psql -v ON_ERROR_STOP=1 -d cortex \
-c "CREATE EXTENSION IF NOT EXISTS vector; CREATE EXTENSION IF NOT EXISTS pg_trgm;"
# Wait until reachable over TCP with password auth.
for i in $(seq 1 30); do
PGPASSWORD=cortex pg_isready -h localhost -U cortex -d cortex && break || sleep 1
done
PGPASSWORD=cortex psql -h localhost -U cortex -d cortex -c "SELECT version();"

- name: Cache pip
uses: actions/cache@v4
Expand Down
41 changes: 24 additions & 17 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,21 +30,6 @@ jobs:
name: Test before release
runs-on: ubuntu-latest

services:
postgres:
image: pgvector/pgvector:pg17
env:
POSTGRES_USER: cortex
POSTGRES_PASSWORD: cortex
POSTGRES_DB: cortex
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U cortex"
--health-interval 10s
--health-timeout 5s
--health-retries 5

env:
DATABASE_URL: postgresql://cortex:cortex@localhost:5432/cortex

Expand All @@ -56,8 +41,30 @@ jobs:
with:
python-version: "3.12"

- name: Enable pg_trgm extension
run: PGPASSWORD=cortex psql -h localhost -U cortex -d cortex -c "CREATE EXTENSION IF NOT EXISTS pg_trgm;"
# Runner-local PostgreSQL + pgvector (no Docker Hub pull). See ci.yml for
# the rationale: anonymous registry-1.docker.io pulls are rate-limited and
# outage-prone; PGDG apt is the canonical, unmetered pgvector source.
- name: Set up PostgreSQL + pgvector (runner-local, no registry pull)
run: |
set -euxo pipefail
sudo systemctl start postgresql
PG_VER="$(pg_lsclusters -h | awk 'NR==1 {print $1}')"
sudo install -d /usr/share/postgresql-common/pgdg
sudo curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc \
-o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc
echo "deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" \
| sudo tee /etc/apt/sources.list.d/pgdg.list
for i in 1 2 3; do sudo apt-get update && break || sleep 5; done
for i in 1 2 3; do sudo apt-get install -y "postgresql-${PG_VER}-pgvector" && break || sleep 5; done
sudo -u postgres psql -v ON_ERROR_STOP=1 \
-c "CREATE ROLE cortex LOGIN SUPERUSER PASSWORD 'cortex';"
sudo -u postgres createdb -O cortex cortex
sudo -u postgres psql -v ON_ERROR_STOP=1 -d cortex \
-c "CREATE EXTENSION IF NOT EXISTS vector; CREATE EXTENSION IF NOT EXISTS pg_trgm;"
for i in $(seq 1 30); do
PGPASSWORD=cortex pg_isready -h localhost -U cortex -d cortex && break || sleep 1
done
PGPASSWORD=cortex psql -h localhost -U cortex -d cortex -c "SELECT version();"

- name: Cache HuggingFace models
uses: actions/cache@v4
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,6 @@ benchmarks/snapshots/
.memsearch/

app-tauri/src-tauri/target/

# Vendored site-packages tree (pip --target install; not part of the repo)
/deps/
2 changes: 2 additions & 0 deletions mcp_server/core/wiki_coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -1205,6 +1205,8 @@ def audit_all_domains(
".venv",
"venv",
"env",
"deps",
"site-packages",
"__pycache__",
".mypy_cache",
".pytest_cache",
Expand Down
11 changes: 10 additions & 1 deletion mcp_server/core/wiki_drift.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,8 +186,17 @@ def _file_exists_under(source_root: str, cited: str) -> bool:
if os.path.isfile(full):
return True
bn = os.path.basename(cited)
# Prune the same vendored / build dirs as list_source_files. Without this,
# a repo carrying a venv/, node_modules/, deps/, or site-packages/ at its
# root makes this per-cited-path fallback walk tens of thousands of files,
# turning one consolidate cycle into a multi-minute stall. The skip set is
# the single source of truth for "not a source tree".
from mcp_server.core.wiki_coverage import _SKIP_DIRECTORIES

for dirpath, dirnames, filenames in os.walk(source_root):
dirnames[:] = [d for d in dirnames if not d.startswith(".")]
dirnames[:] = [
d for d in dirnames if d not in _SKIP_DIRECTORIES and not d.startswith(".")
]
if bn in filenames:
return True
return False
Expand Down
4 changes: 2 additions & 2 deletions mcp_server/handlers/add_rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from typing import Any

from mcp_server.infrastructure.memory_config import get_memory_settings
from mcp_server.infrastructure.memory_store import MemoryStore
from mcp_server.infrastructure.memory_store import MemoryStore, get_shared_store
from mcp_server.handlers._tool_meta import IDEMPOTENT_WRITE

# ── Schema ────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -111,7 +111,7 @@ def _get_store() -> MemoryStore:
global _store
if _store is None:
settings = get_memory_settings()
_store = MemoryStore(settings.DB_PATH, settings.EMBEDDING_DIM)
_store = get_shared_store(settings.DB_PATH, settings.EMBEDDING_DIM)
return _store


Expand Down
4 changes: 2 additions & 2 deletions mcp_server/handlers/anchor.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from typing import Any

from mcp_server.infrastructure.memory_config import get_memory_settings
from mcp_server.infrastructure.memory_store import MemoryStore
from mcp_server.infrastructure.memory_store import MemoryStore, get_shared_store
from mcp_server.handlers._tool_meta import IDEMPOTENT_WRITE

# ── Schema ────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -76,7 +76,7 @@ def _get_store() -> MemoryStore:
global _store
if _store is None:
settings = get_memory_settings()
_store = MemoryStore(settings.DB_PATH, settings.EMBEDDING_DIM)
_store = get_shared_store(settings.DB_PATH, settings.EMBEDDING_DIM)
return _store


Expand Down
4 changes: 2 additions & 2 deletions mcp_server/handlers/assess_coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from typing import Any

from mcp_server.infrastructure.memory_config import get_memory_settings
from mcp_server.infrastructure.memory_store import MemoryStore
from mcp_server.infrastructure.memory_store import MemoryStore, get_shared_store
from mcp_server.handlers._tool_meta import READ_ONLY

# ── Schema ────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -76,7 +76,7 @@ def _get_store() -> MemoryStore:
global _store
if _store is None:
settings = get_memory_settings()
_store = MemoryStore(settings.DB_PATH, settings.EMBEDDING_DIM)
_store = get_shared_store(settings.DB_PATH, settings.EMBEDDING_DIM)
return _store


Expand Down
4 changes: 2 additions & 2 deletions mcp_server/handlers/backfill_memories.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
slug_to_domain,
)
from mcp_server.infrastructure.memory_config import get_memory_settings
from mcp_server.infrastructure.memory_store import MemoryStore
from mcp_server.infrastructure.memory_store import MemoryStore, get_shared_store
from mcp_server.infrastructure.scanner import read_head_tail
from mcp_server.handlers._tool_meta import NON_IDEMPOTENT_WRITE

Expand Down Expand Up @@ -324,7 +324,7 @@ async def handler(args: dict[str, Any] | None = None) -> dict[str, Any]:
"""Backfill prior conversations into the memory store."""
parsed = _parse_args(args)
settings = get_memory_settings()
store = MemoryStore(settings.DB_PATH, settings.EMBEDDING_DIM)
store = get_shared_store(settings.DB_PATH, settings.EMBEDDING_DIM)
ensure_backfill_log(store)

candidates = discover_files(parsed["project_filter"], parsed["max_files"])
Expand Down
4 changes: 2 additions & 2 deletions mcp_server/handlers/checkpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from mcp_server.core.replay import format_restoration
from mcp_server.handlers._tool_meta import IDEMPOTENT_WRITE
from mcp_server.infrastructure.memory_config import get_memory_settings
from mcp_server.infrastructure.memory_store import MemoryStore
from mcp_server.infrastructure.memory_store import MemoryStore, get_shared_store

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -150,7 +150,7 @@ def _get_store() -> MemoryStore:
global _store
if _store is None:
settings = get_memory_settings()
_store = MemoryStore(settings.DB_PATH, settings.EMBEDDING_DIM)
_store = get_shared_store(settings.DB_PATH, settings.EMBEDDING_DIM)
return _store


Expand Down
4 changes: 2 additions & 2 deletions mcp_server/handlers/codebase_analyze.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
)
from mcp_server.handlers.remember import handler as remember_handler
from mcp_server.infrastructure.memory_config import get_memory_settings
from mcp_server.infrastructure.memory_store import MemoryStore
from mcp_server.infrastructure.memory_store import MemoryStore, get_shared_store
from mcp_server.handlers._tool_meta import READ_ONLY

# ── Schema ────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -143,7 +143,7 @@ def _get_store() -> MemoryStore:
global _store
if _store is None:
s = get_memory_settings()
_store = MemoryStore(s.DB_PATH, s.EMBEDDING_DIM)
_store = get_shared_store(s.DB_PATH, s.EMBEDDING_DIM)
return _store


Expand Down
20 changes: 17 additions & 3 deletions mcp_server/handlers/consolidate.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
get_embedding_engine,
)
from mcp_server.infrastructure.memory_config import get_memory_settings
from mcp_server.infrastructure.memory_store import MemoryStore
from mcp_server.infrastructure.memory_store import MemoryStore, get_shared_store
from mcp_server.handlers._tool_meta import IDEMPOTENT_WRITE

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -161,7 +161,7 @@ def _get_store() -> MemoryStore:
global _store
if _store is None:
settings = get_memory_settings()
_store = MemoryStore(settings.DB_PATH, settings.EMBEDDING_DIM)
_store = get_shared_store(settings.DB_PATH, settings.EMBEDDING_DIM)
return _store


Expand All @@ -183,6 +183,20 @@ def _timed(fn, *args, **kwargs) -> dict[str, Any]:
return result


async def _atimed(fn, *args, **kwargs) -> dict[str, Any]:
"""Async counterpart of :func:`_timed` for awaitable cycle functions."""
t0 = time.monotonic()
try:
result = await fn(*args, **kwargs) or {}
except Exception as exc:
ms = int((time.monotonic() - t0) * 1000)
return {"error": f"{type(exc).__name__}: {exc}", "duration_ms": ms}
ms = int((time.monotonic() - t0) * 1000)
if isinstance(result, dict):
result["duration_ms"] = ms
return result


async def handler(args: dict[str, Any] | None = None) -> dict[str, Any]:
"""Run maintenance cycles on the memory system."""
args = args or {}
Expand Down Expand Up @@ -212,7 +226,7 @@ async def handler(args: dict[str, Any] | None = None) -> dict[str, Any]:
if args.get("wiki", True):
cap_raw = args.get("wiki_max_purges_per_axis", 500)
cap = int(cap_raw) if cap_raw is not None and int(cap_raw) > 0 else None
wiki_stats = _timed(
wiki_stats = await _atimed(
run_wiki_maintenance,
store,
apply_stubs=bool(args.get("wiki_apply_stubs", True)),
Expand Down
Loading
Loading